mmap + page cache(零拷贝)详解
1) 什么是“零拷贝”?要解决的问题
传统 I/O 在把数据从应用发送到磁盘或网络时,会在用户态 ↔ 内核态之间做多次拷贝(消耗 CPU 与内存带宽):
- 用户缓冲区 → 内核缓冲区(write)
- 内核缓冲区 → 网卡(DMA)
“零拷贝”是指尽量减少或避免这些内核/用户之间的内存拷贝,把数据直接由内核页缓存(page cache)或 DMA 使用,从而节省 CPU 和内存带宽,提高吞吐并降低延迟。
重要:操作系统层面通常仍会有内存页的移动/引用,但避免了显式的数据拷贝(memcpy)在用户空间和内核空间之间。
2) page cache 是什么?它的作用
- page cache(页缓存) 是操作系统用来缓存磁盘数据的内存区域(以页为单位,通常 4KB)。
- 读文件时,内核会把文件页读入 page cache,随后应用直接从内存读取(避免磁盘)。
- 写文件时,内核把数据写入 page cache 的脏页(dirty page),并在后台异步写回磁盘(写回叫 writeback)。
关键要点:
- page cache 是“内核的内存映射区域”,是 mmap 零拷贝的核心。
- 在有 page cache 的情况下,很多读写操作不需要直接与磁盘交互。
查看 page cache(Linux):/proc/meminfo 的 Cached 字段,free -m 等。
3) mmap 是什么?它做了什么?
mmap 把磁盘文件的一段映射到进程的虚拟地址空间——文件内容就像内存一样可以直接读写。
行为要点:
- 第一次访问映射的地址会触发 page fault,内核把对应页从磁盘读入 page cache,然后映射到进程页表。
- 写入 mmap 区域会把该页标记为 dirty,最终由内核异步 flush 到磁盘,或者通过
msync()/fsync()强制同步到磁盘。 - mmap 操作本身主要是建立页表映射(没有拷贝文件内容到用户缓冲区的 memcpy),因此在读写上节省了内核/用户拷贝。
常用标志:
- MAP_SHARED:写入影响底层文件(且会被写回磁盘)
- MAP_PRIVATE:写入为写时复制,不影响原文件
- PROT_READ/PROT_WRITE:权限
msync(或msync(MS_SYNC))用于显式刷新修改到磁盘
4) mmap + page cache 为什么属于“零拷贝”?
- 当应用
mmap映射文件并直接写内存时,没有发生write()的用户态 → 内核态的拷贝(即没有额外 memcpy)。 - page cache 已经持有数据页;写网络(例如
sendfile())时内核可以直接把 page cache 页 DMA 到网卡,避免把页先拷贝到内核缓冲区再发送。 - 结果:用户态数据直接在映射内存被修改,内核在需要时把页缓存给硬件用,省掉了多次内存拷贝。
举例对比:
- 传统:用户缓冲区 → (write) → 内核缓冲区 → (DMA) → 网卡
- mmap/sendfile:文件页(page cache)→ (DMA) → 网卡;用户只是修改映射页(无 memcpy)
5) mmap 的生命周期与页(page)细节
- Page fault(缺页中断):首次访问触发,把文件页读入 page cache 并映射到进程地址空间。
- Dirty page:写后页被标记为 dirty;内核会在后台 writeback。
- writeback:内核把 dirty page 写回磁盘(异步),通过
pdflush/flush线程。 - msync/munmap/fdatasync/fsync:显式或间接触发写回并保证磁盘持久化。
- swap:内存压力下,页可能被换出到交换区(注意:映射文件的写入页换出也可能触发回写)。
6) durability(持久性)与一致性:风险与刷盘策略
风险:如果只把数据写入 page cache(或 mmap 后未 msync),突然断电或系统崩溃,内存中的 dirty page 可能尚未写盘,数据会丢失。
保证不丢失的做法(工程实践):
msync(..., MS_SYNC)或fsync():强制把内核 page cache 的数据刷进磁盘(阻塞直到写入设备)。- 打开文件时使用
O_DSYNC/O_SYNC:写系统调用本身等待数据写回磁盘(代价高)。 - 对分布式消息系统(如 RocketMQ):使用同步复制(SYNC_MASTER)+ 同步刷盘(SYNC_FLUSH)或使用 DLedger(一致性复制),把风险转移到多个副本上。
吞吐 vs 持久性:越强的持久性(sync),吞吐越低。工程上常见折中:
- 业务允许丢数据少量:异步刷盘 + 异步复制,提高吞吐
- 严格不丢:同步刷盘 + 同步复制或多副本多数写入
7) Zero-copy 在网络 I/O 的实现(sendfile/splice)
mmap 用于文件映射,sendfile(或 splice)在发送文件到 socket 时能把 page cache 的数据直接传给内核网络栈并 DMA 到 NIC,从而实现端到端零拷贝:
- sendfile 从 page cache 拿页,直接发往网卡(避免把页拷贝到用户态缓冲区再 write)
- splice 也能在文件描述符之间传递数据,避免拷贝
RocketMQ 场景:
- 存储侧:mmap 提升磁盘写入的效率
- 网络侧:如果需要可以利用 sendfile/zero-copy 发送数据(某些 broker 实现会尽量减少拷贝)
8) mmap 在 Java 中的使用注意(MappedByteBuffer)
Java 提供 MappedByteBuffer(FileChannel.map())。注意点很多:
-
不会自动释放:
MappedByteBuffer的 native 映射可能被 GC 延迟释放(导致文件无法删除或日志滚动失败)。常见办法:- 使用
sun.misc.Cleaner(非标准,危险) - Java 9+ 使用
Unsafe.invokeCleaner(...)(需要权限) - 使用 JNI 或外部进程管理文件滚动
- 使用
-
强制刷新:
MappedByteBuffer.force()对应msync,但可能性能开销大。 -
页面对齐:操作需考虑页大小(4KB),避免半页写。
-
堆外内存与 GC:映射的内容在 native 内存,不受 Java GC 管控,但要注意内存压力和 OOM。
-
权限问题:映射大量文件会占用虚拟地址空间;32 位 JVM 更受限。
RocketMQ(Java 实现)通过专门的内存池和清理机制规避这些问题——比如 transientStorePool、MappedFile 管理类、定制清理流程。
9) 实际工程中的调优点与陷阱(必读)
- 刷盘策略:选择 SYNC_FLUSH(安全)还是 ASYNC_FLUSH(高吞吐)要权衡。
- 主从复制策略:ASYNC_MASTER 有数据丢失风险,SYNC_MASTER 更安全但延迟高。
- 禁用 swap 或 mlock?:关键服务可考虑
mlock关键页以避免被 swap;也可通过vm.swappiness=1降低swap影响。 - 页大小与对齐:避免频繁的小写导致大量小的 dirty page;批量写入效率更高(顺序写)。
- 内存压力:page cache 会占用大量内存,监控
Cached/Dirty页面,设置vm.dirty_ratio与vm.dirty_background_ratio合理阈值。 - 文件句柄与虚拟地址空间:大文件或大量映射会耗尽 fd 或虚拟地址;在容器/单机上要调
ulimit -n与 JVM 选项。 - MappedByteBuffer 释放:在 Java 中特别注意映射文件释放的实现,避免文件锁死。
- NUMA/CPU 缓存一致性:在多 NUMA 节点上,映射和 DMA 绑定策略会影响性能;需要考虑内存分配策略和 CPU 亲和性。
- IO 调度与 readahead:顺序写可以禁用 readahead,随机读场景需提高 readahead 或使用 madvise(MADV_SEQUENTIAL)。
10) 常用诊断命令 / 性能测试手段
free -m/cat /proc/meminfo:查看 page cache 占用vmstat 1:查看 writeback、io等待iostat -x 1:磁盘 I/O 性能ss -tin/tcpdump:网络层问题perf/ftrace:追踪 page fault 与 syscall 开销fio:磁盘基准(顺序写、随机写)- Java 层:
jmap/jcmd/jstack,以及查看 MappedByteBuffer 使用
11) 在 RocketMQ 场景下的具体应用
RocketMQ 的 CommitLog 采用 mmap + page cache 的组合(并结合自己的内存池/刷盘策略)带来几个好处:
- 顺序写:写入同一本地 commit log 的开销极低(顺序写 + mmap)
- 高吞吐:大量消息写入几乎不用 memcpy,靠 page cache 聚合写回
- 快速读取:消费者通过索引(ConsumeQueue)定位 CommitLog,内核页缓存命中率高,读取延迟低
但 RocketMQ 为了保证可靠性会提供两类刷盘/复制策略以平衡持久性与吞吐:
- 异步刷盘 + 异步复制:极高吞吐、有丢失可能
- 同步刷盘 + 同步复制(或 DLedger):低吞吐、强可靠
RocketMQ 还实现了 transientStorePool(临时存储池)来缓解 mmap 写入热/冷页面带来的抖动。
12) 工程师实战建议(快速清单)
- 生产环境:默认使用 mmap + page cache(高性能),但对于强一致场景同时使用 SYNC_MASTER + SYNC_FLUSH 或 DLedger。
- 如果业务允许少量数据丢失:启用异步刷盘以换取吞吐。
- 在 Java 中:使用成熟的 MappedFile 管理类并严格处理资源释放(避免文件锁死)。
- 监控指标:
Dirty页、writeback、fsync延迟、磁盘队列长度、page fault rate。 - 测试:用
fio/ 自制压测脚本做顺序写/顺序读/并发读写基准,并在不同刷盘策略下对比吞吐与延迟。 - 容器化时:注意虚拟地址空间和 ulimits,尽量运行在有足够内存和调好 swappiness 的宿主机上。
结语(一句话)
mmap + page cache 的价值在于把磁盘 I/O 的“显式拷贝”交给内核页缓存管理,从而节省 CPU 和内存带宽;但要记住:这把可靠性问题(是否写盘)和性能问题(何时 flush)交给了你作为工程师去正确配置。