详解 零拷贝(Zero Copy):mmap、sendfile、DMA gather、splice
目录
什么是零拷贝?
DMA传输原理
零拷贝原理
用户态直接IO
减少拷贝次数
mmap + write
sendfile
sendfile + DMA gather copy
splice
RocketMQ与Kafka
什么是零拷贝?
首先来看传统的IO工作方式:

过程:
- 用户进程向 CPU 发起 read 系统调用读取数据,由用户态切换为内核态,然后一直阻塞等待数据的返回。
- CPU 在接收到指令以后对磁盘发起 I/O 请求,将磁盘数据先放入磁盘控制器缓冲区。
- 数据准备完成以后,磁盘向 CPU 发起 I/O 中断。
- CPU 收到 I/O 中断以后将磁盘缓冲区中的数据拷贝到内核缓冲区,然后再从内核缓冲区拷贝到用户缓冲区。
- 用户进程由内核态切换回用户态,解除阻塞状态,然后等待 CPU 的下一个执行时间钟。
可以看到,这个过程要经历四次拷贝,和四次状态切换。CPU 需要参与多次内存拷贝和上下文切换,效率低下。
零拷贝(Zero-Copy) 是一种优化数据传输效率的技术,其核心思想是避免数据在内存的不同区域之间进行不必要的复制,从而减少 CPU 的开销、内存带宽占用和数据传输延迟。零拷贝通过跳过用户态内存的复制步骤,让数据直接在内核态和硬件之间传输,或通过 “内存映射”“直接缓冲区” 等方式减少复制次数。
DMA传输原理
DMA全称为直接内存访问(Direct Memory Access),能够让外围设备直接访问系统主内存的机制。整个数据传输在DMA控制器的控制下进行,CPU只在开始和结束时做中断处理,在传输过程中CPU可以继续进行其他工作,使CPU计算和IO操作并行执行。
零拷贝原理
在 Linux 中零拷贝技术主要有 3 个实现思路:
- 用户态直接 I/O:应用程序可以直接访问硬件存储,操作系统内核只是辅助数据传输。这种方式依旧存在用户空间和内核空间的上下文切换,硬件上的数据直接拷贝至了用户空间,不经过内核空间。因此,直接 I/O 不存在内核空间缓冲区和用户空间缓冲区之间的数据拷贝。
- 减少数据拷贝次数:在数据传输过程中,避免数据在用户空间缓冲区和系统内核空间缓冲区之间的 CPU 拷贝,以及数据在系统内核空间内的 CPU 拷贝,这也是当前主流零拷贝技术的实现思路。
- 写时复制技术:写时复制指的是当多个进程共享同一块数据时,如果其中一个进程需要对这份数据进行修改,那么将其拷贝到自己的进程地址空间中,如果只是数据读取操作则不需要进行拷贝操作。
用户态直接IO
运行用户态下的库函数直接访问硬件设备,数据跨过内核进行传输,内核除了必要的存储配置工作之外不参加任何工作。
只能适用于不需要内核缓冲区处理的应用程序,这些应用程序通常自已有数据缓存机制,称为自缓存应用程序。同时由于是直接操作磁盘IO,CPU和IO执行时间有差距,通常配合异步IO使用。
减少拷贝次数
mmap + write
mmap的功能就是将一个进程的地址中的一段虚拟地址映射到磁盘文件地址。
mmap目的就是将呢何种读缓冲区的地址与用户空间的缓冲区进行映射,实现了内核缓冲区与应用程序内存的共享,省去了将数据从内核读缓冲区拷贝到用户缓冲区的过程。

1. 磁盘到内核缓存区(DMA 拷贝)
磁盘上的数据通过 DMA(直接内存访问)拷贝 直接传输到内核缓存区的共享区域。
-
DMA 是一种硬件技术,无需 CPU 参与即可完成数据在设备(磁盘)和内存(内核缓存区)之间的传输,提升了效率。
2. 内核缓存区到用户进程(内存映射)
内核缓存区的共享区域通过 map() 映射 到用户进程的共享区域。
-
内存映射(
mmap机制)让用户空间和内核空间共享同一块物理内存,避免了 “内核到用户空间” 的 CPU 拷贝,减少了数据复制开销。
3. 用户进程到 Socket 缓冲区(CPU 拷贝)
用户进程通过 write() 调用,将用户缓冲区的数据通过 CPU 拷贝 传输到Socket 缓冲区。
-
这一步需要 CPU 参与数据复制,因为 Socket 缓冲区属于内核空间,需通过 CPU 完成用户空间到内核空间的写操作。
4. Socket 缓冲区到网卡(DMA 拷贝)
Socket 缓冲区的数据通过 DMA 拷贝 传输到网卡,最终通过网络发送出去。
mmap优势是提高了IO性能,对于大文件效果很好。但是小文件反而会产生过多的内存碎片。例如一个页最大容纳4KB大小,映射5KB的文件将会占用两页内存页。
sendfile
通过 sendfile 系统调用,数据可以直接在内核空间内部进行 I/O 传输,从而省去了数据在用户空间和内核空间之间的来回拷贝。与 mmap 内存映射方式不同的是, sendfile 调用中 I/O 数据对用户空间是完全不可见的。

基于 sendfile 系统调用的零拷贝方式,整个拷贝过程会发生 2 次上下文切换,1 次 CPU 拷贝和 2 次 DMA 拷贝,用户程序读写数据的流程如下:
- 用户进程通过 sendfile () 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)。
- CPU 利用 DMA 控制器将数据从主存或硬盘拷贝到内核空间(kernel space)的读缓冲区(read buffer)。
- CPU 将读缓冲区(read buffer)中的数据拷贝到的网络缓冲区(socket buffer)。
- CPU 利用 DMA 控制器将数据从网络缓冲区(socket buffer)拷贝到网卡进行数据传输。
- 上下文从内核态(kernel space)切换回用户态(user space),sendfile 系统调用执行返回。
相比较于 mmap 内存映射的方式,sendfile 少了 2 次上下文切换,但是仍然有 1 次 CPU 拷贝操作。sendfile 存在的问题是用户程序不能对数据进行修改,而只是单纯地完成了一次数据传输过程。
sendfile + DMA gather copy
DMA gather将内核空间的读缓冲区对应的数据记录到相应的网络缓冲区中,由DMA根据内存地址、地址偏移量将数据批量从读缓冲区拷贝到网卡设备中,省去了内核空间的CPU copy。

基于 sendfile + DMA gather copy 系统调用的零拷贝方式,整个拷贝过程会发生 2 次上下文切换以及 2 次 DMA 拷贝,用户程序读写数据的流程如下:
- 用户进程通过 sendfile () 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)。
- CPU 利用 DMA 控制器将数据从主存或硬盘拷贝到内核空间(kernel space)的读缓冲区(read buffer)。
- CPU 把读缓冲区(read buffer)的文件描述符(file descriptor)和数据长度拷贝到网络缓冲区(socket buffer)。
- 基于已拷贝的文件描述符(file descriptor)和数据长度,CPU 利用 DMA 控制器的 gather/scatter 操作直接批量地将数据从内核的读缓冲区(read buffer)拷贝到网卡进行数据传输。
- 上下文从内核态(kernel space)切换回用户态(user space),sendfile 系统调用执行返回。
用户程序同样不能修改数据,并且本身需要硬件支持。只适用于将数据从文件拷贝到socket 套接字上。
splice
传统的splice只适用于将数据从文件拷贝到socket 套接字上,同样需要硬件支持。Linuex引入后,splice不仅不需要硬件支持,还可以在两个文件描述符之间实现零拷贝。
splice系统调用可以在内核空间的读缓冲区和网络缓冲区之间建立管道(pipeline),避免CPU拷贝操作。

基于 splice 系统调用的零拷贝方式,整个拷贝过程会发生 2 次上下文切换以及 2 次 DMA 拷贝,用户程序读写数据的流程如下:
- 用户进程通过 splice () 函数向内核(kernel)发起系统调用,上下文从用户态(user space)切换为内核态(kernel space)。
- CPU 利用 DMA 控制器将数据从主存或硬盘拷贝到内核空间(kernel space)的读缓冲区(read buffer)。
- CPU 在内核空间的读缓冲区(read buffer)和网络缓冲区(socket buffer)之间建立管道(pipeline)。
- CPU 利用 DMA 控制器将数据从网络缓冲区(socket buffer)拷贝到网卡进行数据传输。
- 上下文从内核态(kernel space)切换回用户态(user space),splice 系统调用执行返回。
RocketMQ与Kafka
RocketMQ采用mmap + write,适用于业务及消息类似小块文件的数据持久化和传输。
Kafka采用sendfile,适用于系统日志消息这种高吞吐量的大块文件的数据持久化和传输。Kafka的索引文件使用mmap + write,数据文件是sendfile。
