← 返回题目列表

零拷贝是什么?它是如何实现的?

高频 中等 第 5 / 27 题 更新于 2026/07/28
零拷贝sendfilemmap网络编程

简化版

零拷贝(Zero-Copy)是指在「读文件 → 发网络」这类数据传输中,减少 CPU 参与的数据拷贝次数和用户态/内核态的切换次数。传统方式要经过「磁盘→内核缓冲区→用户缓冲区→socket 缓冲区→网卡」多次拷贝,其中数据根本没被应用处理,却在内核态和用户态之间来回拷了好几遍,纯属浪费。零拷贝(sendfilemmap)让数据不经过用户空间、直接在内核里流转,省掉了这些无谓的 CPU 拷贝——Kafka、Netty、Nginx 都靠它实现高吞吐。

详细版

传统「读文件发网络」的开销read + write):

① DMA 拷贝:磁盘 → 内核读缓冲区(page cache)
② CPU 拷贝:内核读缓冲区 → 用户缓冲区      ← 应用其实没用这数据
③ CPU 拷贝:用户缓冲区 → socket 缓冲区      ← 又拷回内核
④ DMA 拷贝:socket 缓冲区 → 网卡

4 次拷贝(2 次 DMA + 2 次 CPU)+ 4 次上下文切换。问题在于:数据只是「从磁盘搬到网卡」,应用根本没处理它,但它却在内核态↔用户态之间白白拷了两趟(②③),浪费 CPU 和切换开销。

零拷贝的优化

  • mmap + write:用 mmap 把内核缓冲区映射到用户空间,应用「看得到」数据但不用真的拷贝到用户缓冲区——省掉第②次拷贝。变成 3 次拷贝。
  • sendfile:直接告诉内核「把这个文件发到这个 socket」,数据全程不经过用户空间,在内核里从读缓冲区直接送到 socket 缓冲区。变成 3 次拷贝(还剩 ①→socket→网卡 里的一次 CPU 拷贝)+ 2 次上下文切换。
  • sendfile + DMA gather(SG-DMA):更进一步,网卡的 DMA 直接从内核读缓冲区「聚集」数据发送,连内核缓冲区→socket 缓冲区的 CPU 拷贝也省了——只剩 2 次 DMA 拷贝、0 次 CPU 拷贝,这才是「真·零拷贝」(CPU 零参与拷贝)。

完整版教学

一、先看清「传统方式浪费在哪」

理解零拷贝,关键是先看清传统方式的浪费。一个常见需求:把服务器上的文件通过网络发给客户端(如下载、Kafka 发消息给消费者)。传统写法是 read 文件到内存、再 write 到 socket,背后发生 4 次拷贝:

  • ① 磁盘 → 内核 page cache(DMA,必要);
  • ② 内核 page cache → 用户缓冲区(CPU);
  • ③ 用户缓冲区 → socket 缓冲区(CPU);
  • ④ socket 缓冲区 → 网卡(DMA,必要)。

问题出在 ② 和 ③:数据从磁盘到网卡,应用根本没有对它做任何处理(不解析、不修改,只是转发),但它却被拷到用户空间又原样拷回内核——这两次 CPU 拷贝和随之的用户态/内核态切换,纯属浪费。零拷贝要消灭的就是这种「无谓的过路拷贝」。

二、为什么叫「零拷贝」——零的是 CPU 拷贝

一个常见误解:以为零拷贝是「一次拷贝都没有」。其实不是——①④ 那两次 DMA 拷贝是省不掉的(数据总得从磁盘进内存、从内存到网卡,这是硬件必需)。

零拷贝的「零」,指的是零次 CPU 参与的拷贝——把 ②③ 那两次由 CPU 执行的、在用户态和内核态之间的拷贝干掉。DMA(直接内存访问)拷贝由专门的硬件控制器完成、不占用 CPU,所以它们不算在「零拷贝」要消除的范围里。

所以准确说:零拷贝 = 消除 CPU 拷贝 + 减少用户态/内核态切换,让数据尽量只由 DMA 在内核里流转,CPU 不插手搬运。

三、方案一:mmap + write(省一次拷贝)

mmap 的思路是内存映射——把内核的读缓冲区(page cache)直接映射到用户进程的地址空间:

  • 应用调 mmap 后,能像访问自己内存一样访问内核缓冲区里的数据,但数据并没有真的被拷贝到用户空间(只是建立了映射);
  • 于是 read 那次「内核→用户」的 CPU 拷贝(②)就省掉了
  • 之后 write 时,直接从这个映射的内核缓冲区拷到 socket 缓冲区。

结果:从 4 次拷贝降到 3 次(省了 1 次 CPU 拷贝)。适合需要在用户态修改数据后再发送的场景(因为映射后应用能读写数据)。

四、方案二:sendfile(数据不进用户态)

如果应用只是转发文件、根本不需要看/改数据(如静态文件下载、Kafka 转发消息),那连「映射到用户空间」都多余。sendfile 一步到位:

sendfile(out_fd, in_fd, ...);  // 直接把 in_fd(文件)发到 out_fd(socket)

它告诉内核:「把这个文件的数据直接发到这个 socket」。数据全程在内核里流转,压根不经过用户空间——省去了「内核→用户→内核」的往返。上下文切换也从 4 次降到 2 次。

进一步,配合网卡的 SG-DMA(分散-聚集 DMA),网卡能直接从内核读缓冲区「聚集」数据发送,连「内核读缓冲区→socket 缓冲区」这次 CPU 拷贝也省了——最终只剩 2 次 DMA 拷贝、0 次 CPU 拷贝,达到真正的零拷贝。

五、零拷贝在哪些地方大放异彩

零拷贝是高吞吐系统的标配:

  • Kafka:消费者拉消息时,Kafka 用 sendfileFileChannel.transferTo)把日志文件数据直接发给消费者,不经过用户态——这是 Kafka 高吞吐的关键之一;
  • Netty:提供零拷贝能力(FileRegion 底层用 sendfile,还有 CompositeByteBuf 逻辑上的零拷贝——组合多个 buffer 而不拷贝数据);
  • Nginx:发送静态文件用 sendfile,大幅提升静态资源吞吐;
  • JavaFileChannel.transferTo()(底层 sendfile)、MappedByteBuffer(底层 mmap)。

注意:Netty 说的「零拷贝」有两层含义——一是操作系统层sendfile(避免内核用户态拷贝),二是应用层的(如 CompositeByteBuf 把多个小 buffer 逻辑组合成一个而不真拷贝、slice 共享底层数组),别混淆。

六、常见误区与追问

考点正确口径
传统拷贝磁盘到内核,再到用户态,再回内核 socket
零拷贝目标减少用户态/内核态拷贝和上下文切换
典型技术sendfile、mmap、splice、DMA
traditional:
disk -> kernel buffer -> user buffer -> socket buffer -> NIC

sendfile:
disk -> kernel page cache -> socket/NIC
user space not copying payload

零拷贝不是一次拷贝都没有,而是尽量避免数据在用户态和内核态之间来回搬运。

  • 误区:零拷贝等于没有任何拷贝。 数据仍可能经 DMA、页缓存或内核缓冲区移动,重点是减少 CPU 参与的内存拷贝。
  • 误区:所有业务都适合 sendfile。 sendfile 适合文件直接发送;如果要压缩、加密或修改内容,可能仍要进入用户态处理。
  • 误区:零拷贝只优化带宽。 它还减少 CPU 消耗、上下文切换和内存带宽压力。
  • 追问:mmap 和 sendfile 区别是什么? mmap 把文件映射到用户空间,sendfile 更偏文件到 socket 的内核内传输。
  • 追问:Kafka 为什么强调零拷贝? 日志文件顺序读并发送给消费者,非常适合 page cache 加 sendfile 降低复制成本。
  • 追问:TLS 会影响零拷贝吗? 传统用户态 TLS 需要加密处理,可能打断 sendfile 路径;内核 TLS 可改善部分场景。

七、加强记忆

零拷贝是减少「读文件发网络」中 CPU 参与的拷贝和用户态/内核态切换。传统 read+write 有 4 次拷贝(2 DMA + 2 CPU),其中「内核→用户→内核」两次 CPU 拷贝纯属浪费(数据没被应用处理)。mmap+write 映射内核缓冲区省 1 次 CPU 拷贝;sendfile 让数据全程不进用户态;配合 SG-DMA 可达 0 次 CPU 拷贝(真零拷贝)。「零」指零 CPU 拷贝(DMA 省不掉)。Kafka/Netty/Nginx 靠它高吞吐,Java 对应 transferTo/MappedByteBuffer。