页缓存技术(PageCache/sendfile/mmap)

核心概念解析

1. PageCache(页缓存)

  • 是什么
    Linux 内核将磁盘文件数据缓存在物理内存中的一块区域,称为 PageCache。
  • 作用
    • 读操作:若数据在 PageCache 中,直接返回,避免磁盘 I/O
    • 写操作:先写入 PageCache,由内核异步刷盘(write-back),提升写性能。
  • 特点
    • 对所有进程透明共享;
    • LRU 策略淘汰冷数据;
    • 是"零拷贝"技术的基础。

2. mmap(Memory-Mapped File)

  • 是什么
    将一个文件直接映射到用户进程的虚拟地址空间,程序可像操作数组一样读写文件。

  • 系统调用

    c 复制代码
    void *addr = mmap(NULL, length, PROT_WRITE, MAP_SHARED, fd, 0);
  • 工作原理

    • 不立即加载文件内容;
    • 当程序访问 addr[i] 时,触发缺页中断(Page Fault)
    • 内核分配物理页,从磁盘(或 PageCache)加载数据;
    • 后续读写直接操作内存,无需 read()/write() 系统调用
  • 优点

    • 减少系统调用和上下文切换;
    • 适合大文件随机访问或高频小写。

注意:mmap 本身不等于零拷贝,但它为高效 I/O 提供了基础。

3. sendfile(零拷贝网络传输)

  • 是什么
    Linux 提供的系统调用,允许直接从文件描述符传输数据到 Socket,全程由内核完成。

  • 系统调用

    c 复制代码
    ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
  • 工作原理(Linux 2.4+)

    • 数据路径:磁盘 → PageCache → 网卡
    • CPU 不参与数据拷贝,仅由 DMA 控制器完成;
    • 内核通过 Scatter-Gather DMA 直接组装网络包。
  • 前提条件

    • 源必须是文件(支持 mmap);
    • 目标必须是 Socket;
    • 数据需在 PageCache 中(否则先加载)。

sendfile 是真正的"零拷贝"(Zero-Copy)。

交互流程图(Mermaid Sequence Diagram)

以下分别绘制 传统 I/Ommap 写入sendfile 读取 三种场景的数据流,并标注关键组件交互。
Disk Kernel (PageCache) NIC (Network) User Process Disk Kernel (PageCache) NIC (Network) User Process 场景1: 传统 read() + write() ------ 4次拷贝, 4次上下文切换 场景2: mmap 写入文件 ------ 1次CPU拷贝, 0次显式系统调用 场景3: sendfile 读取并发送 ------ 0次CPU拷贝, 零拷贝! 1. read(fd, buffer, size) 2. DMA: 磁盘 → PageCache 3. 数据加载完成 4. CPU拷贝: PageCache → 用户buffer 5. write(socket, buffer, size) 6. CPU拷贝: 用户buffer → Socket缓冲区 7. DMA: Socket缓冲区 → 网卡 8. 发送完成 9. *(mmap_addr + offset) = data 10. 缺页中断 (Page Fault) 11. (若冷数据) DMA: 磁盘 → PageCache 12. 映射物理页到虚拟地址 13. 数据已写入 PageCache 14. (异步) DMA: PageCache → 磁盘 15. sendfile(socket, file_fd, offset, count) 16. (若冷数据) DMA: 磁盘 → PageCache 17. DMA: PageCache → 网卡 (Scatter-Gather) 18. 发送完成

关键对比:拷贝次数与上下文切换

场景 CPU 拷贝次数 DMA 拷贝次数 上下文切换 是否零拷贝
传统 I/O 2 次 2 次 4 次
mmap 写入 0~1 次* 1~2 次 0~1 次* ❌(但高效)
sendfile 读取 0 次 2 次 2 次

*注:mmap 首次访问触发缺页中断算 1 次上下文切换,后续无开销。

三者如何协同工作?

在高性能服务中,mmap + PageCache + sendfile 可形成完美闭环

  1. 写入阶段
    • 应用通过 mmap 将数据写入文件 → 数据进入 PageCache
  2. 读取阶段
    • 另一请求调用 sendfile 读该文件 → 内核发现数据已在 PageCache
    • 直接通过 DMA 将 PageCache 数据发往网卡;
  3. 全程
    • 无 CPU 拷贝
    • 无用户态参与
    • 延迟最低、吞吐最高

🌰 典型应用:

  • Web 服务器(如 Nginx)静态文件服务;
  • 消息队列(RocketMQ/Kafka)消息读取;
  • 数据库 WAL 日志传输。

注意事项

  1. PageCache 是共享资源

    • 若系统内存不足,PageCache 被回收 → 性能骤降;
    • 建议为 I/O 密集型服务预留足够内存。
  2. mmap 不等于持久化

    • 数据在 PageCache 中可能因断电丢失;
    • 需调用 msync() 或依赖 OS 刷盘策略。
  3. sendfile 有局限性

    • 不能修改数据(如加解密、压缩);
    • 若需处理数据,仍需传统 I/O。

总结

  • PageCache:操作系统级缓存,是高性能 I/O 的基石;
  • mmap:让用户态高效写入文件,减少系统调用;
  • sendfile:实现真正的零拷贝网络传输,释放 CPU;

💡 三者结合 = 最大化利用硬件(DMA)、最小化 CPU 干预 = 极致 I/O 性能

概括

"mmap 是写入数据,sendfile 是发送数据"

mmap 主要优化写入路径 (文件生成),sendfile 优化读取+网络发送路径(文件分发)。
"都使用了 PageCache"

PageCache 是 Linux I/O 性能的"中枢",无论是 read/writemmap 还是 sendfile,只要操作普通文件,默认都会经过 PageCache(除非用 O_DIRECT 绕过)。
"mmap 将数据写入 PageCache 后异步刷盘,不等待,体现响应优势"

mmap 的写入本质上是写内存(虚拟地址)→ 触发缺页 → 写 PageCache ,应用线程立即返回 ,刷盘由内核后台完成。这显著降低了写入延迟,特别适合高频小写场景(如日志、消息)。
"sendfile 直接跳过 UserProcess,连接 PageCache 到网络端口,避免拷贝"

传统 read + write 需要把数据从内核拷到用户态再拷回内核,而 sendfile 全程在内核态完成,CPU 不搬运数据,仅靠 DMA 控制器传输,真正实现"零 CPU 拷贝"。

注意事项

  • mmap 本身不保证数据持久化 (断电可能丢),需配合 msync() 或依赖 OS 刷盘策略;
  • sendfile 要求源是文件、目标是 socket ,且不能修改数据内容(如加密、压缩),否则仍需传统 I/O。

传统 read/write vs mmap vs sendfile 对比表

维度 传统 read() + write() mmap()(用于写入) sendfile()(用于读取+发送)
典型用途 通用文件读写 高频写入/大文件映射 文件 → Socket 零拷贝传输
是否经过 PageCache ✅ 是 ✅ 是 ✅ 是
CPU 数据拷贝次数 2 次 (PageCache ↔ 用户 buffer) 0 次 (直接写 PageCache) 0 次 (PageCache → NIC)
DMA 拷贝次数 2 次 1~2 次 2 次
上下文切换次数 4 次 (read 进/出 + write 进/出) 0~1 次 (仅首次缺页中断) 2 次 (sendfile 调用进/出)
是否零拷贝 ❌ 否 ❌(但高效) ✅ 是
写入延迟 较高(需等 write 返回) 极低(写内存即返回) 不适用(只读)
读取+网络吞吐 中等 不适用(主要用于写) 极高(CPU 开销最小)
能否修改数据 ✅ 可在用户态处理 ✅ 可直接修改内存 ❌ 无法干预数据内容
适用场景 通用、需处理数据 日志写入、消息存储、大文件编辑 静态文件服务、消息拉取、视频流

关键结论

  • mmap 的优势在"写" :让写入像操作内存一样快,降低延迟
  • sendfile 的优势在"读+发" :让传输像 DMA 直连一样高效,提升吞吐、降低 CPU
  • 两者都依赖 PageCache:热数据在内存中时性能最佳;
  • 传统 read/write 最灵活但开销最大 :适合需要在用户态处理数据的场景(如解密、格式转换)。

高性能系统设计原则

  • 能用 sendfile 就不用 read/write(如静态资源、原始消息转发);
  • 高频写入优先考虑 mmap(如日志、CommitLog);
  • 必须处理数据时,才退回到传统 I/O
相关推荐
步步为营DotNet1 天前
深度解析.NET中MemoryCache:高效缓存策略与性能优化的关键
缓存·性能优化·.net
wangbing11251 天前
redis的存储问题
数据库·redis·缓存
Benny的老巢1 天前
Chrome 缓存迁移教程:5种方法解决 C 盘空间不足问题(2026年最新)
c语言·chrome·缓存
oMcLin1 天前
如何在 RHEL 8 上搭建高效的 Redis 集群,支持跨地域的数据同步与低延迟缓存访问?
数据库·redis·缓存
报错小能手1 天前
线程池学习(四)实现缓存线程池(Cached ThreadPool)
java·学习·缓存
山沐与山1 天前
【Redis】双重判定锁详解:缓存击穿的终极解决方案
java·redis·缓存
咖丨喱1 天前
【解析并缓存 P2P_ATTR_DEVICE_INFO】
缓存·asp.net·p2p
C_心欲无痕2 天前
浏览器缓存: IndexDB
前端·数据库·缓存·oracle
虫小宝2 天前
优惠券省钱app高并发秒杀系统:基于Redis与消息队列的架构设计
数据库·redis·缓存