彻底理解 mmap:从虚拟内存、缺页中断、写时复制到 Page Cache

在接触 Linux 内存管理时,我们反复听到虚拟内存、缺页中断、写时复制(COW)和 Page Cache 这些概念。而 mmap 正是将这些零散知识点串联起来的绝佳案例。本文用一张思维导图的脉络,通俗拆解 mmap 的底层原理与使用细节,让你真正"学透"而不仅仅是"背过"。

前置知识回顾

在进入 mmap 之前,先快速过一遍背景:

  • 虚拟内存:每个进程看到的是独立的、连续的地址空间,背后映射到不连续的物理页框。
  • 缺页中断:当进程访问的虚拟地址没有对应物理页时,触发缺页异常,内核分配物理页并填充数据。
  • COW(写时复制):多个进程共享同一物理页,一旦有进程需要写,就复制一份私有副本,保证隔离。
  • Page Cache:内核用于缓存文件数据的物理内存区域,读写文件实际上优先与 Page Cache 交互。

mmap 正是建立在这一套基础设施之上的上层机制。

一、mmap 到底是什么?

mmap 在进程虚拟地址空间创建一段内存区间,和磁盘文件建立映射关系;访问这段虚拟地址时,由缺页异常按需加载文件数据到内核页缓存,进程直接操作页面,**省去传统 read/write 中内核缓冲区到用户缓冲区的数据拷贝;磁盘 IO 依然会在缺页时发生,**优势是简化数据拷贝路径,并且可以减少频繁读写场景下的系统调用**。

三个核心关键字

  1. 虚拟地址映射
    mmap 调用时,内核只是在进程的页表中建立"虚拟地址 → 文件偏移"的逻辑绑定并不会立刻把文件内容读进物理内存
  2. 缺页中断
    当你第一次真正访问这段虚拟地址时,CPU 发现没有对应物理页,触发缺页中断。内核会分配物理内存,并从磁盘读取一页数据(通常 4KB)到物理内存的 Page Cache 中,然后更新页表。
  3. 按需加载
    你访问哪一页,内核才加载哪一页,而不是一次性把整个文件读进来。这就实现了懒加载,节省内存与时间。

mmap vs read/write 的本质区别

传统 read() 流程

复制代码
磁盘  →  内核 Page Cache  →  CPU拷贝  →  用户缓冲区(用户态内存)
  • 两次数据拷贝:①磁盘到 Page Cache(DMA),② Page Cache 到用户缓冲区(CPU 拷贝)。
  • 每次读写都要通过 read/write 系统调用,频繁切入内核态。

mmap 流程

复制代码
磁盘  →  内核 Page Cache (用户进程直接通过虚拟地址读写这块物理内存)
  • 减少一次 CPU 拷贝:数据从磁盘到 Page Cache 后,用户空间直接就能访问,无需再拷贝到用户缓冲区。
  • 减少系统调用 :之后读写就像操作普通内存指针,无需反复 read/write
  • 天然适合随机访问:想读文件任意偏移处的数据,直接指针偏移即可,内核只加载用到的页。
  • 总结
  • mmap 仅在初始化映射时执行一次 mmap 系统调用。
  • 当目标文件页面已经载入 PageCache:进程通过虚拟地址访问内存,MMU 完成地址翻译,全程用户态运行,无内核切换。
  • 如果页面尚未加载,访问内存触发硬件缺页异常被动进入内核加载数据;这和主动调用 read/write 系统调用不是一回事。
  • read/write 模式,每一次 IO 操作都需要程序主动发起系统调用,并且额外存在一次 PageCache 到用户缓冲区的 CPU 拷贝,这也是 mmap 性能优势来源。

二、mmap 怎么用?文件映射与匿名映射

mmap 主要分为两大类,背后的载体截然不同。

1. 文件映射(绑定磁盘文件)

通过 mmap 将一个普通文件映射进虚拟地址空间,底层依赖的就是 Page Cache。也就是说,被映射的文件数据会以页为单位缓存在 Page Cache 里,进程直接读写这些页。

2. 匿名映射(MAP_ANONYMOUS)

不关联任何磁盘文件,内核直接分配空白物理页 。数据没有文件背景,不经过 Page Cache

glibc 中,当 malloc 申请的内存大于 128KB 时,底层就会改用匿名 mmap,而不是调整堆的 brk
🔍 深入对比:文件映射与匿名映射的缺页和页面回收

文件映射(带 fd 普通 mmap)

mmap(fd, offset, len)

  • **创建阶段:**虚拟区间逻辑绑定「磁盘文件 + 文件偏移」;没有物理内存。
  • **首次访问触发缺页:**内核根据记录的磁盘位置,读取数据到 PageCache,生成文件页;虚拟页 ↔ 物理文件页。
  • **页面回收:**可以直接丢弃,不需要 Swap;再次访问重新从磁盘文件加载。

匿名映射(MAP_ANONYMOUS)

mmap(NULL, len, prot, MAP_ANONYMOUS | MAP_SHARED/MAP_PRIVATE, -1, 0)

  • **创建阶段:**虚拟区间没有绑定任何磁盘文件,内核只标记这是一段匿名映射区域;依旧不分配物理内存。
  • **首次访问触发缺页:**内核不会去磁盘读取任何文件,直接分配一块清零的物理内存页 → 匿名页;虚拟页 ↔ 匿名页。
  • **页面回收:**匿名页没有对应的磁盘原始文件!内存不足时必须把页面内容写入 Swap 分区;下次访问缺页,从 Swap 读回内存。

MAP_SHARED vs MAP_PRIVATE(重点理解 COW)

这两个标志决定了多个进程之间 以及与文件之间的交互方式。

MAP_SHARED(共享映射)
  • 多个进程映射同一个文件时,它们共享同一份 Page Cache 物理页
  • 任何一个进程对内存的修改,其他进程立刻可见
  • 修改产生的脏页最终由内核异步刷回磁盘 (或调用 msync() 强制同步)。
MAP_PRIVATE(私有映射 + 写时复制)
  • 初始状态:同样共享底层文件的物理页(Page Cache)。
  • 一旦某个进程尝试写 → 触发 写时复制(COW):内核为当前进程分配一个全新的物理页,复制原页内容,然后修改页表指向新页。后续的写入操作都在这个私有副本上进行。
  • 修改不会回写到原文件,对其他进程不可见。

💡 与 fork 的联动fork() 创建子进程时,父子进程的私有内存也是通过 COW 共享同一物理页。这背后的硬件机制(页表项写保护位、缺页陷阱)与 MAP_PRIVATE 完全一致。
即:MAP_PRIVATE 相当于在文件映射上实现了类似 fork 的 COW 行为。

三、mmap 常用在哪些场景?

1. 大文件随机访问

例如日志解析、简单的数据库存储引擎。利用 mmap 可以将随机读请求转化为直接内存访问,热数据留在 Page Cache,无需频繁 lseek + read

2. 进程间通信(共享内存 IPC)

使用 MAP_SHARED | MAP_ANONYMOUS 创建共享内存区域,多个进程可直接读写同一块物理内存,吞吐高、延迟低。

⚠️ 但内核不提供任何同步机制,需要用户自己用互斥锁、信号量或原子操作来避免竞态。

3. 动态库加载

Linux 加载动态链接库(.so)时,底层就是用 mmap 将库文件映射到进程地址空间,且通常使用 MAP_PRIVATE (修改不写回文件)或 MAP_SHARED(代码段只读)。

4. 大块匿名内存分配

如前所述,glibcmalloc 对于超过 MMAP_THRESHOLD(默认 128KB)的请求直接使用匿名 mmap,避免堆碎片化并方便独立释放。

四、为什么 mmap 性能好?三点优势总结

  1. 减少系统调用
    映射建立后,读写变成内存访问,无需反复 read/write
  2. 减少一次 CPU 数据拷贝
    从磁盘到用户空间,只需一次 DMA 到 Page Cache,用户态可直接使用。
  3. 按需加载 + 页缓存复用
    只有真正访问到的页才会加载,且 Page Cache 可被多个映射共享,节省物理内存。

五、注意事项

  1. 不要用于小文件或一次性顺序读写
    mmap 本身有系统调用和页表建立的固定开销,对于很小的文件或只需要顺序读一次的场景,传统 read 更快。
  2. 文件截断风险(SIGBUS)
    如果文件被外部 truncate 截断,而进程还在访问已映射但超出文件大小的地址,会收到 SIGBUS 信号,直接崩溃。必须通过信号处理或预先保护。
  3. MAP_SHARED 不是线程安全 / 进程安全的
    多个进程同时写共享映射区,必须自己用锁或原子操作保证同步,否则数据会错乱。
  4. 缺页风暴
    如果映射了超大区域,又进行完全随机的访问,可能导致大量缺页中断,性能不升反降。

六、总结

mmap 能够把磁盘文件映射至进程虚拟地址空间,将文件 I/O 转变为直接内存访问。它依靠缺页中断完成按需加载,核心收益是减少数据拷贝与系统调用开销。

该机制广泛用于大文件随机读写、进程间共享内存、动态链接库加载、大容量内存分配场景。

同时存在诸多使用限制:不适用于小规模顺序 I/O;文件截断可能触发 SIGBUS;MAP_SHARED 模式要求上层实现同步;纯粹随机访问会引发频繁缺页,造成性能损耗。

七、极易混淆的两个补充知识点

  1. 文件映射的缺页 vs 匿名映射的缺页
    • 文件映射缺页:内核从磁盘文件读取数据到 Page Cache,然后映射给进程。
    • 匿名映射缺页:内核分配一个空白物理页(通常全零填充),物理内存紧张时会被换出到 Swap 分区
  2. 文件映射页回收与 Swap 无关
    当内存紧张需要回收 Page Cache 中的文件映射页时,如果页是干净的(未修改),直接丢弃;如果是脏页,先写回文件再释放。
    绝不会被换出到 Swap 。这是因为文件映射的数据本身就有文件作为后备存储,缺页时重新从文件读取即可。
    而匿名页(无文件背景)没有后备存储,只能暂存到 Swap。
相关推荐
程序员AlbertTu5 小时前
第1章 操作系统初识
操作系统
free352 天前
【进程管理】进程与线程
操作系统
free352 天前
【进程管理】生命周期与调度算法
操作系统
free352 天前
【进程管理】Linux下的进程管理
操作系统
charlie1145141915 天前
Cinux · 第一次跳进 Ring 3:用户态与特权隔离
开发语言·c++·操作系统·开源项目
程序猿乐锅5 天前
【操作系统 | 第一章】从资源管理到虚拟机
操作系统
CoovallyAIHub7 天前
一个集装箱从船到火车的两个小时,AI 搭档 Coco在中间做了什么
操作系统·agent·资讯
驱动探索者7 天前
RISC-V 指令集深度解析:从 ISA 到 Zephyr
网络·计算机·操作系统·os
码农小韩8 天前
Linux操作系统(六)——软件包管理
linux·操作系统·linux驱动·嵌入式软件开发·linux应用