文档目录

mmap 是零拷贝技术的第一把核心武器。它解决的是传统 IO 中最痛苦的一步:内核缓冲区 → 用户缓冲区 的 CPU 拷贝。

它的核心思想极其简单,但硬件实现极其精妙:让用户态程序直接看到内核的 Page Cache,而不是把数据拷贝一份给用户。


1. 传统 read() 的内存布局 vs mmap 的内存布局

传统 read():数据被拷贝了一次

+-------------------+          +-------------------+
|   用户态地址空间   |          |   内核态地址空间   |
|                   |          |                   |
|  +-------------+  |          |  +-------------+  |
|  | 用户缓冲区   |  |          |  | Page Cache  |  |
|  | (buf)       |  |          |  | (磁盘数据)   |  |
|  +------^------+  |          |  +------^------+  |
|         |         |          |         |         |
|         +---- CPU 拷贝 (memcpy) ----+         |
|                   |          |                   |
|                   |          |  +------+------+  |
|                   |          |  | 磁盘/文件    |  |
|                   |          |  +-------------+  |
+-------------------+          +-------------------+

数据路径:磁盘 → (DMA) → Page Cache → (CPU 拷贝) → 用户缓冲区

mmap:用户态直接映射 Page Cache

+-------------------+          +-------------------+
|   用户态地址空间   |          |   内核态地址空间   |
|                   |          |                   |
|  +-------------+  |          |  +-------------+  |
|  | 用户缓冲区   |  |          |  | Page Cache  |  |
|  | (mmap映射)  |  |  ← 共享同一块物理内存 →  |  | (磁盘数据)   |  |
|  +-------------+  |          |  +------^------+  |
|                   |          |         |         |
|                   |          |  +------+------+  |
|                   |          |  | 磁盘/文件    |  |
|                   |          |  +-------------+  |
+-------------------+          +-------------------+

数据路径:磁盘 → (DMA) → Page Cache → 用户态直接访问(无拷贝)

关键差异:mmap 将内核空间的 Page Cache 物理内存页,直接映射到用户进程的虚拟地址空间。用户态程序访问这个地址,就是直接访问 Page Cache,不需要任何 CPU 拷贝。


2. mmap 的硬件原理:MMU 页表共享

mmap 的本质是修改 MMU(内存管理单元)的页表,让用户态和内核态共享同一组物理页。

实现步骤

[1] 磁盘数据通过 DMA 写入主存的物理页 0x1000(Page Cache)
[2] 内核在页表中建立两条映射:
      - 内核虚拟地址 0xFFFF8000... → 物理页 0x1000
      - 用户虚拟地址 0x7F0000... → 物理页 0x1000  ← 新增的映射!
[3] 用户态程序读取 0x7F0000... → MMU 查页表 → 直接访问物理页 0x1000

硬件角色

角色 做了什么
DMA 控制器 将磁盘数据直接搬运到物理页 0x1000(Page Cache)
MMU 维护页表,实现用户态虚拟地址到同一物理页的映射
CPU 执行用户态程序,当访问 mmap 返回的地址时,直接触发 TLB 命中,访问物理页

整个过程中,CPU 没有执行过一次 memcpy,它只是访问内存,数据早在 Page Cache 中了。


3. mmap 的“双面性”:好处与代价

好处

优势 说明
零 CPU 拷贝 数据从磁盘到内存(DMA)后,用户态直接访问,无需拷贝。
减少系统调用 mmap 只需要一次系统调用建立映射,后续访问不需要 read/write 陷入内核。
内存复用 多个进程可以同时 mmap 同一个文件,共享同一份物理内存,节省主存。

代价(这是你要记住的陷阱)

代价 硬件原因
Page Fault 开销 首次访问 mmap 区域时,MMU 查页表发现未命中 → 触发缺页中断 → 内核把物理页挂到进程页表 → 代价 ~1μs
TLB 压力 用户态和内核态共享物理页,需要两个虚拟地址映射同一个物理页 → 占用 TLB 条目
同步问题 如果内核修改了 Page Cache(如写回磁盘),用户态可能看到不一致的数据,需要 msync 强制刷新
无法用于小文件 mmap 需要分配页表条目,建立映射,对于小文件(< 4KB),系统调用开销可能比 read 还大

4. mmap 与 DMA 的协同工作流程(完整时序)

以读取文件并发送到网卡为例(零拷贝的关键路径):

[步骤 1] 用户态调用 mmap()
         └─→ 内核在进程的 VMA(虚拟内存区域)中记录映射,但不分配物理页

[步骤 2] 用户态访问 mmap 返回的地址
         └─→ MMU 查页表 → 未命中 → 触发缺页中断
         └─→ 内核发起磁盘 I/O(DMA 读取到 Page Cache 物理页)
         └─→ 内核在页表中建立映射:用户虚拟地址 → 该物理页

[步骤 3] DMA 完成传输,数据在 Page Cache 中
         └─→ 用户态程序再次访问同一地址 → TLB 命中 → 直接读取数据(无拷贝)

[步骤 4] 用户态程序处理完数据(如解析 HTTP 头),调用 write() / send()
         └─→ 数据直接从 Page Cache 经 DMA 发送到网卡(如果使用 sendfile,则没有用户态参与)

关键:步骤 2 的缺页中断是一次性的。后续访问都在 TLB 命中路径上,完全无开销。


5. mmap 在零拷贝链路中的定位

传统 IO:磁盘 → Page Cache (DMA) → 用户缓冲区 (CPU) → Socket 缓冲区 (CPU) → 网卡 (DMA)
         ├─────────────────────┤   ├───────────────┤   ├─────────────────────┤
              拷贝 ① (DMA)           拷贝 ② (CPU)         拷贝 ③ (DMA)

mmap:   磁盘 → Page Cache (DMA) → 用户态直接访问 → Socket 缓冲区 (CPU) → 网卡 (DMA)
         ├─────────────────────┤   ├───────────────┤   ├─────────────────────┤
              拷贝 ① (DMA)       无 CPU 拷贝!      拷贝 ③ (DMA)

mmap 消除了步骤 ② 中的 CPU 拷贝,但用户态和 Socket 缓冲区之间的拷贝(步骤 ③)依然存在。


6. 笔记存档:mmap 核心概念速查

概念 解释
定义 将文件/设备的内存映射到用户进程的虚拟地址空间,实现零拷贝访问。
硬件基础 MMU 页表共享(同一物理页对应内核虚拟地址 + 用户虚拟地址)
消除了什么 消除 read()/write() 中的内核↔用户 CPU 拷贝(memcpy)
新增了什么开销 缺页中断(首次访问)、TLB 额外条目占用
适用场景 大文件传输(> 4KB)、多进程共享同一文件、内存数据库
不适用场景 小文件(< 4KB)、需要频繁修改数据且同步写回的场景
关键补充函数 msync()(强制同步写回)、munmap()(解除映射)、madvise()(提示内核预读策略)