3. mmap 系统调用概述:原型、映射类型与权限标志
mmap(memory map)是 Linux 内核提供的、用于在进程虚拟地址空间与文件或匿名物理内存之间建立直接映射关系的系统调用。理解 mmap 与 munmap 的原型、映射分类以及权限标志位的组合语义,是掌握用户态高性能内存管理的基础。
3.1 mmap / munmap 系统调用原型
mmap 的核心功能是在进程的虚拟地址空间中分配一段连续区域,并将其与某个文件描述符(或匿名页)建立映射。munmap 则用于解除映射,释放虚拟地址区间。
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
int munmap(void *addr, size_t length);
| 参数 | 含义 | 典型值 / 说明 |
|---|---|---|
addr |
映射起始地址(提示) | 通常传 NULL,由内核选择;非 NULL 时需配合 MAP_FIXED |
length |
映射长度(字节) | 必须为页大小(4096)的整数倍,否则内核会向上取整 |
prot |
映射区域访问权限 | PROT_READ / PROT_WRITE / PROT_EXEC / PROT_NONE |
flags |
映射类型与行为标志 | MAP_SHARED / MAP_PRIVATE / MAP_ANONYMOUS / MAP_FIXED 等 |
fd |
文件描述符 | 文件映射时有效;匿名映射传 -1 |
offset |
文件内偏移 | 必须为页大小整数倍;通常为 0 |
返回值:成功时返回映射区域的起始虚拟地址(void*);失败时返回
MAP_FAILED(即 (void*)-1),并设置 errno。
3.2 文件映射与匿名映射
根据映射后端物理内存的来源,mmap 分为两大类:文件映射和匿名映射。两者的核心区别在于是否关联一个已打开的文件描述符。
3.2.1 文件映射(File-backed Mapping)
将文件的一段内容直接映射到进程的虚拟地址空间。对映射区域的读写会直接反映到文件中(MAP_SHARED),或通过写时拷贝(Copy-on-Write)产生私有副本(MAP_PRIVATE)。
- 典型用途:文件 I/O 加速、共享库加载、大文件随机访问。
- 关键参数:
fd必须为有效文件描述符;offset指定文件内起始位置。 - 性能优势:避免用户态与内核态之间的数据拷贝,利用内核页缓存(Page Cache)实现按需加载。
3.2.2 匿名映射(Anonymous Mapping)
不关联任何文件,映射区域由内核分配物理页并初始化为零。通常用于进程间共享内存(配合 MAP_SHARED)或动态内存分配(如 glibc 的 malloc 在大块分配时内部使用 mmap)。
- 典型用途:进程间通信(共享内存)、大块堆内存分配、线程栈创建。
- 关键参数:
fd传 -1;flags必须包含MAP_ANONYMOUS。 - 性能优势:延迟分配(Demand Paging),仅在首次访问时触发缺页中断并分配物理页。
工程经验:在性能敏感场景中,文件映射应优先使用
MAP_SHARED 以利用内核的脏页回写机制;匿名映射配合 MAP_POPULATE 标志可预分配物理页,避免运行时缺页抖动。
3.3 映射权限与标志位
mmap 的 prot 参数控制映射区域的访问权限,flags 参数控制映射的共享属性、内存分配策略以及内核行为。正确组合这两个参数是避免段错误、数据损坏或性能退化的关键。
3.3.1 权限位(prot)
| 标志 | 值 | 含义 |
|---|---|---|
PROT_READ |
0x1 | 允许读取映射区域 |
PROT_WRITE |
0x2 | 允许写入映射区域 |
PROT_EXEC |
0x4 | 允许执行映射区域中的指令 |
PROT_NONE |
0x0 | 禁止任何访问(常用于 guard page) |
注意:文件映射的权限不能超过打开文件时使用的
open() 模式。例如,以只读方式打开的文件,即使 mmap 指定 PROT_WRITE,内核也会返回 EACCES 错误。
3.3.2 核心标志位(flags)
| 标志 | 分类 | 语义说明 |
|---|---|---|
MAP_SHARED |
共享类型 | 映射区域的修改对所有共享该映射的进程可见,并会同步回文件(文件映射) |
MAP_PRIVATE |
共享类型 | 写时拷贝(Copy-on-Write),修改仅对当前进程可见,不写回文件 |
MAP_ANONYMOUS |
映射类型 | 创建匿名映射,忽略 fd 参数;通常与 MAP_SHARED 或 MAP_PRIVATE 组合 |
MAP_FIXED |
地址约束 | 强制使用 addr 指定的地址,若地址不可用则失败;谨慎使用 |
MAP_POPULATE |
预分配 | 在 mmap 返回时预先分配物理页,避免后续缺页中断(适用于实时场景) |
MAP_NORESERVE |
交换策略 | 不预留交换空间,允许超额承诺(overcommit),适用于大块稀疏映射 |
MAP_LOCKED |
锁定 | 将映射页锁定在物理内存中,禁止换出(需特权或 RLIMIT_MEMLOCK) |
3.3.3 常见组合模式
- 文件 I/O 加速:
MAP_SHARED | PROT_READ | PROT_WRITE,配合文件描述符,实现零拷贝读写。 - 进程间共享内存:
MAP_ANONYMOUS | MAP_SHARED | PROT_READ | PROT_WRITE,用于无亲缘关系进程间的数据交换。 - 私有匿名堆:
MAP_ANONYMOUS | MAP_PRIVATE | PROT_READ | PROT_WRITE,glibc malloc 在分配大于 128KB 的内存时内部使用此组合。 - 代码加载与执行:
MAP_PRIVATE | PROT_READ | PROT_EXEC,用于动态库映射,支持写时拷贝。
3.4 munmap 与映射生命周期管理
munmap 用于解除映射,释放虚拟地址区间。调用后,进程对该区域的访问将触发 SIGSEGV。内核会立即释放对应的物理页(对于文件映射,脏页会被标记为需要回写)。
- 参数要求:
addr必须是 mmap 返回的起始地址,length必须与 mmap 时一致(或为页对齐值)。 - 部分解除:Linux 支持对映射区域的部分解除,但
addr和length必须页对齐。 - 自动解除:进程退出时,所有映射区域会被内核自动解除;但显式调用 munmap 是良好的资源管理习惯。
性能要点:频繁的 mmap / munmap 操作会引入 TLB 刷新和页表更新开销。在需要反复映射同一文件的场景中,建议复用映射区域(通过 mremap 调整大小)或使用内存池缓存映射句柄。