页缓存技术(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
相关推荐
梅孔立9 小时前
两种免费的翻译API调用方式详解 - Edge官方API与个人缓存服务
前端·缓存·edge
gwf2169 小时前
SSD读写速度深度解析:顺序读写vs随机读写、IOPS、延迟,你的硬盘性能到底怎么看?
git·嵌入式硬件·缓存·github·智能硬件
番茄炒鸡蛋加糖12 小时前
缓存三大问题
缓存
江晓鱼未暖16 小时前
十七、Redis 核心原理与架构详解
大数据·数据库·数据仓库·redis·缓存·架构
小罗水17 小时前
第13章 Redis 缓存、幂等锁与任务状态
数据库·redis·缓存
热心市民lcj17 小时前
Spring Boot 整合 Caffeine 本地缓存实战
spring boot·后端·缓存
zx11545018 小时前
【热点 Key 探测与加长缓存 TTL 实战】
缓存
toooooop81 天前
如何用 ss + ps 精准定位本机 Redis 的“隐形”消费者?
linux·数据库·redis·缓存
软萌萌的11 天前
C#中的多级缓存架构设计与实现深度解析
缓存·c#·wpf
醉城夜风~2 天前
HTML表单域学习博客:表单标签、输入框、按钮详解
android·java·缓存