mmap¶
是什么 / 解决什么问题¶
mmap 将文件(或匿名内存)映射到进程的虚拟地址空间。读写映射区域直接操作 page cache,跳过 read/write 系统调用的数据拷贝。
使用模式¶
文件映射¶
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
int fd = open("data.bin", O_RDWR);
struct stat st;
fstat(fd, &st);
// 映射整个文件
void *addr = mmap(NULL, st.st_size,
PROT_READ | PROT_WRITE, // 权限
MAP_SHARED, // 共享映射
fd, 0); // 文件 fd 和偏移
// 直接当内存用
char *data = (char *)addr;
printf("first byte: %c\n", data[0]);
data[100] = 'x'; // 直接修改文件内容
// 解除映射
munmap(addr, st.st_size);
close(fd); // close 后映射仍然有效
私有映射 vs 共享映射¶
// MAP_SHARED: 修改会写回文件,其他映射该文件的进程可见
void *shared = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
// MAP_PRIVATE: COW (Copy-on-Write)
// 修改只对本进程可见,不影响文件
void *private = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE, fd, 0);
匿名映射(不关联文件)¶
// 分配大块内存(malloc 底层对大块分配用的就是这个)
void *mem = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
// fd = -1, 不关联文件
// 释放
munmap(mem, size);
进程间共享内存¶
// 方法1: MAP_SHARED + 文件
int fd = open("/tmp/shared", O_RDWR | O_CREAT, 0644);
ftruncate(fd, size);
void *shared = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// 方法2: MAP_SHARED | MAP_ANONYMOUS(只能父子进程间)
void *shared = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
pid_t pid = fork(); // 子进程继承映射
// 方法3: shm_open + mmap(推荐)
int fd = shm_open("/myshm", O_RDWR | O_CREAT, 0644);
ftruncate(fd, size);
void *shared = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
行为与语义¶
延迟加载(demand paging)¶
mmap() 返回时,内存页并未实际分配!
只是建立了虚拟地址空间到文件的映射关系。
首次访问某页时:
→ 触发 page fault
→ 内核从磁盘读取该页到 page cache
→ 建立页表映射
→ 程序继续执行(对程序透明)
这意味着 mmap 一个 10GB 的文件是立即返回的,实际内存按需分配。
MAP_SHARED 的写入行为¶
写入 MAP_SHARED 区域:
→ 直接修改 page cache 中的页
→ 页被标记为 dirty
→ 后台 writeback 线程定期刷盘
→ 或者调用 msync() 手动刷盘
// 显式刷盘
msync(addr, length, MS_SYNC); // 同步,等落盘
msync(addr, length, MS_ASYNC); // 异步,只标记 dirty
mmap vs read/write¶
| 维度 | read/write | mmap |
|---|---|---|
| 数据拷贝 | 2次(磁盘→page cache→用户buf) | 1次(磁盘→page cache,用户直接访问) |
| syscall | 每次操作一个 syscall | 映射后无 syscall(通过 page fault) |
| 随机访问 | 需要 lseek | 直接用指针 |
| 小文件 | 性能差异不大 | mmap 的 setup 开销相对大 |
| 大文件顺序读 | read + readahead 更优 | page fault 开销大 |
| 大文件随机读 | 频繁 syscall | 更优(无 syscall 开销) |
mmap 的限制¶
- 映射大小受虚拟地址空间限制(64位系统几乎无限)
- 文件扩大后需要重新 mmap(或用
mremap) - SIGBUS:访问超出文件实际大小的映射区域会收到 SIGBUS 信号
- 不适合流式数据(socket/pipe 不能 mmap)
性能考量¶
预读优化¶
// 告诉内核访问模式
madvise(addr, length, MADV_SEQUENTIAL); // 顺序访问,增大预读
madvise(addr, length, MADV_RANDOM); // 随机访问,关闭预读
madvise(addr, length, MADV_WILLNEED); // 即将访问,提前加载
// 预热:强制加载到内存
madvise(addr, length, MADV_POPULATE_READ); // 5.14+ 一次性缺页
大页 (Huge Pages)¶
// 使用透明大页(2MB 页代替 4KB 页)
// 减少 TLB miss,提升大内存访问性能
madvise(addr, length, MADV_HUGEPAGE);
// 或显式使用 hugetlbfs
void *addr = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_HUGETLB, -1, 0);
何时用 mmap¶
适合: - 大文件随机访问(数据库文件、索引文件) - 多进程共享内存 - 只读映射配置文件/资源文件 - 内存映射 I/O(嵌入式设备寄存器)
不适合: - 小文件(mmap 的 setup/teardown 开销不划算) - 顺序扫描大文件(read + fadvise 更优) - 需要精确控制 I/O 时机(mmap 的 page fault 时机不可预测) - 文件大小频繁变化
常见陷阱与 FAQ¶
1. SIGBUS 信号¶
// 文件被其他进程 truncate 缩小后,访问超出范围的映射区域
// → SIGBUS(不是 SIGSEGV)
// 预防:
// - 用 flock 保护
// - 处理 SIGBUS 信号
2. close(fd) 后映射仍有效¶
void *addr = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
close(fd); // 没问题!映射不依赖 fd
// addr 仍然可用,直到 munmap
3. MAP_PRIVATE 的写时复制开销¶
首次写入 MAP_PRIVATE 映射页时触发 COW:分配新物理页 + 拷贝内容。大范围写入时可能不如直接 read 到 buffer 修改。
4. 内存映射文件的一致性¶
MAP_SHARED 写入 ≠ 立即落盘。其他进程的 MAP_SHARED 可以看到修改(因为共享 page cache),但 fsync/msync 前可能丢失。
观测与调试¶
# 查看进程的内存映射
cat /proc/<pid>/maps
# 或 pmap -x <pid>
# 查看文件在 page cache 中的驻留情况
vmtouch /path/to/file
# 查看 page fault 统计
ps -o pid,min_flt,maj_flt -p <pid>
# min_flt: 次缺页(page cache 命中)
# maj_flt: 主缺页(需要磁盘 I/O)
# perf 统计 page fault
perf stat -e page-faults,major-faults ./app
延伸阅读¶
man 2 mmap— 完整参数说明man 2 madvise— 访问模式提示man 2 msync— 刷盘控制man 2 mremap— 调整映射大小