前言
“零拷贝”(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 次数据拷贝。
- DMA 拷贝:磁盘 -> 内核缓冲区(Read Buffer)。
- CPU 拷贝:内核缓冲区 -> 用户缓冲区(User Buffer)。 (系统调用 read 返回)
- CPU 拷贝:用户缓冲区 -> Socket 缓冲区(Kernel Space)。 (调用 write)
- DMA 拷贝:Socket 缓冲区 -> 网卡(NIC)。
传统模式下,数据在内核态和用户态之间来回搬运,CPU 必须亲自参与两次拷贝,极其浪费资源。
演进:从 2 拷贝到 0 拷贝
这里的“2拷贝”指的是 CPU 拷贝 的次数,因为 DMA 拷贝(硬件负责,不消耗 CPU)是无法避免的。
为了优化上述流程,操作系统引入了多种 API。
mmap + write (减少一次拷贝)
mmap(内存映射)利用虚拟内存技术,将内核缓冲区与用户缓冲区映射到同一块物理内存。
API: mmap() + write()
-
原理:
- 应用调用
mmap,DMA 将数据从磁盘加载到内核缓冲区。 - 应用调用
write,CPU 将数据从内核缓冲区(因为映射关系,用户态也能访问)直接拷贝到 Socket 缓冲区。 - DMA 将数据从 Socket 缓冲区拷贝到网卡。
- 应用调用
sendfile (减少一次拷贝)
Linux 引入了 sendfile 系统调用,专门用于在两个文件描述符之间传输数据。
- API:
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count); - 原理:
- 用户调用
sendfile。DMA 将数据从磁盘拷贝到内核缓冲区。 - CPU 将数据从内核缓冲区拷贝到 Socket 缓冲区。
- DMA 将数据从 Socket 缓冲区拷贝到网卡。
- 用户调用
sendfile + DMA Scatter/Gather (真正的 Zero Copy)
这是目前公认的“标准零拷贝”。前提是网卡硬件支持 Scatter-Gather (分散/收集) 功能。
- API: 依然是
sendfile。 - 原理:
- DMA 将数据从磁盘拷贝到内核缓冲区。
- CPU 不再拷贝数据,而是将内核缓冲区的内存地址描述符(Address & Length)写入 Socket 缓冲区。
- 网卡的 DMA 引擎根据描述符,直接从内核缓冲区读取数据发送。