零拷贝的原理、演变与主流实战

49 阅读2分钟

前言

“零拷贝”(Zero Copy)这个概念,它不是指完全没有数据拷贝,而是指减少或消除数据在 CPU 执行期间的内存拷贝,从而降低 CPU 的负载并减少上下文切换。

传统的 I/O 痛点

假设我们需要开发一个文件服务器,将磁盘上的文件读取出来,通过网络发送给客户端。 传统的代码逻辑通常是这样的(伪代码):

// 1. 读取文件到 buffer
read(file_fd, tmp_buf, len);
// 2. 将 buffer 发送给 socket
write(socket_fd, tmp_buf, len);

这个过程涉及 4 次上下文切换4 次数据拷贝

  1. DMA 拷贝:磁盘 -> 内核缓冲区(Read Buffer)。
  2. CPU 拷贝:内核缓冲区 -> 用户缓冲区(User Buffer)。 (系统调用 read 返回)
  3. CPU 拷贝:用户缓冲区 -> Socket 缓冲区(Kernel Space)。 (调用 write)
  4. DMA 拷贝:Socket 缓冲区 -> 网卡(NIC)。

传统模式下,数据在内核态和用户态之间来回搬运,CPU 必须亲自参与两次拷贝,极其浪费资源。

演进:从 2 拷贝到 0 拷贝

这里的“2拷贝”指的是 CPU 拷贝 的次数,因为 DMA 拷贝(硬件负责,不消耗 CPU)是无法避免的。

为了优化上述流程,操作系统引入了多种 API。

mmap + write (减少一次拷贝)

mmap(内存映射)利用虚拟内存技术,将内核缓冲区与用户缓冲区映射到同一块物理内存。

API: mmap() + write()

  • 原理:

    1. 应用调用 mmap,DMA 将数据从磁盘加载到内核缓冲区。
    2. 应用调用 write,CPU 将数据从内核缓冲区(因为映射关系,用户态也能访问)直接拷贝到 Socket 缓冲区。
    3. DMA 将数据从 Socket 缓冲区拷贝到网卡。

sendfile (减少一次拷贝)

Linux 引入了 sendfile 系统调用,专门用于在两个文件描述符之间传输数据。

  • API: ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
  • 原理:
    1. 用户调用 sendfile。DMA 将数据从磁盘拷贝到内核缓冲区。
    2. CPU 将数据从内核缓冲区拷贝到 Socket 缓冲区
    3. DMA 将数据从 Socket 缓冲区拷贝到网卡。

sendfile + DMA Scatter/Gather (真正的 Zero Copy)

这是目前公认的“标准零拷贝”。前提是网卡硬件支持 Scatter-Gather (分散/收集) 功能。

  • API: 依然是 sendfile
  • 原理:
    1. DMA 将数据从磁盘拷贝到内核缓冲区。
    2. CPU 不再拷贝数据,而是将内核缓冲区的内存地址描述符(Address & Length)写入 Socket 缓冲区。
    3. 网卡的 DMA 引擎根据描述符,直接从内核缓冲区读取数据发送。