第 49 章 内存映射 (Memory Mappings)
核心结论
-
mmap() 本质:把文件或匿名内存映射进调用进程的虚拟地址空间——后续像访问普通内存一样访问映射区;页按需从文件加载或按需分配。
-
四种映射组合:file/anonymous × private/shared = 四种用途——private file (初始化内存)、private anon (malloc 大块)、shared file (mmap I/O + IPC)、shared anon (fork 后父子共享)。
-
mmap/munmap/m sync:
mmap()创建;munmap()解除(exec 自动解除;fork 子进程继承);msync()显式把 SHARED 映射刷回磁盘(MS_SYNC 阻塞 / MS_ASYNC 后台 / MS_INVALIDATE)。 -
MAP_PRIVATE 靠 COW:多个进程初始共享同一物理页;任一进程写入时内核复制一份新页给它(copy-on-write);写入不持久化到文件、不影响他进程。
-
MAP_ANONYMOUS vs /dev/zero:Linux 两种等价的创建匿名映射方式——MAP_ANONYMOUS (BSD 派) 或 open /dev/zero + mmap (SysV 派);都把字节初始化为 0。
-
mmap I/O 优劣:避免 read/write 的「内核缓冲→用户缓冲」二次拷贝——随机大文件访问有性能优势;顺序小块访问无优势;小 I/O 开销反而更大(page fault + TLB miss)。
-
SIGSEGV vs SIGBUS:访问超出映射范围 → SIGSEGV;映射超出文件大小(且无对应页) → SIGBUS(仅 file mapping);违反 PROT_* 权限 → SIGSEGV。
-
非线性映射:remap_file_pages() (Linux 特有) 操纵页表把文件页重排——避免为每个非线性段创建独立 VMA;仅适用于 MAP_SHARED。
|
本章主旨
本章是第 50 章「虚拟内存操作」(mprotect/mlock/madvise)和第 54 章「POSIX 共享内存」的前置。mmap() 是 Linux 一切内存映射机制的母调用——文件 I/O、进程间通信、动态库加载、malloc 大块、JIT 代码生成都基于它。读者需要建立四象限思维:(1) 映射有没有 backing file?→ file vs anon;(2) 写入是否可见于他进程?→ shared vs private。掌握这两个维度之后,所有 mmap 衍生 API(shm_open、mremap、remap_file_pages)都变得自然。 |
一、核心概念
本章围绕 6 个核心概念展开:从映射四象限、mmap/munmap、mmap 标志、匿名映射、msync 到非线性映射。
| 概念 | 定义 + 重要性 | 实现提示 |
|---|---|---|
映射四象限 (file/anonymous × private/shared) |
file/anonymous 决定 backing;private/shared 决定写入可见性;组合得到 private-file(init text/data)、private-anon(malloc 大块)、shared-file(mmap I/O + IPC)、shared-anon(fork 父子共享) |
表 49-1;private 用 COW 实现;shared 写入即时可见且持久到文件 |
mmap() / munmap() / 保护位 |
|
§49.2;addr=NULL 让内核选(推荐);offset 必须页对齐;length 向上取整到页;SUSv4 放松对齐要求 |
prot × flags × open mode 矩阵 |
写入映射需要 PROT_WRITE + O_WRONLY/O_RDWR;只读映射允许 O_RDONLY;MAP_PRIVATE + O_RDONLY 可任意 prot(不写回文件) |
§49.4.4;O_WRONLY + mmap 报 EACCES(硬件不允许 write-only 页);PROT_WRITE 隐含 PROT_READ |
mmap I/O 性能模型 |
read/write 双缓冲(kernel buf cache + user buf);mmap 单缓冲(kernel 与 user 共享同一物理页);随机大文件访问性能优、顺序小 I/O 无优势 |
§49.4.2;性能提升需 msync/sync_file_range 帮助 write-back;mmap 不保证写入时机 |
MAP_ANONYMOUS 与 /dev/zero |
两种等价匿名映射创建方式;MAP_ANONYMOUS 来自 BSD(需 _BSD_SOURCE 或 _GNU_SOURCE),/dev/zero 来自 SysV;都把字节初始化 0;MAP_SHARED |
MAP_ANONYMOUS 可让父子共享 |
§49.7;MAP_SHARED |
MAP_ANONYMOUS 自 Linux 2.4 才支持;glibc malloc() 用 MAP_PRIVATE anon 实现大块分配(≥ MMAP_THRESHOLD=128KB) |
msync() 与非线性映射 |
二、详细笔记
49.1 映射四象限
What:mmap 创建的映射有两维度——(1) 是否 backing 文件(file vs anonymous);(2) 写入可见性(private vs shared)。
Why:四象限对应四种典型用途——理解后能一眼看出 mmap 参数如何选择。
How:
| 类型 | 用途 | 写入行为 |
|---|---|---|
Private file |
初始化进程 text 段、data 段;加载 .so;mmcat 类似 cat 的简单映射 |
写入不持久化到文件,不影响他进程;COW |
Private anonymous |
malloc 大块(≥128KB);guard page;分配零填充内存 |
写入私有;fork 后父子各自一份(COW) |
Shared file |
内存映射 I/O;无关进程通过同一文件 IPC |
写入立即可见于他进程;最终持久化到文件 |
Shared anonymous |
fork 后父子共享内存;MAP_SHARED |
MAP_ANONYMOUS,-1,0 |
When:写私有内存块 → private anon;加载 .so → private file;进程间共享文件数据 → shared file;fork 后父子共享小段内存 → shared anon。
Example:glibc malloc() 对 ≥ MMAP_THRESHOLD (默认 128 KB) 的分配用 mmap(MAP_PRIVATE|MAP_ANONYMOUS, …)——大块 free 时直接 munmap() 归还 OS,避免碎片。
49.2 mmap() / munmap() / 保护位
What:mmap(addr, len, prot, flags, fd, offset) 创建新映射;munmap(addr, len) 撤销;prot 是 PROT_NONE/READ/WRITE/EXEC 的位或。
Why:mmap 是其他一切映射机制的母调用;munmap 对应解除。
How:
// 摘自《The Linux Programming Interface》 第 49 章
#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);
/* mmcat:mmap + write 实现 cat */
int fd = open(argv[1], O_RDONLY);
struct stat sb;
fstat(fd, &sb);
char *addr = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (addr == MAP_FAILED) errExit("mmap");
write(STDOUT_FILENO, addr, sb.st_size);
munmap(addr, sb.st_size);
prot 取值(§49.2 表 49-2):
-
PROT_NONE— 区域不可访问。 -
PROT_READ— 可读。 -
PROT_WRITE— 可写。 -
PROT_EXEC— 可执行。
flags 必含其一:MAP_PRIVATE 或 MAP_SHARED。违反保护位访问 → SIGSEGV。
When:始终 addr = NULL 让内核选地址(最可移植);offset 必须页对齐(SUSv3;SUSv4 放松);length 自动向上取整到页。
Example:第 49 章 t_mmap.c 用 mmap(NULL, MEM_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0) 把命令行第二个参数写入共享文件映射——多次运行能累积修改。
49.3 mmap I/O 与 read/write 性能对比
What:用 MAP_SHARED 文件映射替代 read/write——程序直接对映射区做指针操作;内核自动把修改传播到 backing file。
Why:随机大文件访问场景能省掉一次内核↔用户缓冲的拷贝。
How:数据流对比(§49.4.2):
| 操作 | read/write 路径 | mmap 路径 |
|---|---|---|
输入 |
disk → kernel buf cache → user buf → 程序 |
disk → 同一物理页(unified VM) → 程序 |
输出 |
程序 → user buf → kernel buf cache → disk |
程序 → 同一物理页 → kernel 自动刷盘(msync 显式) |
多进程同文件 |
每个进程独立 user buf |
所有进程共享同一 kernel 页 |
When:mmap 适合——(1) 随机访问大文件;(2) 多进程共享;(3) 把文件当内存结构访问(cast struct)。不适合——(1) 顺序小块 I/O;(2) 小 I/O(page fault + TLB miss 开销大于 read/write);(3) 网络 socket/pipe(不能用 mmap)。
Example:第 49 章 mmcat.c 把整个文件 mmap 进内存后直接 write(STDOUT, addr, size)——比 read+write 少一次 copy。
49.4 匿名映射(MAP_ANONYMOUS / /dev/zero)
What:没有 backing file 的映射——常用于分配进程私有内存或父子共享内存。
Why:替代 malloc;实现父子进程间零成本共享。
How:
// 摘自《The Linux Programming Interface》 第 49 章
/* 方式 1:MAP_ANONYMOUS(BSD 派) */
addr = mmap(NULL, length, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
/* 方式 2:open /dev/zero(SysV 派) */
int fd = open("/dev/zero", O_RDWR);
addr = mmap(NULL, length, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
close(fd);
/* 父子共享:MAP_SHARED|MAP_ANONYMOUS + fork */
addr = mmap(NULL, sizeof(int), PROT_READ|PROT_WRITE,
MAP_SHARED|MAP_ANONYMOUS, -1, 0);
*addr = 1;
switch (fork()) {
case 0: (*addr)++; break; /* child */
default: wait(NULL); printf("%d\n", *addr); break; /* parent 看到 2 */
}
glibc 用 MAP_PRIVATE|MAP_ANONYMOUS 实现大块 malloc(≥128 KB)——free 时直接 munmap 归还 OS。
When:MAP_PRIVATE 用于分配零填充私有内存;MAP_SHARED|MAP_ANONYMOUS 用于 fork 后父子共享。Linux 2.4+ 才支持 MAP_SHARED anon。
Example:第 49 章 anon_mmap.c 演示两种创建方式(USE_MAP_ANON 宏切换)——父子共享一个 int,子进程 (*addr)++ 后父进程看到 2。
49.5 msync() 同步控制
What:msync(addr, length, flags) 显式把 MAP_SHARED 映射刷回 backing file,或让文件的他进程写入可见于本进程。
Why:内核不保证写入时机——msync 给程序显式控制点。
How:
// 摘自《The Linux Programming Interface》 第 49 章
#include <sys/mman.h>
int msync(void *addr, size_t length, int flags);
flags 三选一:
-
MS_SYNC— 阻塞直到所有修改页写入磁盘。 -
MS_ASYNC— 后台异步刷盘;可调fsync(fd)阻塞等完成。 -
MS_INVALIDATE— 让文件的他进程写入失效本地缓存,下次访问从文件重新加载。
When:(1) 数据库/事务系统需强制持久化 → MS_SYNC;(2) 让他进程 read() 看到本进程写入 → MS_SYNC 或 MS_ASYNC;(3) 让本进程看到他进程 write() 写入文件 → MS_INVALIDATE。
Example:第 49 章 t_mmap.c 在 memset + strncpy 后调 msync(addr, MEM_SIZE, MS_SYNC)——保证命令行参数真的写盘,下次进程能读到。
49.6 其他 mmap 标志与非线性映射
What:除 MAP_PRIVATE/MAP_SHARED 外,Linux 支持 MAP_ANONYMOUS、MAP_FIXED、MAP_LOCKED、MAP_HUGETLB、MAP_NORESERVE、MAP_POPULATE、MAP_UNINITIALIZED;mremap() 调整大小;remap_file_pages() 做非线性映射。
Why:高级场景——大页(HugeTLB)、预读、稀疏数组、嵌入式优化。
How:
// 摘自《The Linux Programming Interface》 第 49 章
#include <sys/mman.h>
/* mremap 调整大小(Linux 特有) */
void *mremap(void *old_addr, size_t old_size, size_t new_size,
int flags, ...);
#define MREMAP_MAYMOVE 1 /* 允许内核搬迁 */
#define MREMAP_FIXED 2 /* 必须配合 MREMAP_MAYMOVE */
/* remap_file_pages 非线性(Linux 特有,自 2.6) */
int remap_file_pages(void *addr, size_t size, int prot,
size_t pgoff, int flags);
MAP_NORESERVE 与 /proc/sys/vm/overcommit_memory 配合——overcommit=0(默认)下,私有可写映射需预留 swap;MAP_NORESERVE 跳过预留(适合稀疏数组)。
When:remap_file_pages() 用于数据库、虚拟机(避免为每个 view 创建独立 VMA);mremap() 用于 realloc() 大块 mmap 内存(glibc realloc 内部用 mremap)。
Example:第 49 章 §49.11 例:mmap(0, 3*ps, …) 后 remap_file_pages(addr, ps, 0, 2, 0) 把 file page 0 映射到 memory page 2。
三、关键图表
|
非可视化条目(系统调用 / 标志)
|
四、思维导图
mindmap
root((第 49 章 内存映射))
映射四象限
file vs anon
private vs shared
private file 初始化
shared file IPC
shared anon fork 共享
private anon malloc
mmap munmap
addr NULL 推荐
prot 保护位
length 页对齐
exec 自动撤销
fork 继承
mmap I/O 性能
随机大文件优
顺序小块无优势
少一次拷贝
unified VM
匿名映射
MAP_ANONYMOUS
dev zero
MAP_SHARED anon fork
glibc malloc
msync 同步
MS_SYNC 阻塞
MS_ASYNC 后台
MS_INVALIDATE 失效
高级标志
MAP_FIXED
MAP_HUGETLB
MAP_LOCKED
MAP_NORESERVE
mremap
remap_file_pages
五、重点与易错点
-
「mmap 创建的是虚拟地址映射,不是物理内存」——物理页按需分配;第一次访问触发 page fault 才真正从文件加载或分配。
-
MAP_PRIVATE 写入靠 COW——多个进程初始共享同一物理页(fork 也共享);任一写入触发内核复制新页;写入不持久化、不影响他进程。
-
prot 与 open mode 必须匹配——
PROT_WRITE + MAP_SHARED要求O_RDWR;O_WRONLY+ mmap 报EACCES(硬件不允许 write-only 页)。 -
MAP_SHARED file = mmap I/O + IPC——修改持久化到文件;多进程 mmap 同一文件立即看到互相写入(unified VM 下)。
-
MAP_SHARED anon 必须 fork 后才共享——同进程多次 mmap SHARED|ANONYMOUS 互不共享(每次调用创建独立映射);只有 fork 继承才共享。
-
MS_SYNC 阻塞刷盘 vs MS_ASYNC 后台——MS_SYNC 等磁盘写入完成;MS_ASYNC 后台启动,可调
fsync(fd)阻塞等。 -
SIGSEGV vs SIGBUS——超出映射区 → SIGSEGV;映射超出文件大小且无可对应页(file mapping) → SIGBUS;违反 PROT_* → SIGSEGV。
-
mmap 偏移必须页对齐——
offset % PAGE_SIZE == 0;SUSv3 严格要求;SUSv4 放松。 -
mmap 不保证写入时机——必须
msync或fsync才能保证持久化;否则可能进程崩溃导致数据丢失。 -
mmap 性能优势场景——随机大文件 + 多进程共享;顺序小 I/O 用 read/write 更简单也更快。
-
glibc malloc 大块用 mmap——默认阈值 128 KB;
mallopt(M_MMAP_THRESHOLD, …)调整;大块 free 直接 munmap 避免碎片。 -
remap_file_pages() 已废弃——Linux 4.0+ 内核仍保留但不再推荐;新代码用 MAP_FIXED 多 mmap 或 fallocate + mmap。
-
mremap() 仅 Linux——可移植代码不能用;BSD/Solaris 不支持;用 munmap + mmap 替代。
-
fork 继承映射 + MAP_NORESERVE——子进程继承父进程的 MAP_NORESERVE 设置。
-
exec 撤销所有映射——exec 后进程地址空间全新;mmap 创建的映射全部消失。
-
跨章衔接:第 50 章「虚拟内存操作」讲 mprotect(改保护)/mlock(锁页)/madvise(建议)/posix_madvise;第 54 章「POSIX 共享内存」用 shm_open + mmap 实现无关进程 IPC;第 55 章「文件锁」解决 mmap I/O 的并发写入协调。