2. 进程内存映射:mmap()系统调用详解、匿名映射与文件映射、MAP_SHARED与MAP_PRIVATE的区别
好,咱们今天聊聊 mmap()。这个系统调用,说白了就是QNX里进程管理内存的「瑞士军刀」。我做了这么多年嵌入式,几乎每个项目都离不开它。你想想看,一个进程怎么访问文件?怎么分配大块内存?怎么实现进程间共享数据?答案往往就是 mmap()。
2.1 mmap() 系统调用:把「文件」变成「内存」
mmap() 的核心思想很简单:建立一段虚拟地址到物理内存或文件内容的映射。调用成功后,进程就能像读写内存一样操作文件,或者直接获得一块匿名内存。
它的原型长这样:
#include <sys/mman.h>
void *mmap(
void *addr, // 建议的起始地址,通常传 NULL
size_t length, // 映射长度(字节)
int prot, // 保护标志:PROT_READ, PROT_WRITE, PROT_EXEC, PROT_NONE
int flags, // 映射类型:MAP_SHARED, MAP_PRIVATE, MAP_ANONYMOUS 等
int fd, // 文件描述符(匿名映射传 -1)
off_t offset // 文件偏移量(通常为 0)
);
返回值是映射区的起始地址。如果失败,返回 MAP_FAILED(也就是 (void *)-1)。
关键参数解读:
- addr:我个人习惯传
NULL,让内核自己选地址。除非你有特殊需求(比如固定地址的硬件寄存器映射),否则别瞎指定。 - length:必须是页大小的整数倍。QNX 默认页大小是 4096 字节(4K)。如果你传 100 字节,内核会向上取整到 4096。
- prot:权限组合。注意,
PROT_EXEC在 QNX 安全策略下可能受限,尤其是 SELinux 或沙箱环境。 - flags:这是重点,决定了映射的行为。
- fd:文件映射时传打开的文件描述符;匿名映射传
-1。 - offset:文件映射的起始偏移,也必须是页对齐的。
我的经验: 我曾经在一个项目中,因为 offset 没对齐,导致 mmap() 一直返回 EINVAL。查了半天手册才发现这个坑。嗯,记住:offset 必须是页大小的整数倍。
2.2 匿名映射 vs 文件映射
这是 mmap() 的两大应用场景。我分别说说。
2.2.1 匿名映射:分配大块内存的利器
匿名映射,就是映射一块没有文件 backing 的内存。说白了,就是直接向内核要一块物理内存,映射到进程的虚拟地址空间。
用法很简单:flags 里加上 MAP_ANONYMOUS,fd 传 -1。
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>
int main() {
size_t size = 1024 * 1024; // 1 MB
void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (ptr == MAP_FAILED) {
perror("mmap failed");
exit(1);
}
// 现在可以像普通数组一样使用这块内存
char *buf = (char *)ptr;
buf[0] = 'A';
buf[1023] = 'Z';
printf("匿名映射成功,地址:%p\n", ptr);
// 使用完后记得释放
munmap(ptr, size);
return 0;
}
什么时候用匿名映射?
- 需要分配大块内存(比如几 MB 甚至几百 MB),而
malloc()可能因为堆碎片化而失败。 - 需要内存对齐到页边界。
- 需要共享内存给子进程(配合
MAP_SHARED)。
注意: 匿名映射分配的内存,默认是 零初始化 的。但别指望它像 calloc() 那样保证全零——QNX 内核确实会清零,但如果你用 MAP_UNINITIALIZED 标志(某些系统支持),可能得到脏内存。我建议你显式 memset 一下,除非你确定不需要。
2.2.2 文件映射:把文件当内存用
文件映射,就是把一个文件的内容映射到进程的地址空间。这样,读写文件就像读写内存一样简单。内核负责在后台处理页面的换入换出。
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd = open("/tmp/data.bin", O_RDWR | O_CREAT, 0644);
if (fd == -1) {
perror("open failed");
exit(1);
}
// 设置文件大小
size_t size = 4096;
ftruncate(fd, size);
void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED) {
perror("mmap failed");
close(fd);
exit(1);
}
// 直接通过指针读写文件
char *data = (char *)ptr;
data[0] = 'H';
data[1] = 'i';
printf("文件映射成功,写入数据\n");
// 同步到磁盘(可选)
msync(ptr, size, MS_SYNC);
munmap(ptr, size);
close(fd);
return 0;
}
文件映射的好处:
- 省去了
read()/write()的系统调用开销,尤其是随机访问时。 - 内核自动管理页面缓存,多个进程映射同一个文件可以共享物理内存。
- 适合大文件处理(比如数据库、日志文件)。
我的经验: 我曾经在一个数据采集系统中,用文件映射代替了传统的 fread()/fwrite()。结果呢?I/O 吞吐量提升了将近 30%。原因很简单:mmap() 避免了用户态和内核态之间的数据拷贝。但要注意,如果文件频繁修改且需要实时落盘,msync() 的调用时机要把握好。
2.3 MAP_SHARED vs MAP_PRIVATE:共享还是私有?
这两个标志决定了映射的「可见性」和「写行为」。这是 mmap() 最核心的区分,也是面试常考题。
| 特性 | MAP_SHARED | MAP_PRIVATE |
|---|---|---|
| 写操作影响 | 修改直接写回底层文件(或共享内存) | 修改只影响本进程,采用写时复制(Copy-on-Write) |
| 其他进程可见性 | 其他映射同一文件的进程可见修改 | 其他进程不可见修改 |
| 典型用途 | 进程间共享内存、文件 I/O 优化 | 私有数据、代码段加载、调试 |
| 性能开销 | 写操作直接触发,无额外拷贝 | 首次写时触发缺页中断,拷贝页面 |
2.3.1 MAP_SHARED:大家一起改
用 MAP_SHARED 映射的内存,所有映射同一区域(或同一文件)的进程都能看到修改。而且,修改会写回底层文件(如果是文件映射)。
这简直就是进程间通信(IPC)的利器。我经常用它来实现共享内存,比 shmget() 那套 System V 接口灵活多了。
// 进程 A:写入数据
int fd = open("/tmp/shared", O_RDWR | O_CREAT, 0644);
ftruncate(fd, 4096);
void *ptr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
sprintf((char *)ptr, "Hello from process A");
// 进程 B:读取数据
int fd = open("/tmp/shared", O_RDONLY);
void *ptr = mmap(NULL, 4096, PROT_READ, MAP_SHARED, fd, 0);
printf("Received: %s\n", (char *)ptr); // 输出 "Hello from process A"
注意: 使用 MAP_SHARED 时,必须确保文件描述符 fd 的打开权限与 prot 一致。比如 prot 有 PROT_WRITE,那么 fd 必须以 O_RDWR 打开。否则 mmap() 会返回 EACCES。
2.3.2 MAP_PRIVATE:各改各的,互不影响
MAP_PRIVATE 则完全相反。它采用 写时复制(Copy-on-Write, CoW) 机制。初始时,所有进程共享同一物理页面。一旦某个进程尝试写入,内核会先拷贝一份页面给该进程,然后才允许修改。其他进程看到的还是原始数据。
这有什么用?
- 程序加载:动态链接器加载共享库时,通常用
MAP_PRIVATE。这样多个进程可以共享代码段,但各自的数据段互不干扰。 - 调试:你想修改一个文件的内容做测试,但又不想破坏原文件。用
MAP_PRIVATE映射,随便改,原文件纹丝不动。 - 安全:敏感数据不想被其他进程看到?用
MAP_PRIVATE。
// 进程 A:用 MAP_PRIVATE 映射文件
int fd = open("/tmp/data", O_RDONLY);
void *ptr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
// 修改只影响本进程
((char *)ptr)[0] = 'X';
// 进程 B:同样映射,但看到的是原始数据
int fd2 = open("/tmp/data", O_RDONLY);
void *ptr2 = mmap(NULL, 4096, PROT_READ, MAP_PRIVATE, fd2, 0);
printf("%c\n", ((char *)ptr2)[0]); // 输出原始字符,不是 'X'
避坑指南: 我曾经遇到一个性能问题:一个进程用 MAP_PRIVATE 映射了一个大文件(几百 MB),然后频繁修改其中的数据。结果呢?每次修改都触发缺页中断和页面拷贝,性能惨不忍睹。后来改成 MAP_SHARED 才解决。所以,如果你确定不需要写时复制,就别用 MAP_PRIVATE,尤其是大块内存频繁写的场景。
2.4 实际项目中的选择策略
说了这么多,到底怎么选?我总结了一个简单的决策树:
- 需要进程间共享数据? → 用
MAP_SHARED+ 文件映射或匿名映射。 - 需要私有副本,不想影响原文件? → 用
MAP_PRIVATE+ 文件映射。 - 需要分配大块私有内存? → 用
MAP_PRIVATE | MAP_ANONYMOUS。 - 需要共享内存但不想创建文件? → 用
MAP_SHARED | MAP_ANONYMOUS(配合fork()或shm_open())。
我的建议: 在 QNX 实时系统中,我倾向于少用 MAP_PRIVATE 的写时复制。因为缺页中断的延迟不可预测,对实时性有影响。如果必须用,尽量在初始化阶段完成所有写操作,避免运行时触发 CoW。
好了,关于 mmap() 的核心内容就这些。记住:映射类型决定了行为,权限决定了安全,对齐决定了成败。下一节我们聊聊 munmap() 和 msync() 的细节,以及如何正确管理映射生命周期。