ZaynPei Lv6

零拷贝 (Zero-Copy) 是一种I/O操作优化技术。它的核心思想是:在数据从一个I/O设备(如磁盘)传输到另一个I/O设备(如网卡)的过程中,尽可能地减少甚至完全消除 CPU 对数据内容的复制操作。这里的“拷贝”指的主要是“CPU 拷贝”,而不是 DMA(直接内存访问)拷贝。

假设我们要实现一个最常见的场景:“从磁盘读取一个文件,然后通过网络发送出去”(例如,一个 Web 服务器返回一个 index.html 页面)。在传统的 read() 和 send() 模式下,数据流转如下:

第1次切换 (用户 -> 内核): 应用程序调用 read(),发起系统调用。

第1次拷贝 (DMA Copy): 硬件(DMA 控制器)将数据从磁盘读取到内核缓冲区(Page Cache)。(CPU 不参与)

第2次拷贝 (CPU Copy): CPU 将数据从内核缓冲区复制到应用程序的缓冲区(例如,你 C++ 程序中的 char buffer[1024])。

第2次切换 (内核 -> 用户): read() 调用返回。应用程序(用户空间)现在拿到了数据。

第3次切换 (用户 -> 内核): 应用程序调用 send(),再次发起系统调用,希望把数据发送出去。

第3次拷贝 (CPU Copy): CPU 将数据从应用程序的缓冲区(刚才那个 buffer)复制到内核的 Socket 缓冲区。

第4次拷贝 (DMA Copy): 硬件(DMA 控制器)将数据从Socket 缓冲区复制到网卡,然后发送出去。(CPU 不参与)

第4次切换 (内核 -> 用户): send() 调用返回。

痛点总结:

4 次上下文切换 (Context Switches): 线程在用户空间和内核空间之间来回切换,开销巨大。

4 次数据拷贝: 两次 DMA 拷贝(硬件负责,可接受)和两次 CPU 拷贝(性能杀手)。

请注意,数据从头到尾内容没有发生任何变化,但 CPU 却忙于在内存中“搬运”它两次,这完全是浪费。

零拷贝技术就是为了消除上述第2次和第3次(由 CPU 执行的)拷贝。最著名的实现是 Linux 上的 sendfile 系统调用。

当你的程序(如 Nginx)调用 sendfile(socket_fd, file_fd, …) 时,数据流转变为:

第1次切换 (用户 -> 内核): 应用程序调用 sendfile()。

第1次拷贝 (DMA Copy): DMA 将数据从磁盘读取到内核缓冲区 (Page Cache)。

第2次拷贝 (CPU Copy): CPU 将数据从内核缓冲区直接复制到Socket 缓冲区。(注意:数据没有被复制到用户空间!)

第3次拷贝 (DMA Copy): DMA 将数据从Socket 缓冲区复制到网卡。

第2次切换 (内核 -> 用户): sendfile() 返回。

效果:

2 次上下文切换 (减少了 50%)。

3 次数据拷贝 (1 次 CPU Copy, 2 次 DMA Copy)。

这已经消除了“数据进出用户空间”的开销,性能大幅提升。但这还不是“零”拷贝,CPU 还是拷贝了 1 次。

还有一种常见的技术是 mmap(内存映射文件): 应用程序调用 mmap(),它在用户空间和内核空间之间共享一块内存区域。这块区域直接映射到内核的 Page Cache。

当应用程序(用户空间)访问这块内存时,它实际上是在直接访问内核缓冲区。

mmap 帮你省去了 read() 调用(以及第2次 CPU 拷贝)。

但是,当你调用 send() 时,你仍然需要将数据从 Page Cache(现在也映射到了用户空间)复制到 Socket 缓冲区(第3次 CPU 拷贝)。

所以,mmap + send 组合,实现了 3 次拷贝和 4 次切换。它只比传统方式少了一次 CPU 拷贝。

On this page