← 返回题目列表

什么是零拷贝(Zero-Copy)?sendfile、mmap 如何减少数据拷贝?

高频 中等 第 5 / 27 题 更新于 2026/07/28
零拷贝sendfilemmapDMA

简化版

零拷贝(Zero-Copy)指在把文件数据从磁盘发到网卡的过程中,尽量减少 CPU 参与的数据拷贝次数和用户态/内核态切换次数,从而提升 IO 吞吐、降低 CPU 占用。传统「读文件再发网络」要经历 4 次数据拷贝 + 4 次上下文切换:磁盘→内核缓冲区(DMA)→用户缓冲区(CPU)→内核 socket 缓冲区(CPU)→网卡(DMA),其中两次 CPU 拷贝纯属多余——数据只是过一下用户态又原样送回内核。mmap 让用户态和内核共享同一块内存,省掉一次 CPU 拷贝;sendfile 让数据在内核内部从文件直接送到 socket,用户态完全不碰数据,配合网卡的 DMA gather(SG-DMA)能做到零次 CPU 拷贝。Kafka、Nginx 发送静态文件都靠它。

详细版

一、传统「读文件→发网络」有多浪费

代码 read(file) → write(socket) 背后发生了:

① 磁盘 ──DMA拷贝──► 内核 页缓存
② 内核页缓存 ──CPU拷贝──► 用户缓冲区        (read 返回,切回用户态)
③ 用户缓冲区 ──CPU拷贝──► 内核 socket 缓冲区  (write 进入内核)
④ socket缓冲区 ──DMA拷贝──► 网卡
  • 4 次数据拷贝:2 次 DMA(硬件干,不占 CPU)+ 2 次 CPU 拷贝(②③,占 CPU)。
  • 4 次上下文切换:read 的进/出内核、write 的进/出内核,各 2 次。

关键浪费:数据从内核页缓存拷到用户缓冲区,用户程序根本没动它,又原样拷回内核 socket 缓冲区——②③这两次 CPU 拷贝完全是白干

二、mmap + write:省掉一次 CPU 拷贝

mmap 把内核页缓存直接映射到用户地址空间,用户态和内核共享同一块物理内存,不用再拷到独立的用户缓冲区:

① 磁盘 ──DMA──► 内核页缓存(用户态通过 mmap 直接可见,无拷贝)
② 内核页缓存 ──CPU拷贝──► socket 缓冲区
③ socket缓冲区 ──DMA──► 网卡
  • 拷贝降为 3 次(2 DMA + 1 CPU),上下文切换仍 4 次(mmap + write 各 2 次)。

三、sendfile:用户态完全不碰数据

sendfile 一个系统调用直接让数据在内核内部从文件送到 socket,数据全程不进用户态

① 磁盘 ──DMA──► 内核页缓存
② 内核页缓存 ──CPU拷贝──► socket 缓冲区
③ socket缓冲区 ──DMA──► 网卡
  • 拷贝 3 次,上下文切换降为 2 次(只有 sendfile 一次进出内核)。

四、sendfile + SG-DMA:真·零拷贝

如果网卡支持 SG-DMA(Scatter-Gather DMA,分散-聚集),可以让网卡直接从内核页缓存读数据,连②那次 CPU 拷贝都省了:

① 磁盘 ──DMA──► 内核页缓存
② 网卡 ──SG-DMA 直接从页缓存取──► 网卡
  • CPU 拷贝 0 次(只剩 2 次 DMA),上下文切换 2 次——这才是名副其实的「零拷贝」(零 CPU 拷贝)。

完整版教学

一、先搞清楚:拷贝和切换为什么是开销

要理解零拷贝,先明确它优化的两样东西:

  • 数据拷贝:把一块数据从内存 A 复制到内存 B。CPU 拷贝要 CPU 一个字节一个字节搬,既占 CPU 又占内存带宽;DMA 拷贝由专门的 DMA 控制器(硬件)完成,不占 CPU。零拷贝的目标是消灭 CPU 拷贝(DMA 拷贝是必要的、消不掉)。
  • 上下文切换:每次系统调用(read/write)都要从用户态陷入内核态、返回时再切回,保存/恢复寄存器、切换栈都有成本。调用越多,切换越多。

所以「零拷贝」严格说是**「零次 CPU 拷贝」+ 尽量少的上下文切换**,DMA 拷贝仍然存在。

二、传统方式的四次拷贝,逐步拆解

以「把一个文件通过网络发出去」为例,最朴素的写法:

read(file_fd, buf, len);     // 文件 → 用户缓冲区
write(socket_fd, buf, len);  // 用户缓冲区 → socket
步骤拷贝类型谁干
1磁盘 → 内核页缓存DMA硬件
2内核页缓存 → 用户缓冲区CPUCPU
3用户缓冲区 → socket 缓冲区CPUCPU
4socket 缓冲区 → 网卡DMA硬件

外加 4 次上下文切换(read 进内核、read 返回用户态、write 进内核、write 返回用户态)。

核心洞察:如果我只是「把文件发出去」,中间根本不需要在用户态加工数据,那么第 2、3 步的 CPU 拷贝就是纯浪费——数据在用户态转了一圈原样送回内核。零拷贝技术就是想办法干掉这两次。

三、mmap:让用户态和内核共享内存

mmap 把文件对应的内核页缓存直接映射进用户进程的虚拟地址空间。于是用户程序能「看到」内核页缓存,不需要把数据单独拷贝一份到用户缓冲区。

buf = mmap(file_fd, len);    // 文件页缓存映射到用户空间,不拷贝
write(socket_fd, buf, len);  // 内核页缓存 → socket 缓冲区(1 次 CPU 拷贝)
  • 少了「内核页缓存 → 用户缓冲区」这次 CPU 拷贝,从 4 次降到 3 次拷贝
  • 但上下文切换仍是 4 次(mmap、write 各进出一次)。
  • 适用:需要在用户态读到/小改数据(因为数据可见)但又想省一次拷贝的场景。

四、sendfile:数据不出内核

sendfile 更彻底:一个系统调用就完成「从文件读、往 socket 写」,数据自始至终留在内核,不进用户态

sendfile(socket_fd, file_fd, offset, len);  // 内核内部:文件 → socket
  • 数据流:磁盘 →(DMA)→ 内核页缓存 →(CPU 拷贝)→ socket 缓冲区 →(DMA)→ 网卡。
  • 拷贝 3 次,上下文切换降到 2 次(就一个 sendfile 系统调用)。
  • 代价:用户态看不到数据,所以只适合「原样转发」(发静态文件、代理转发),不能在中途加工。

五、SG-DMA:连最后一次 CPU 拷贝也省掉

sendfile 还剩一次 CPU 拷贝(页缓存 → socket 缓冲区)。如果网卡支持 SG-DMA(Scatter-Gather DMA),内核只需把数据的位置和长度描述符传给 socket 缓冲区,网卡的 DMA 引擎就能直接从内核页缓存里「聚集」数据发送,不用先拷到 socket 缓冲区。

磁盘 →(DMA)→ 内核页缓存 →(SG-DMA 网卡直接读)→ 网卡
  • CPU 拷贝 = 0,只剩 2 次 DMA + 2 次上下文切换。这是真正的零拷贝。

六、演进对比总表

方案CPU 拷贝总拷贝上下文切换用户态能否读数据
read + write244
mmap + write134
sendfile132
sendfile + SG-DMA022

七、谁在用零拷贝

  • Kafka:消费者拉消息时,Broker 用 sendfile 把日志文件直接怼到网络 socket,是它高吞吐的关键之一。
  • Nginx / Tomcat:发送静态文件时开启 sendfile on,避免把文件搬进用户态再发出。
  • NettyFileRegion 底层就是 sendfile;它还有另一种「零拷贝」概念——CompositeByteBuf 用逻辑组合避免 ByteBuf 之间的内存拷贝(属于用户态层面的零拷贝,和内核 sendfile 是两码事,别混)。

Java 里 FileChannel.transferTo() 底层就是 sendfileMappedByteBuffer 对应 mmap

八、常见误区与追问

考点正确口径
传统路径内核页缓存到用户态,再回 socket 缓冲
零拷贝减少用户态参与和 CPU 内存拷贝
场景文件下载、静态资源、Kafka 日志发送
read + write:
disk -> page cache -> user buffer -> socket buffer -> NIC

sendfile:
disk -> page cache -> socket/NIC
less CPU copy and fewer context switches

性能题里说零拷贝,要讲清“省哪几次拷贝”和“什么时候不能省”。

  • 误区:零拷贝完全没有数据移动。 DMA、页缓存和网卡发送仍会移动数据,减少的是 CPU 参与的用户态拷贝。
  • 误区:只要用了 mmap 就一定更快。 mmap 也有缺页、页表和访问模式成本,场景不合适可能收益有限。
  • 误区:动态压缩响应也能直接 sendfile。 需要修改内容时通常要进入用户态处理,无法简单走文件到 socket 路径。
  • 追问:sendfile 适合什么? 适合文件内容不经应用修改直接发送到 socket 的场景。
  • 追问:Kafka 为什么能利用零拷贝? 消息以日志文件形式顺序存储,消费时可借 page cache 和 sendfile 发送。
  • 追问:TLS 会带来什么影响? 用户态 TLS 需要加密数据,可能打断零拷贝;内核 TLS 可优化部分路径。

九、加强记忆

零拷贝优化的是**「读文件→发网络」中多余的 CPU 拷贝和上下文切换**。传统 read+write 要 4 次拷贝(2 次 CPU 白搬)+ 4 次切换mmap 让用户态和内核共享页缓存,省一次 CPU 拷贝(降到 3 次拷贝);sendfile 让数据全程留在内核、用户态不碰,切换降到 2 次;sendfile + 网卡 SG-DMA 让网卡直接从页缓存取数,CPU 拷贝归零。记住:零拷贝 = 零 CPU 拷贝,DMA 仍在;Kafka、Nginx 发文件都靠 sendfile;缺点是用户态摸不到数据、不能中途加工