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()(提示内核预读策略) |