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内存映射核心流程 用户态:调用 mmap() 系统调用 sys_mmap 内核态:创建虚拟内存区域(VMA) 文件映射? 建立文件与VMA 的映射关系 分配匿名页 (不关联文件) 返回虚拟地址,映射完成(物理页尚未分配) 访问内存 → 缺页中断 → 内核按需加载物理页

看到没?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。

可可精品资料库