「Linux 系统编程」mmap 建立共享内存映射
1 什么是 mmap
mmap
(Memory-mapped I/O)是一种高效的 磁盘文件与内存缓冲区映射的机制。通过 mmap
可以将一个磁盘文件直接映射到进程的地址空间中,使得 对内存的读写操作直接反映到文件上,无需使用传统的 read
和 write
函数。
2 mmap 与 munmap 函数
2.1 mmap 函数
mmap
函数用于 创建一个共享内存映射区。
函数原型:
1 |
|
参数说明:
addr
: 指定 映射区的首地址。通常传入NULL,表示由系统自动分配length
: 映射区的 大小(需要 文件实际大小)prot
: 映射区的 保护权限PROT_READ
: 可读PROT_WRITE
: 可写PROT_READ | PROT_WRITE
: 可读写
flags
: 映射区的 共享属性MAP_SHARED
: 修改 会 反映到磁盘文件MAP_PRIVATE
: 修改不会反映到磁盘文件
fd
: 文件描述符,用于创建映射区的文件offset
: 文件偏移量,偏移量offset
必须是系统页大小(通常4KB)的整数倍。
返回值:
- 成功:返回 映射区的首地址
- 失败:返回
MAP_FAILED
(即(void *)-1
),并设置errno
2.2 munmap 函数
munmap
函数用于释放共享内存映射区,参数为 mmap
返回的地址(必须和 mmap
返回的地址是同一个)和 映射区大小。
1 |
|
2.3 mmap 建立映射区示例
1 |
|
注意事项:
- 文件大小:映射文件不能为空文件,必须有实际大小
- 权限匹配:
- 使用
MAP_SHARED
时,映射区权限应 文件打开权限 - 使用
MAP_PRIVATE
时,只需要文件有读权限
- 使用
- 偏移量:必须是系统页大小(通常4KB)的整数倍
- 越界访问:不能越界访问映射区
- 指针操作:
munmap
必须使用mmap
返回的原始地址,避免使用指针位移等操作 - 错误检查:一定要检查
mmap
返回值,确保映射成功
3 父子进程通过 mmap 通信
父进程先创建映射区,指定 MAP_SHARED
权限。然后在父进程中 fork()
创建子进程。父进程负责读取子进程写入的数据。
在下面的示例中还对比了全局变量,可以看到全局变量的修改遵循的还是 读时共享、写时复制 的原则。
1 |
|
使用 MAP_SHARED
时,子进程的修改会反映到父进程中。如果使用 MAP_PRIVATE
,则修改不会共享。
有血缘关系的进程也可以通过建立 mmap 匿名映射区来实现通信,这种方式不需要实际文件的支持:
1 |
|
4 无血缘关系进程通过 mmap 通信
无血缘关系的进程也可以通过 mmap
通信,需要打开同一个文件并创建映射区。
写进程:
1 |
|
读进程:
1 |
|
先执行写进程,再执行读进程。
5 mmap 实现进程通信的原理
mmap
IPC 的原理核心在于让两个或多个独立的进程 将自己的虚拟地址空间映射到同一个物理内存页。
5.1 mmap 连接虚拟地址与物理内存
当进程调用 mmap
时,它请求内核在其虚拟地址空间中创建一段新的映射。
-
对于文件映射(无血缘关系IPC常用):
- 进程A打开一个文件(例如
shared_mem.file
),并获得文件描述符fd
。 - 进程A调用
p = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0)
。 - 内核并不会立即将整个文件内容加载到内存。它只是在进程A的页表中创建一系列页表项(Page Table Entries),将这些虚拟页标记为“已映射”,但此时它们可能并未关联到实际的物理内存页(RAM),我们称之为未驻留(Not Present)。
- 当进程A首次读写指针
p
指向的地址时,会触发一个缺页中断(Page Fault)。 - 内核的缺页中断处理程序被调用。它分配一个物理内存页,然后将文件
shared_mem.file
中相应的内容(例如4KB)读取到这个物理页中。 - 内核更新进程A的页表,将发生访问的虚拟页映射到刚分配的物理页。
- 现在,进程A对
p
的读写操作就直接作用于这片物理内存了。
- 进程A打开一个文件(例如
-
对于匿名映射(有血缘关系IPC常用,如
MAP_ANONYMOUS
):- 进程调用
mmap
时使用了MAP_ANONYMOUS
标志,并设置fd = -1
。 - 内核同样在调用进程的页表中创建映射项。
- 当进程首次访问时触发缺页中断,内核直接分配一个被初始化为0的物理内存页并将其映射到进程的虚拟地址空间。这个过程没有文件参与。
- 进程调用
5.2 MAP_SHARED 实现共享
flags
参数中的 MAP_SHARED
是实现IPC的 关键。它告诉内核:
“这个映射是共享的。对这片内存的修改应该对其他映射了相同区域的所有进程可见,如果背后有文件,最终也应该写回文件。”
- 当进程 A 在共享映射中修改数据时,它直接修改了 物理内存 中的内容。
- 由于进程 B 也使用
MAP_SHARED
将它的虚拟地址映射到了 同一片物理内存,进程 B 就能立刻看到进程 A 所做的更改。 - 内核负责维护这种映射关系的一致性。对进程而言,它们好像是在读写自己私有的内存,但实际上内核通过让它们的虚拟页指向相同的物理页,默默地实现了数据的共享。
这与 MAP_PRIVATE
形成鲜明对比:MAP_PRIVATE
会使用 写时复制(Copy-on-Write)。当一个进程尝试修改私有映射的内存时,内核会为该进程单独复制一个物理页的副本,后续修改只影响这个副本,其他进程自然就看不到这个修改。
5.3 数据同步到文件
如果映射有后端文件(非匿名映射),内核的 页缓存(Page Cache) 机制会介入。
- 第一次读取文件数据时,数据从磁盘被读到物理内存的 页缓存 中。
mmap
映射的正是这个 页缓存 中的页面。- 因此,进程对映射区的读写,相当于直接读写 页缓存。
- 内核会在后台某个时刻(根据系统负载和脏页比例)将 页缓存 中被修改过的数据 写回磁盘。
6 mmap 实现进程通信的特点
mmap
是一种高性能、灵活且功能强大的 IPC 机制,适用于需要 处理大容量数据、追求极致性能 或需要 数据持久化 的场景。但是,必须 手动处理同步问题 并 谨慎管理映射资源。
6.1 mmap 通信的优点
-
零拷贝:传统的
read/write
需要将数据从内核缓冲区拷贝到用户空间,而mmap
让进程 直接通过指针操作内存,避免了这次昂贵的数据拷贝,极大提升了效率。 -
数据持久化能力:文件备份的映射使得通信数据可以 持久化到磁盘文件,进程重启后数据不丢失。
-
支持无血缘关系进程通信:多个进程能够打开同一个文件并对其建立映射,就可以通信。
-
大容量数据共享:适合共享非常大的数据结构或文件,所有进程通过指针访问,无需担心管道或消息队列的大小限制。
6.2 mmap 通信的缺点
-
不提供同步机制:mmap 只共享了内存空间,但 不提供任何同步原语(如互斥、信号量)。多个进程同时读写会产生资源竞争,导致数据不一致等问题。
-
资源管理复杂度:映射区大小必须谨慎处理,不能超过文件大小(通常需配合 ftruncate),访问越界会导致总线错误(SIGBUS)
-
一致性开销:对于文件映射,内核 需要管理页缓存与磁盘文件的回写,可能会带来额外开销。
6.3 mmap 与传统 IPC 方式对比
特性 | mmap IPC | 传统IPC(如管道、FIFO) |
---|---|---|
性能 | 高(零拷贝,直接内存访问) | 较低(数据需要在内核和用户空间间拷贝) |
容量 | 大(仅受虚拟地址空间和磁盘限制) | 小(通常有内核队列大小限制) |
自然性 | 高(如同操作变量) | 低(需调用特定API,如 read /write /msgsnd ) |
数据持久化 | 支持(文件映射可持久化) | 不支持(通信数据为内核缓冲,进程结束即消失) |
无血缘支持 | 支持(通过文件) | 部分支持(如 FIFO) |
同步机制 | 不提供(需额外实现) | 提供(内核管理阻塞/唤醒,如空管道读阻塞) |
复杂度 | 中高(需自行管理内存、同步、文件大小) | 低(内核管理缓冲和同步) |