一、内存映射(mmap) — 让文件像内存一样访问
大家好,我是你们的嵌入式讲师。今天我们来聊聊 Linux 下非常实用的一个机制——mmap。说实话,我最早接触 mmap 是在做视频采集卡驱动的时候,那时候需要把硬件采集的帧数据快速传给用户态程序,用 read/write 效率太低了。后来发现 mmap 简直就是为这种场景量身定做的。
1.1 mmap 的概念与原理
mmap,全称 memory map,中文叫内存映射。说白了,就是把一个文件或者一段物理内存,映射到进程的虚拟地址空间里。映射完成后,你操作这块内存,就相当于在操作文件。
为什么会这样?因为 Linux 内核帮你把文件内容和虚拟内存页关联起来了。你读内存页,内核自动从磁盘加载数据;你写内存页,内核在背后帮你同步回磁盘。这一切对应用程序是透明的。
核心思想:mmap 让「文件 I/O」变成了「内存读写」,省去了用户态和内核态之间的数据拷贝。
我画了一张图,帮你理解 mmap 的映射关系:
你看,两个进程各自有虚拟地址空间,但通过 mmap 映射到了同一块物理内存或同一个文件。这就是 mmap 实现进程间通信(IPC)的基础。
1.2 mmap 的创建与参数详解
创建 mmap 的核心函数就一个:mmap()。它的原型长这样:
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
六个参数,一个都不能错。我一个个讲,顺便说说我踩过的坑。
| 参数 | 含义 | 个人经验 |
|---|---|---|
addr |
建议的映射起始地址,传 NULL 让内核选 | 我从来都传 NULL,省心 |
length |
映射长度,单位字节 | 必须是页大小的整数倍?不一定,但建议对齐 |
prot |
保护标志:PROT_READ / PROT_WRITE / PROT_EXEC / PROT_NONE | 读写就 PROT_READ|PROT_WRITE |
flags |
映射类型:MAP_SHARED / MAP_PRIVATE / MAP_ANONYMOUS 等 | IPC 必须用 MAP_SHARED,否则白忙活 |
fd |
文件描述符,匿名映射传 -1 | 记得先 open 文件,别传错了 |
offset |
文件偏移,必须是页大小的整数倍 | 我曾经传了非对齐值,mmap 直接返回 MAP_FAILED |
⚠️ 重要提醒:offset 参数必须是系统页大小(通常是 4096)的整数倍。如果你要映射文件中间某段,先算好偏移量。我当年刚用 mmap 时,传了个 100 的偏移,结果 mmap 报错,查了半天手册才明白。
1.3 mmap 的读写操作
映射完成后,读写就简单了——直接操作指针。来看个例子:
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main() {
// 打开文件
int fd = open("test.txt", O_RDWR | O_CREAT, 0644);
if (fd == -1) {
perror("open");
exit(1);
}
// 设置文件大小
ftruncate(fd, 4096);
// 创建映射
char *map = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (map == MAP_FAILED) {
perror("mmap");
close(fd);
exit(1);
}
// 写数据 —— 直接内存操作
strcpy(map, "Hello, mmap! 这是通过内存映射写入的。");
// 读数据 —— 也是直接内存操作
printf("读取到: %s\n", map);
// 清理
munmap(map, 4096);
close(fd);
return 0;
}
你看,代码里没有 read、write 系统调用,就是 memcpy 和 printf。效率高在哪?省去了内核缓冲区和用户缓冲区之间的数据拷贝。
1.4 mmap 的同步 — msync
这里有个关键问题:你写入了内存,数据什么时候落到磁盘?
对于 MAP_SHARED 映射,内核会在后台择机回写。但如果你需要确保数据立即落盘——比如写日志、写数据库——就得主动调用 msync。
int msync(void *addr, size_t length, int flags);
flags 有三个选项:
- MS_ASYNC:异步写,函数立即返回,内核慢慢写
- MS_SYNC:同步写,等数据写完了才返回
- MS_INVALIDATE:使其他映射失效,慎用
💡 我的习惯:在嵌入式设备上,如果突然断电可能导致数据丢失,我会在关键位置加 msync(..., MS_SYNC)。虽然慢一点,但安全。曾经有个项目,同事没加 msync,设备掉电后配置文件全丢了,教训深刻。
1.5 mmap 的解除 — munmap
用完了要释放,调用 munmap:
int munmap(void *addr, size_t length);
参数就是 mmap 返回的地址和长度。注意:
- munmap 后,再访问那块内存就是非法操作,会段错误
- 进程退出时,内核会自动解除所有映射,但显式调用是好习惯
- 如果 fork 了子进程,子进程会继承映射,但 munmap 只影响调用者
1.6 mmap 在 IPC 中的应用
这才是重头戏。两个进程通过 mmap 共享同一块内存,就能通信了。有两种方式:
方式一:文件映射
两个进程打开同一个文件,都用 MAP_SHARED 映射。一个进程写,另一个进程读。适合有血缘关系或无血缘关系的进程。
方式二:匿名映射
用 MAP_ANONYMOUS | MAP_SHARED,不需要文件描述符。但只能用于有亲缘关系的进程(父子进程)。
// 父进程
int *shared = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
*shared = 0;
if (fork() == 0) {
// 子进程
(*shared)++; // 修改共享变量
printf("子进程: shared = %d\n", *shared);
exit(0);
}
// 父进程
wait(NULL);
printf("父进程: shared = %d\n", *shared); // 能看到子进程的修改
运行结果:
子进程: shared = 1
父进程: shared = 1
看到了吗?父子进程通过匿名映射共享了一个 int 变量。这就是最简单的 mmap IPC。
1.7 mmap 与共享内存的对比
很多初学者会问:mmap 和 System V 共享内存(shmget)有什么区别?我整理了一张表:
| 对比项 | mmap | System V 共享内存 (shmget) |
|---|---|---|
| 创建方式 | mmap() + 文件或匿名 | shmget() + shmat() |
| 持久性 | 依赖文件(文件映射)或进程生命周期(匿名) | 内核对象,显式删除前一直存在 |
| 同步机制 | 需要自己加锁(互斥锁、信号量) | 同样需要自己加锁 |
| 使用场景 | 文件 I/O 加速、大文件处理、简单 IPC | 高性能 IPC、无文件依赖的场景 |
| 可移植性 | POSIX 标准,跨平台好 | System V 标准,Linux/Unix 支持 |
我的建议:新项目优先用 mmap。原因很简单——POSIX 标准,接口简洁,还能和文件操作结合。除非你需要在无文件的情况下做高性能 IPC,而且进程之间没有亲缘关系,那可以考虑 shmget。但说实话,我最近五年几乎没用过 shmget 了。
小结
mmap 的核心就一句话:把文件或内存映射到进程地址空间,然后当内存用。它省去了 read/write 的数据拷贝,效率高;它能让多个进程共享同一块物理内存,实现 IPC。但要注意:同步要自己管(msync),并发要自己锁(互斥锁),参数要对齐(offset 页对齐)。
嗯,这一章的内容就到这。下一章我们深入 mmap 的实战——用 mmap 实现一个高效的共享内存环形缓冲区。到时候我会把当年做视频采集卡时踩的坑都抖出来。