%% # 纲要 > 主干纲要、Hint/线索/路标 # Q&A #### 已明确 #### 待明确 > 当下仍存有的疑惑 **❓<font color="#c0504d"> 有什么问题?</font>** %% # 内存映射 > 参见 [^1] [^2] [^3] mmap 的作用是**内存映射**——向**内核请求申请一块新的虚拟地址空间**,将一个文件或特殊对象**映射到该内存空间**。 ### 三种使用方式 | 使用方式 | 说明 | | ------------------------------------------- | ---------------------------------------------------------------------------- | | (1)为 "**普通文件**" 提供 "**==内存映射 I/O==**" | 磁盘上文件映射到内存后,直接**通过操作内存来读/写文件内容** | | (2)为 "**POSIX 共享内存对象**" 提供 "**==共享内存映射==**" | 实现任意进程间 "**共享内存**"。 | | (3)为进程直接提供 "**==匿名内存映射==**" | 进程内直接分配一块 "**匿名内存**"(内容全为**二进制 0**),无关联文件。 <br>(只能在关联进程间实现**共享内存**,例如父子进程间) | ##### (1)映射文件——内存映射 I/O 1. 调用 `open` 打开一个磁盘文件,获取 `fd`; 2. 通过 `mmap` 将 `fd` 关联文件**映射到内存** ```c int fd = open("file.txt", O_RDWR | O_CREAT); void* addr = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); ``` ##### (2)映射共享内存对象——共享内存 1. 调用 `shm_open` 打开一个 POSIX 共享内存对象,获取 `fd`; 2. 通过 `mmap` 将 `fd` 关联对象**映射到内存** ```c int shm_fd = shm_open(shm_name, O_CREAT | O_RDWR, 0666); void* addr = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); ``` ##### (3)匿名内存映射 匿名内存映射:设置`MAP_ANON`标志位, fd 参数传入为 `-1`。 ```c // // 直接分配一块4KB的匿名内存 void* addr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); ``` <br><br> ## 内存映射 API ### mmap 函数 ![[_attachment/02-开发笔记/05-操作系统/进程管理/共享内存.assets/IMG-共享内存-CADADFAF33281153CB32F504EE9AEC69.png|706]] 参数说明: - `addr`:被映射至的虚拟内存地址(需搭配 `MAP_FIXED`) - 未指定 `MAP_FIXED` 时仅作为建议地址,**不强制**。故该情况下通常**设置为 `NULL`**,表示由内核自动选择合适地址(页对齐) - `length`:映射的**字节数**; - `prot`:保护标志,指定**对映射区域的访问权限**,可选项为: - `PROT_READ`:可读 - `PROT_WRITE`:可写入 - `PROT_EXEC`:可执行 - `PROT_NONE`:不可访问 - `flags`:映射方式,其中 `MAP_SHARED` 与 `MAP_PRIVATE` 必须指定其一; - `MAP_SHARED`:**==共享映射==**,对 "**映射内存**" 的修改会**同步**到 "**底层被映射的文件/对象**",**反之亦然**。 - `MAP_PRIVATE`:**==私有映射==**(**==写时拷贝==**),对 "**映射内存**" 修改只在当前进程的内存中可见,**不改变底层文件/对象**,`munmap` 后将丢弃所有内存中变更。 - `MAP_ANON`:**匿名映射**,不与文件关联; - Linux 下定义了等价的 `MAP_ANONYMOUS` 标志[^5] - `MAP_FIXED`:**强制按指定地址映射**,若不可行则出错返回 `MAP_FAILED`; - `fd`:文件描述符,表示**需要被映射的文件**。若使用匿名映射,则 `fd` 应设置为-1; - `offset`:文件中**被映射的起始位置的偏移量**,必须是页面大小的整数倍。 > [!note] mmap 成功返回后,`fd` 关联文件可被关闭,不影响已建立的映射。 > [!NOTE] mmap 映射 "普通文件"[^1] > > ![[_attachment/02-开发笔记/05-操作系统/进程管理/共享内存.assets/IMG-共享内存-25667BB1CF7B2EC3E1C75172D100A5CF.png|574]] > [!important] fork 对 mmap 的影响 > > fork 形成的父子进程间 **初始时共享物理内存页帧** 且受 "**写时拷贝**"(COW)机制的影响。 > > 对于父进程**在 fork 之前通过 mmap 映射的内存区域**,取决于 `mmap` 映射方式[^2]: > > - `MAP_SHARED` :**父子进程对映射内存的写入直接==同步==到被映射的原文件**,不涉及写时拷贝,父子进程 **==共享映射内存区的物理页==**。 > - `MAP_PRIVATE` :父子进程对映射内存的写入**不影响原文件**,写入**触发==写时拷贝==**,变更仅在执行写入的进程内可见,如下图: > > ![[_attachment/02-开发笔记/05-操作系统/进程管理/共享内存.assets/IMG-共享内存-39D633740F0D6337BD3F5AD7777C04E5.png|722]] > > <br> #### 🚨 mmap 背后的内存分配 两点注意事项[^4]: 1. OS 以 "**==页==**" 为单位分配内存,故**实际分配给 `mmap` 的内存块大小是 =="页大小" 的整数倍==**。 2. mmap 请求大小 "**可超出文件大小所需的整数内存页**",但 **==超出部分的页==不可访问**,将触发 **SIGBUS**。 > [!example] 示例一[^4] > > 由于按页分配,故按 **"被映射文件大小"** 进行映射时,实际分配的内存可能大于 "**被映射文件的实际大小**"。 > 最后一页中**超过文件大小的部分,均为二进制 0**。这部分内存 **==可被读/写==**,但**不会影响到被映射的原文件**。 > > ![[_attachment/02-开发笔记/05-操作系统/进程管理/共享内存.assets/IMG-共享内存-E48873AA1EDB93EB2B64D84982FA67CA.png|477]] > > 验证示例: > ![[_attachment/02-开发笔记/05-操作系统/进程管理/共享内存.assets/IMG-共享内存-A086F59CB072C5AAEE188F9A8DAA3D0C.png|472]] > > [!example] 示例二[^4] > > 当 mmap 请求的映射大小 "**超出文件大小所需的整数页**" 时,尽管 mmap 成功返回,但**超出部分的页不可访问**,如下所示: > > ![[_attachment/02-开发笔记/05-操作系统/进程管理/共享内存.assets/IMG-共享内存-A2E9BD893A15609B3C6413457A90DAC3.png|565]] > 验证示例: > 文件大小实际只需 1 个页,但 `mmap` 请求了 5000 字节故 **OS 为其分配 2 个页**,然而访问第二个页时触发 SIGBUS。 > > ![[_attachment/02-开发笔记/05-操作系统/进程管理/共享内存.assets/IMG-共享内存-E9DD45D7BB6D2B9616EFC95CAD52962B.png|526]] > > > [!error] mmap 相关的错误信号: SIGSEGV 与 SIGBUS > > - **SIGBUS**:是在内存映射区内访问,但 "**已超出了底层支撑对象的大小**"。 > - 若 mmap 请求的内存 "**超出文件大小所需的整数页**",则**超出部分的页不可访问**,将触发 **SIGBUS**。 > - **SIGSEGV**:非法内存引用,触发原因包括两种: > 1. 尝试**写入设置为 "==只读==" 的映射内存区**; > 2. 尝试访问 "**映射内存区**" 之外的非法地址。 <br><br> ### munmap 函数 > ![[_attachment/02-开发笔记/05-操作系统/进程管理/共享内存.assets/IMG-共享内存-350240AFED53D9D1EBF8F1F5C2E9EE0D.png|704]] `munmap` 用以从进程的地址空间中 "**删除一个映射关系**"。 <br> ### msync 函数 > ![[_attachment/02-开发笔记/05-操作系统/进程管理/共享内存.assets/IMG-共享内存-BFE3FE9FAE692F2B2C8618510B13D526.png|729]] `MAP_SHARED` 映射方式下,进程对映射内存的修改会**同步**到 "**底层被映射的文件/对象**"——由内核**在稍后某个时刻**来完成对文件的更新。 `msync` 函数用以 **立即执行同步**,**将修改过的页刷入到被映射文件中**,类似于 `fsync`。 > [!NOTE] 若映射方式为 `MAP_PRIVATE`,则 `msync` 无效,不会修改被映射的文件。 <br><br><br> # 共享内存 共享内存:多个进程的虚拟内存空间共享 "**==相同的物理页帧==**",从而实现**进程间通信**。 特点: - **高效**:在所有 IPC 机制中最为高效,进程间通过直接访问内存来通信,**不需要经由内核进行数据传递/拷贝**。 - **需要同步机制**:保证多个进程间**读写共享内存时互斥** ### OS 提供的共享内存接口 共享内存 API 有两种标准实现: - **POSIX 共享内存**: - `<sys/mman.h>` 头文件中,提供了 `shm_open`, `mmap`, `shm_unlink`, `munmap` 等方法来操作共享内存 - **System V 共享内存**: - `<sys/shm.h>` 头文件中,提供了 `shmget`, `shmat`, `shmdt`, `shmctl` 等接口来操作共享内存 ![[_attachment/02-开发笔记/05-操作系统/进程管理/共享内存.assets/IMG-共享内存-1B3429B758C8E43F3C0F36AC70D5146F.png|502]] 以 POSIX 共享内存为例。使用步骤大致如下 1. **创建共享内存对象**:由某个进程通过 `shm_open` 首先创建一个**共享内存对象**,其余进程则可以打开已存在的同名对象。 2. **映射共享内存对象至进程的地址空间**:各进程通过 `mmap` 以 `MAP_SHARED` 方式**将对象映射至其地址空间**。 3. **数据访问**:进程通过指针**直接读/写==映射内存==中的数据** 4. **同步管理**:需使用额外的同步机制(如互斥锁)**协调多个进程对共享内存中同一块数据的读写**; 5. **移除映射内存、删除共享内存对象**: - `munmap(addr, size)` :从进程地址空间中**移除一段映射内存**; - `shm_unlink(shm_name)`:删除共享内存对象 <br><br> # POSIX 共享内存 POSIX 标准提供了两种**共享内存**方式: 1. 映射**普通文件**——由`open` 打开并由 `mmap` 映射到各进程的地址空间。 2. 映射 **POSIX ==共享内存对象==**——由 `shm_open` 打开一个 POSX IPC 名并返回 fd,再由 `mmap` 映射到各进程空间。 > [!NOTE] 两种方式差别在于 "**获取文件描述符的方式**" > > 获取 `fd` 后**均由 `mmap` 以 `MAP_SHARED` 方式映射到各进程的地址空间**,各进程的映射内存区**指向==相同的物理页帧==**,从而实现共享内存。 > > ![[_attachment/02-开发笔记/05-操作系统/进程管理/共享内存.assets/IMG-共享内存-1D48043FD2B5CB7C146838CBE7B13AFE.png|543]] > <br> ## POSIX 共享内存对象 ![[_attachment/02-开发笔记/05-操作系统/进程管理/共享内存.assets/IMG-共享内存-F712070CAC143EA5D1D93A7493E03C49.png|763]] - `shm_open` 用以 **新建或打开一个已存在的共享内存区对象**,返回其**文件描述符**。 - `shm_unlink` 用以删除一个共享内存对象。(**不影响已映射的共享内存**) > [!NOTE] `shm_open` 本质上是通过 tmpfs 伪文件系统创建了一个 "**匿名内存文件**"。 > > tmpfs 是挂载在 `/dev/shm` 的 "伪文件系统",其本身**常驻内存**中, > 而 `shm_open` 本质上是在该伪文件系统中 **创建/打开一个内存文件**。 > > 可通过 `ls -l /dev/shm` 查看已创建的所有 POSIX 共享内存对象: > > ![[_attachment/02-开发笔记/05-操作系统/进程管理/共享内存.assets/IMG-共享内存-C35A7C400C42E1F653ACE799D4D3B491.png|685]] > > 因此多个进程通过 `mmap` 映射**同一共享内存对象**时,实质上即是**将各进程的映射内存指向了 "同一物理页帧"**,从而实现共享。 <br> ### 共享内存示例 ```c #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/mman.h> #include <fcntl.h> #include <semaphore.h> #include <string.h> #include <sys/wait.h> /* 使用POSIX共享内存对象与mmap实现进程间共享内存 * 1) shm_open打开POSIX共享内存对象 * 2) 各进程通过mmap将其映射至自己的虚拟内存空间. */ struct ShmData { int count; sem_t mutex; } shm_data; const int loops = 10; int main() { // 共享内存对象 const char *shm_name = "/my_shm"; // 共享内存对象的名称 const size_t shm_size = sizeof(struct ShmData); // 共享内存的大小 struct ShmData *shm_ptr; // 指向共享内存的指针 int shm_fd; // 共享内存对象的文件描述符 // 创建或打开共享内存对象, 返回文件描述符 shm_fd = shm_open(shm_name, O_CREAT | O_RDWR, 0666); if (shm_fd == -1) { perror("shm_open"); exit(EXIT_FAILURE); } // 设置共享内存对象的大小 if (ftruncate(shm_fd, shm_size) == -1) { perror("ftruncate"); exit(EXIT_FAILURE); } // 对shm_data结构体初始化, 而后将其作为数据写入共享内存对象. shm_data.count = 0; sem_init(&shm_data.mutex, 1, 1); write(shm_fd, &shm_data, shm_size); // 使用fork()创建子进程 if (fork() == 0) { shm_ptr = (struct ShmData*)mmap(NULL, shm_size, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0); close(shm_fd); // 关闭共享内存对象的文件描述符(完成映射后即可关闭) printf("子进程[%d]: mmap addr: %p\n", getpid(), shm_ptr); for (int i = 0; i < loops; ++i) { sem_wait(&shm_ptr->mutex); printf("子进程[%d]: count: %d\n", getpid(), shm_ptr->count++); sem_post(&shm_ptr->mutex); } // 解除映射 munmap(shm_ptr, shm_size); exit(EXIT_SUCCESS); } // 父进程 void* addr = (void*)0x7ffff7ffb000; // 固定地址需使用`MAP_FIXED` shm_ptr = (struct ShmData*)mmap(addr, shm_size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, shm_fd, 0); close(shm_fd); // 关闭共享内存对象的文件描述符(完成映射后即可关闭) printf("父进程[%d]: mmap addr: %p\n", getpid(), shm_ptr); for (int i = 0; i < loops; ++i) { sem_wait(&shm_ptr->mutex); printf("父进程[%d]: count: %d\n", getpid(), shm_ptr->count++); sem_post(&shm_ptr->mutex); } // 解除映射 munmap(shm_ptr, shm_size); // 删除共享内存对象 if (shm_unlink(shm_name) == -1) { perror("shm_unlink"); exit(EXIT_FAILURE); } wait(NULL); return 0; } ``` %% # System V 共享内存 ```c #include <sys/ipc.h> #include <sys/shm.h> #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <sys/wait.h> #include <string.h> int main() { key_t key; int shmid; char *shm_ptr; // 生成一个共享内存对象的键, 用作外部名 key = ftok("shmfile", 65); // 创建一个大小为1024字节的共享内存段 shmid = shmget(key, 1024, 0666 | IPC_CREAT); if (shmid == -1) { perror("shmget"); exit(EXIT_FAILURE); } // 将共享内存段附加到当前进程的地址空间 shm_ptr = (char*) shmat(shmid, NULL, 0); if (shm_ptr == (char*) -1) { perror("shmat"); exit(EXIT_FAILURE); } // 使用fork()创建子进程 pid_t pid = fork(); if (pid < 0) { perror("fork"); exit(EXIT_FAILURE); } else if (pid == 0) { // 子进程: 向共享内存写入数据 printf("子进程(%d) 正向共享内存写入数据...\n", getpid()); const char* msg = "Hello from child process!"; strncpy(shm_ptr, msg, strlen(msg) + 1); printf("子进程(%d) 已完成写入\n", getpid()); exit(EXIT_FAILURE); } else { // 父进程: 等待子进程完成 wait(NULL); // 读取共享内存中的数据 printf("父进程(%d) 从共享内存读取数据: %s\n", getpid(), shm_ptr); // 分离共享内存 if (shmdt(shm_ptr) == -1) { perror("shmdt"); exit(EXIT_FAILURE); } // 删除共享内存段 if (shmctl(shmid, IPC_RMID, NULL) == -1) { perror("shmctl"); exit(EXIT_FAILURE); } } return 0; } ``` %% <br><br> # Buffer ## 闪念 > sudden idea ## 候选资料 > Read it later <br> # ♾️参考资料 # Footnotes [^1]: 《UNIX 环境高级编程》(P424) [^2]: 《深入理解计算机系统》P583~P584 [^3]: 《UNIX 网络编程卷 2:进程间通信》(P247) [^4]: 《UNIX 网络编程卷 2:进程间通信》(P257~258) [^5]: 《UNIX 环境高级编程》(P465)