4、mmap系统调用:文件映射与匿名映射,大块内存分配的利器
说到内存分配,前面我们聊了malloc、brk这些老朋友。但如果你要分配一块超大的内存——比如几百MB甚至几个GB——再用malloc就有点力不从心了。这时候,mmap就该登场了。
我个人习惯把mmap看作是「内存分配的瑞士军刀」。它既能做文件映射,也能做匿名映射,底层直接跟虚拟内存打交道。说白了,它就是操作系统给你开的一扇后门,让你能精细控制进程的地址空间。
4.1 mmap是什么?
mmap的全称是memory map,即内存映射。它的核心思想是:把文件或设备的一部分,直接映射到进程的虚拟地址空间。映射完成后,你读写这块内存,就相当于读写文件——不需要read/write系统调用,也不需要用户态和内核态之间的数据拷贝。
嗯,这里要注意:mmap不是用来替代malloc的。它更适合大块内存、文件I/O、共享内存这些场景。小对象分配用mmap,反而浪费。
核心概念:mmap创建的是虚拟内存区域(VMA),内核通过缺页中断按需加载物理页。映射的单位是页(通常是4KB),所以分配大小必须是页的整数倍。
4.2 两种映射模式
mmap有两种基本模式,我当年刚学时也容易搞混。咱们一个一个说。
4.2.1 文件映射
文件映射,就是把一个磁盘文件映射到内存。你操作内存,内核自动同步回磁盘。这在处理大文件时特别爽——不用自己管理缓冲区,不用手动seek。
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
int fd = open("bigfile.dat", O_RDWR);
size_t length = 1024 * 1024 * 100; // 100MB
void *addr = mmap(NULL, length, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
if (addr == MAP_FAILED) {
perror("mmap failed");
exit(1);
}
// 现在可以直接读写这块内存
memcpy(addr, "hello", 5); // 数据会写回文件
munmap(addr, length);
close(fd);
我在项目中遇到过一个大坑:文件映射后,如果其他进程截断了文件,你映射的区域可能产生SIGBUS信号。所以生产环境一定要加信号处理。
4.2.2 匿名映射
匿名映射不关联任何文件,纯粹分配一块内存。它和堆分配的区别在于:匿名映射的内存是独立于堆的,由内核直接管理。
// 分配100MB匿名内存
void *addr = mmap(NULL, 1024 * 1024 * 100,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS,
-1, 0);
if (addr == MAP_FAILED) {
perror("mmap failed");
exit(1);
}
// 使用完后释放
munmap(addr, 1024 * 1024 * 100);
你想想看,为什么malloc分配大块内存时,底层会调用mmap?因为brk只能线性扩展堆,而mmap可以在地址空间任意位置分配,不会造成堆碎片。
4.3 关键参数详解
| 参数 | 取值 | 含义 |
|---|---|---|
addr |
NULL 或指定地址 | 建议映射地址,内核不一定采纳。传NULL最安全 |
length |
映射长度(字节) | 必须是页大小的整数倍,否则内核会向上取整 |
prot |
PROT_READ / PROT_WRITE / PROT_EXEC / PROT_NONE | 内存保护标志,可以按位或组合 |
flags |
MAP_SHARED / MAP_PRIVATE / MAP_ANONYMOUS 等 | 映射类型和属性 |
fd |
文件描述符 | 文件映射时使用,匿名映射传-1 |
offset |
文件偏移量 | 必须是页对齐的 |
我曾经踩过的坑:MAP_SHARED和MAP_PRIVATE的区别。MAP_SHARED的修改会写回文件,适合多进程共享。MAP_PRIVATE采用写时复制(COW),修改不影响原文件。如果你只是想读文件,用MAP_PRIVATE更安全。
4.4 mmap vs malloc:什么时候用谁?
这个问题我经常被问到。我的建议很简单:
- 小于128KB:用malloc。brk分配更快,碎片更少。
- 大于128KB:glibc的malloc会自动转用mmap。但你也可以手动调用mmap。
- 需要文件I/O:用mmap文件映射,省去read/write的系统调用开销。
- 需要共享内存:用mmap匿名映射 + MAP_SHARED,比System V共享内存更现代。
说白了,mmap的优势在于:零拷贝、按需加载、共享方便。但代价是每次映射/解映射都需要修改页表,频繁操作会有性能开销。
4.5 核心流程图:mmap的内存映射过程
下面这张图展示了mmap从调用到物理内存分配的全过程。我画这张图时,特意把关键路径标出来了。
看到没?mmap返回时,物理页还没分配。只有当你真正读写内存时,才会触发缺页中断,内核才去加载数据。这就是所谓的惰性分配——按需加载,节省物理内存。
4.6 实战经验:大块内存分配的最佳实践
我的建议:如果你要分配超过128KB的内存,直接用mmap。别让malloc在背后偷偷调用mmap——你自己控制更放心。特别是做高性能服务时,明确知道内存从哪里来,对排查问题很有帮助。
几个实用技巧:
- 预分配物理页:调用mmap后,遍历内存区域写入0,可以提前触发缺页中断,避免运行时卡顿。
- 使用MAP_POPULATE:这个标志可以让mmap在返回前就预加载页表,适合对延迟敏感的场景。
- 注意对齐:offset和addr都必须是页对齐的(4KB的整数倍),否则mmap会返回EINVAL。
- 不要忘记munmap:mmap分配的内存不会自动释放,进程退出时才会回收。长期运行的服务一定要显式调用munmap。
我曾经踩过的坑:有一次做数据库缓存层,用mmap分配了2GB的共享内存。结果忘了设置MAP_SHARED,默认用了MAP_PRIVATE。多个进程各自写时复制,内存直接爆了。排查了一整天才发现是flags的问题。所以,共享内存一定要显式指定MAP_SHARED。
好了,mmap的核心内容就这些。它不像malloc那么常用,但一旦用上,往往就是解决性能瓶颈的关键。记住一句话:大块内存找mmap,文件I/O也找mmap,共享内存还是找mmap。