零拷贝
摘要:什么是零拷贝?
零拷贝(Zero-Copy) 并不是指完全没有数据拷贝,而是指尽可能地减少或避免在应用程序的用户空间(User Space)和操作系统的内核空间(Kernel Space)之间进行不必要的CPU数据拷贝。其核心目标是让数据在从一个I/O设备(如硬盘)到另一个I/O设备(如网卡)的传输过程中,最大限度地利用硬件(如DMA)来搬运数据,从而解放CPU,减少上下文切换,显著提升数据传输的性能和效率。
一、 理解背景:传统I/O的痛点
要理解零拷贝的价值,我们必须先了解传统的数据传输方式有多么“昂贵”。
假设我们要实现一个简单的文件服务器,其功能是:从磁盘读取一个文件,然后通过网络发送给客户端。
传统I/O的操作流程:
我们用代码来看,通常是这样的:
1 | read(file_fd, buffer, len); |
这个看似简单的两行代码,在操作系统层面会触发一系列复杂的步骤:
第一次拷贝(DMA Copy):
- 应用程序调用
read()系统调用,发起读文件请求。 - CPU 发出指令,上下文从用户态切换到内核态。
- DMA(Direct Memory Access)控制器介入,将磁盘上的文件数据直接拷贝到内核空间的一个缓冲区,我们称之为内核缓冲区(Page Cache)。这个过程由DMA完成,不占用CPU。
- 应用程序调用
第二次拷贝(CPU Copy):
read()系统调用需要返回数据给应用程序。- CPU 将数据从内核缓冲区拷贝到应用程序指定的用户缓冲区(
buffer)。 read()调用返回,上下文从内核态切换回用户态。此时,数据已经到了我们的应用程序内存中。
第三次拷贝(CPU Copy):
- 应用程序调用
write()系统调用,发起网络发送请求。 - 上下文再次从用户态切换到内核态。
- CPU 将数据从用户缓冲区(
buffer)拷贝到与网络套接字(Socket)关联的另一个内核缓冲区,即 Socket 缓冲区。
- 应用程序调用
第四次拷贝(DMA Copy):
- 数据已经准备好发送。
- DMA 控制器将数据从 Socket 缓冲区拷贝到网卡的缓冲区(NIC Buffer)。
- 网卡将数据打包成网络帧,通过物理链路发送出去。
write()调用返回,上下文再次从内核态切换回用户态。

传统I/O的成本总结:
- 4 次数据拷贝:2次DMA拷贝 + 2次CPU拷贝。
- 4 次上下文切换:2次
read()切换 + 2次write()切换。
问题在哪里?
- CPU拷贝是多余的:在整个流程中,应用程序(用户空间)只是一个“中转站”。数据从内核缓冲区拷贝到用户缓冲区,又原封不动地拷贝回内核的Socket缓冲区。这个来回拷贝对于“数据传输”这个场景来说,是完全没有必要的,白白浪费了CPU周期和内存带宽。
- 上下文切换是昂贵的:每次用户态和内核态之间的切换,都需要保存和恢复大量的寄存器状态、程序计数器等信息,开销很大。
零拷贝技术就是为了解决这两个核心问题而生的。
二、 零拷贝的实现方式
零拷贝有多种实现技术,下面我们由浅入深地介绍几种主流的方式。
1. mmap + write 方式
mmap(Memory Map)是一种内存映射技术,它可以将内核缓冲区的一部分直接映射到应用程序的地址空间。这样,应用程序就可以像访问普通内存一样访问这块内核缓冲区,而无需将数据从内核拷贝到用户空间。
操作流程:
- 应用程序调用
mmap()系统调用。- 上下文切换到内核态。
- DMA 控制器将磁盘数据拷贝到内核缓冲区(Page Cache)。
mmap()系统调用将这块内核缓冲区映射到用户空间的虚拟地址,此时内核空间和用户空间共享了这块内存。mmap()返回,上下文切换回用户态。
- 应用程序调用
write()系统调用。- 上下文再次切换到内核态。
- CPU 将数据从内核缓冲区(现在也被用户空间共享)直接拷贝到 Socket 缓冲区。
- DMA 控制器将数据从 Socket 缓冲区拷贝到网卡。
write()返回,上下文切换回用户态。

mmap + write 的改进:
- 数据拷贝减少为 3 次:1次DMA拷贝 + 1次CPU拷贝 + 1次DMA拷贝。
- 我们成功地消除了一次从内核到用户的CPU拷贝。
- 上下文切换仍为 4 次(
mmap两次,write两次)。
优点:减少了一次CPU拷贝。
缺点:mmap 映射的内存如果被其他进程意外修改,可能会导致数据污染。此外,对于小文件,mmap 的开销(建立映射、管理页表)可能比传统的 read 更大。
2. sendfile 方式 (Linux)
sendfile 是 Linux 2.1 内核引入的一个专门用于在两个文件描述符之间传输数据的系统调用,它极大地简化了数据传输过程,是零拷贝的经典实现。
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
out_fd: 目标文件描述符(如 socket)。in_fd: 源文件描述符(如文件)。
操作流程(基础版):
- 应用程序调用
sendfile()系统调用。- 上下文切换到内核态。
- DMA 控制器将磁盘数据拷贝到内核缓冲区(Page Cache)。
- CPU 将数据从内核缓冲区直接拷贝到 Socket 缓冲区。
- DMA 控制器将数据从 Socket 缓冲区拷贝到网卡。
sendfile()返回,上下文切换回用户态。

sendfile 的改进:
- 数据拷贝减少为 3 次:1次DMA拷贝 + 1次CPU拷贝 + 1次DMA拷贝。
- 上下文切换减少为 2 次:一次系统调用就完成了所有操作。
相比 mmap,sendfile 进一步减少了上下文切换次数,性能更好。
3. sendfile + DMA Scatter/Gather (真正的零拷贝)
从 Linux 2.4 内核开始,sendfile 得到了进一步增强,如果网卡硬件支持 Scatter-Gather I/O(分散-聚集 I/O)功能,那么连内核内部的CPU拷贝都可以省掉。
Scatter-Gather I/O 是什么?
它是一种 DMA 技术,允许 DMA 控制器从多个不连续的内存缓冲区中读取数据(Scatter),然后将它们组装成一个连续的数据流写入到目标设备(Gather)。
操作流程(增强版):
- 应用程序调用
sendfile()系统调用。- 上下文切换到内核态。
- DMA 控制器将磁盘数据拷贝到内核缓冲区(Page Cache)。
- 关键步骤:CPU 不再拷贝数据到 Socket 缓冲区。取而代之的是,CPU 将一个描述符(Descriptor) 追加到 Socket 缓冲区。这个描述符包含了两个信息:
- 数据在内核缓冲区中的内存地址。
- 数据的长度。
- DMA 控制器根据 Socket 缓冲区中的描述符,直接从内核缓冲区将数据拷贝到网卡。这个过程被称为 DMA Gather Copy。
sendfile()返回,上下文切换回用户态。

终极改进:
- 数据拷贝减少为 2 次:全部是 DMA 拷贝,CPU 拷贝次数为 0!这才是“零拷贝”这个名字最贴切的诠释。
- 上下文切换为 2 次。
这是目前在Linux上实现文件到网络传输最高效的方式。
三、 零拷贝的应用场景
零拷贝技术被广泛应用于需要高性能数据传输的场景,特别是那些数据内容本身不需要被应用程序处理的场景。
- Web 服务器:像 Nginx、Apache 等,在处理静态文件请求(如图片、HTML、CSS文件)时,会大量使用
sendfile来提高性能。 - 消息中间件:像 Kafka、RocketMQ 等,它们在从磁盘读取消息数据并发送给消费者时,也严重依赖零拷贝技术来达到极高的吞吐量。Kafka 的高性能传说很大程度上就归功于对
sendfile的极致运用。 - 文件服务器:如 FTP、Samba 服务器。
- 数据库:某些数据库在进行数据备份或网络同步时也会使用。
四、 总结:零拷贝的优势与局限
优势:
- 减少CPU开销:避免了用户空间和内核空间之间多余的CPU数据拷贝。
- 减少上下文切换:使用
sendfile等系统调用可以将多次I/O操作合并为一次,减少了内核态和用户态之间的切换次数。 - 提升性能:CPU被解放出来去处理其他任务,系统的整体吞吐量得到显著提升。
- 避免内存带宽瓶颈:减少了对内存总线的占用。
局限性:
- 数据无法修改:因为数据没有被拷贝到用户空间,所以应用程序在数据传输过程中无法对其进行修改(如加密、压缩)。如果需要修改数据,就必须走传统I/O的路径。
- 依赖操作系统和硬件支持:最高效的零拷贝(如
sendfile+ DMA Gather)需要操作系统内核和网卡硬件的同时支持。 - 适用场景有限:主要适用于数据“原封不动”的转发场景。
零拷贝方案对比
| 技术方案 | CPU 拷贝次数 | DMA 拷贝次数 | 上下文切换次数 | 核心思想 |
|---|---|---|---|---|
| 传统 I/O | 2 | 2 | 4 | 简单直接,但效率低下 |
| mmap + write | 1 | 2 | 4 | 共享内核与用户空间内存,减少一次CPU拷贝 |
| sendfile | 1 | 2 | 2 | 合并系统调用,减少上下文切换 |
| sendfile + SG-DMA | 0 | 2 | 2 | 利用硬件特性,彻底消除CPU拷贝 |
参考
[1] 什么是零拷贝?










