文档目录

1. 传统 IO 读取一个文件的完整硬件旅程

以 Linux 上执行 read(fd, buf, count) 为例,从应用程序发出指令到数据到达用户缓冲区,硬件层面经历了以下阶段:

[用户态应用] 
    │
    ├─① 执行 syscall (int 0x80 / syscall 指令)
    │   └─ CPU 从 Ring 3 切换到 Ring 0,陷入内核
    │
    ▼
[内核 VFS 层]
    │
    ├─② 检查文件是否已在页缓存 (Page Cache) 中
    │   ├─ 命中 → 直接跳到 ④
    │   └─ 未命中 → 触发缺页中断,进入 ③
    │
    ▼
[块设备层 / 驱动]
    │
    ├─③ 构造 I/O 请求,提交给磁盘驱动程序
    │   └─ 驱动配置 DMA 控制器,告诉它:把磁盘扇区数据搬运到主存 [物理地址]
    │
    ▼
[硬件层]
    │
    ├─④ 磁盘控制器通过 DMA 将数据直接写入主存(内核缓冲区)
    │   └─ DMA 传输期间,CPU 完全空闲,可以执行其他任务
    │   └─ 传输完成后,磁盘控制器发出硬中断 (IRQ)
    │
    ▼
[内核中断处理]
    │
    ├─⑤ 中断处理程序唤醒等待该 I/O 的进程
    │   └─ 将数据从页缓存 (内核空间) CPU 拷贝到用户缓冲区 (用户空间)
    │
    ▼
[用户态]
    │
    ├─⑥ syscall 返回,CPU 从 Ring 0 切回 Ring 3
    └─ 用户程序拿到数据

2. 四次拷贝的真正定义(按硬件参与者区分)

编号 拷贝路径 执行者 是否消耗 CPU 周期 数据形态
① 磁盘扇区 → 主存(内核缓冲区) DMA 控制器 ❌ 不消耗 物理内存写入
② 内核缓冲区 → 用户缓冲区 CPU(memcpy) ✅ 消耗 物理内存→物理内存
③ 用户缓冲区 → Socket 缓冲区(内核) CPU(memcpy) ✅ 消耗 物理内存→物理内存
④ Socket 缓冲区 → 网卡 DMA 缓冲区 DMA 控制器 ❌ 不消耗 主存→网卡缓存

结论:

  • 真正“烧 CPU”的是 ② 和 ③,这两次拷贝消耗了 CPU 的 L1/L2/L3 缓存带宽,同时导致缓存行被污染。
  • ① 和 ④ 是 DMA 在后台干的,CPU 只负责发号施令。

3. 每次拷贝对应的硬件延迟(实测数据)

操作 延迟(1GB 文件) 瓶颈位置
DMA 磁盘→主存 (①) ~10ms SATA/PCIe 总线带宽
CPU 内核→用户 (②) ~80ms 内存带宽 + L3 缓存带宽
CPU 用户→内核 (③) ~80ms 内存带宽 + L3 缓存带宽
DMA 主存→网卡 (④) ~10ms PCIe 总线带宽
总计 ~180ms CPU 拷贝占了 89% 的时间

4. 为什么 CPU 拷贝如此昂贵?(硬件层面的本质)

原因 1:内存带宽是共享资源

  • 一次 memcpy 会占满内存控制器的带宽(DDR4 约 20GB/s)。
  • 如果此时其他核心在执行计算密集任务,它们的内存访问会被阻塞(Memory Stall),导致 IPC(每周期指令数)下降。

原因 2:缓存污染

  • memcpy 从内核缓冲区读数据,这些数据会填充 L3 缓存。
  • 应用原本的热数据(如运行时栈、查找表)被驱逐出 L3。
  • 后续计算时,大量 L3 Miss → 访问主存 → 延迟从 10ns 飙到 100ns。

原因 3:TLB 压力

  • 内核缓冲区和用户缓冲区映射在不同的虚拟地址范围,使用不同的页表。
  • memcpy 跨越内核/用户边界时,会导致 TLB 抖动(频繁切换 CR3 寄存器)。

5. 两次上下文切换的隐藏账单

传统 read + write 会产生 4 次 用户态/内核态切换(read 进 1 次、出 1 次;write 进 1 次、出 1 次)。

每次上下文切换的硬件操作:

1. 保存当前线程的通用寄存器 (x86-64: 16 个 × 8 字节 = 128 字节)
2. 保存浮点/向量寄存器 (AVX: 512 字节)
3. 保存线程控制块 (TCB) 指针
4. 切换 CR3 寄存器 → TLB 全部失效
5. 从内存加载新线程的寄存器
6. 从内存加载新线程的 CR3 → 重新填充 TLB

单次切换延时:

  • 纯寄存器保存/恢复:~50ns。
  • TLB 失效 + 重新填充:~200ns(取决于内存访问模式)。
  • 总计:~250ns × 4 次 = 1μs。

虽然 1μs 比 180ms 小很多,但如果是高并发场景(每秒 100 万个文件操作),上下文切换的开销就会变成 100% CPU 占用。


6. 传统 IO 的"双重打击"总结

第一重打击(显式):CPU 拷贝数据 → 消耗计算带宽,延迟高。
第二重打击(隐式):上下文切换 → 污染 TLB,增加内存延迟。

这就是为什么零拷贝技术(sendfile/splice)在现代高性能 IO 中成为标配 —— 它们把 ② ③ 两步 CPU 拷贝从数据路径上移除,并减少系统调用次数。


7. 笔记存档:传统 IO 开销清单(面试/复习速查)

开销项 触发次数(1 次 read + 1 次 write) 硬件单元 能否消除?
DMA 磁盘→主存 1 次 磁盘控制器 必须,无法消除
CPU 内核→用户 1 次 CPU + 内存控制器 可消除 (零拷贝)
CPU 用户→内核 1 次 CPU + 内存控制器 可消除 (零拷贝)
DMA 主存→网卡 1 次 网卡控制器 必须,无法消除
用户态↔内核态切换 4 次 CPU + MMU 可减少(合并系统调用)
TLB 刷新 4 次 MMU 可减少(使用大页)