「Linux 系统编程」mmap 建立共享内存映射

1 什么是 mmap

mmap(Memory-mapped I/O)是一种高效的 磁盘文件与内存缓冲区映射的机制。通过 mmap 可以将一个磁盘文件直接映射到进程的地址空间中,使得 对内存的读写操作直接反映到文件上无需使用传统的 readwrite 函数


2 mmap 与 munmap 函数

2.1 mmap 函数

mmap 函数用于 创建一个共享内存映射区

函数原型

1
2
3
#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

参数说明

  • addr: 指定 映射区的首地址。通常传入NULL,表示由系统自动分配
  • length: 映射区的 大小(需要 \le 文件实际大小)
  • 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
int munmap(void *addr, size_t length);

2.3 mmap 建立映射区示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
// 需要打开一个文件
int fd = open("shared_mem.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
perror("open error");
exit(1);
}

// 调整文件大小
ftruncate(fd, 1024);

// 创建映射区,小于等于文件大小
char *p = mmap(NULL, 1024, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (p == MAP_FAILED) {
perror("mmap error");
exit(1);
}

close(fd); // 映射建立后即可关闭文件

// 使用映射区
sprintf(p, "Hello, Kirisame Marisa!");

// 释放映射区
munmap(p, 1024);

return 0;
}

注意事项

  • 文件大小:映射文件不能为空文件,必须有实际大小
  • 权限匹配
    • 使用 MAP_SHARED 时,映射区权限应 \le 文件打开权限
    • 使用 MAP_PRIVATE 时,只需要文件有读权限
  • 偏移量:必须是系统页大小(通常4KB)的整数倍
  • 越界访问:不能越界访问映射区
  • 指针操作munmap 必须使用 mmap 返回的原始地址,避免使用指针位移等操作
  • 错误检查:一定要检查 mmap 返回值,确保映射成功


3 父子进程通过 mmap 通信

父进程先创建映射区,指定 MAP_SHARED 权限。然后在父进程中 fork() 创建子进程。父进程负责读取子进程写入的数据。

在下面的示例中还对比了全局变量,可以看到全局变量的修改遵循的还是 读时共享、写时复制 的原则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>

int glob = 233;

int main() {
int fd = open("shared_mem.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
perror("open error");
exit(1);
}

// 调整为 4 字节,一个整形数据大小
ftruncate(fd, 4);

// 创建整形数据大小的共享内存映射区
int *p = mmap(NULL, 4, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (p == MAP_FAILED) {
perror("mmap error");
exit(1);
}

// 可以关闭打开的文件
close(fd);

pid_t pid = fork();
if (pid == 0) {
// 子进程写入
*p = 888;
glob = 777;
printf("Child: *p = %d, glob = %d\n", *p, glob);
} else {
// 父进程读取
wait(NULL); // 等待子进程执行完毕,读取共享内存映射区数据
printf("Parent: *p = %d, glob = %d\n", *p, glob);
// 释放共享内存映射区
munmap(p, 4);
}

return 0;
}

使用 MAP_SHARED 时,子进程的修改会反映到父进程中。如果使用 MAP_PRIVATE,则修改不会共享。

有血缘关系的进程也可以通过建立 mmap 匿名映射区来实现通信,这种方式不需要实际文件的支持:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>

int glob = 233;

int main() {
// 创建整形数据大小的共享内存映射区,匿名映射区不需要文件支持
int *p = mmap(NULL, 4, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (p == MAP_FAILED) {
perror("mmap error");
exit(1);
}

pid_t pid = fork();
if (pid == 0) {
// 子进程写入
*p = 888;
glob = 777;
printf("Child: *p = %d, glob = %d\n", *p, glob);
} else {
// 父进程读取
wait(NULL); // 等待子进程执行完毕,读取共享内存映射区数据
printf("Parent: *p = %d, glob = %d\n", *p, glob);
// 释放共享内存映射区
munmap(p, 4);
}

return 0;
}


4 无血缘关系进程通过 mmap 通信

无血缘关系的进程也可以通过 mmap 通信,需要打开同一个文件并创建映射区。

写进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

struct STU {
int id;
char name[20];
char sex;
};

int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: %s file_shared\n", argv[0]);
exit(1);
}

int fd = open(argv[1], O_RDWR | O_CREAT, 0664);
ftruncate(fd, sizeof(struct STU));

struct STU *p = mmap(NULL, sizeof(struct STU), PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
if (p == MAP_FAILED) {
perror("mmap error");
exit(1);
}
close(fd);

// 不断更新共享内存
while (1) {
p -> id ++ ;
sprintf(p -> name, "Student-%d", p -> id);
p -> sex = (p -> id % 2) ? 'M' : 'F';
sleep(1);
}

munmap(p, sizeof(struct STU));

return 0;
}

读进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>

struct STU {
int id;
char name[20];
char sex;
};

int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: %s file_shared\n", argv[0]);
exit(1);
}

int fd = open(argv[1], O_RDONLY);
struct STU *p = mmap(NULL, sizeof(struct STU), PROT_READ,
MAP_SHARED, fd, 0);
if (p == MAP_FAILED) {
perror("mmap error");
exit(1);
}
close(fd);

// 不断读取共享内存
while (1) {
printf("Reader: ID: %d, Name: %s, Sex: %c\n", p->id, p->name, p->sex);
sleep(2);
}

munmap(p, sizeof(struct STU));

return 0;
}

先执行写进程,再执行读进程。



5 mmap 实现进程通信的原理

mmap IPC 的原理核心在于让两个或多个独立的进程 将自己的虚拟地址空间映射到同一个物理内存页

5.1 mmap 连接虚拟地址与物理内存

当进程调用 mmap 时,它请求内核在其虚拟地址空间中创建一段新的映射。

  • 对于文件映射(无血缘关系IPC常用):

    1. 进程A打开一个文件(例如 shared_mem.file),并获得文件描述符 fd
    2. 进程A调用 p = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0)
    3. 内核并不会立即将整个文件内容加载到内存。它只是在进程A的页表中创建一系列页表项(Page Table Entries),将这些虚拟页标记为“已映射”,但此时它们可能并未关联到实际的物理内存页(RAM),我们称之为未驻留(Not Present)
    4. 当进程A首次读写指针 p 指向的地址时,会触发一个缺页中断(Page Fault)
    5. 内核的缺页中断处理程序被调用。它分配一个物理内存页,然后将文件 shared_mem.file 中相应的内容(例如4KB)读取到这个物理页中。
    6. 内核更新进程A的页表,将发生访问的虚拟页映射到刚分配的物理页
    7. 现在,进程A对 p 的读写操作就直接作用于这片物理内存了。
  • 对于匿名映射(有血缘关系IPC常用,如 MAP_ANONYMOUS:

    1. 进程调用 mmap 时使用了 MAP_ANONYMOUS 标志,并设置 fd = -1
    2. 内核同样在调用进程的页表中创建映射项。
    3. 当进程首次访问时触发缺页中断,内核直接分配一个被初始化为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)
同步机制 不提供(需额外实现) 提供(内核管理阻塞/唤醒,如空管道读阻塞)
复杂度 中高(需自行管理内存、同步、文件大小) 低(内核管理缓冲和同步)

「Linux 系统编程」mmap 建立共享内存映射
https://marisamagic.github.io/2025/09/07/20250907/
作者
MarisaMagic
发布于
2025年9月7日
许可协议