什么是零拷贝
零拷贝(Zero-Copy)技术是指电脑执行操作时,CPU不需要参与数据的搬运复制。这种技术通常用于网络传输文件时,节省CPU周期和内存带宽。
传统的IO流程
早期的I/O原始过程是这样的:
CPU发出指令给磁盘控制器,然后返回;
磁盘控制器收到指令后,将数据复制到磁盘的内部缓冲区,随后对CPU发起IO中断信号
CPU收到中断信号后,将缓冲区的读到寄存器中,再将寄存器中的数据写入的内存,写入到内存期间CPU是无法执行其他任务
执行过程如图所示:
整个数据搬运到内存的过程中都需要CPU参与计算。如果用到千兆网卡或者磁盘传输大量数据的时候,CPU一直处于搬运复制数据的过程中,将会对系统的负载和吞吐量产生比较大的影响。
于是发明了DMA(Direct Memory Acess)技术,也就是直接内存访问。简单理解就是,在磁盘和内存进行数据搬运时,这些工作会由DMA控制器进行,而不是CPU,这样可以减轻CPU的负载。执行过程如下图:
可以看到,整个数据从磁盘到内存传输的过程中,CPU不再参与搬运,全都是DMA控制器完成。早期DMA只存在于主板上,如今基本上每个I/0设备都有自己的DMA控制器。
如果服务端需要有文件传输的功能,简单的方式是:调用系统read()函数 将磁盘文件读入内存,然后通过调用系统write()函数将内存数据写给网络协议栈发送给客户端。如下图:
首先可以看到,读磁盘文件写入到网卡,一共经历了4次的用户态和内核态的切换。原因是:用户线程调用了系统函数一次read()和一次write(),每次系统调用都需要先从用户态切换到内核态,等内核态完成任务后,再从内核态切换回用户态。
其次,发送了4次数据拷贝,两次拷贝是由DMA完成的,两次拷贝是CPU完成的。
由此我们可以分析出,搬运一份数据存在冗余的用户态和内核态的切换以及多余的拷贝。所以想要提高文件传输性能,需要减少用户态和内核态的切换和拷贝次数。
如何实现零拷贝
可以通过调用sendfile()函数替代前面的read()和write()系统调用,这样可以减少一次系统调用,也就减少了两次次用户态和内核态之间的切换开销。其次,该函数可以直接把内核缓冲区里面的数据拷贝的socket缓冲区,这样就只有2次上下文切换,和3次数据拷贝。如下图所示:
但是这个还不是真正的零拷贝技术,从内核2.4版本开始,如果网卡支持SG-DMA(The Scatter-Gather Direct Memory Access),可以进一步减少CPU把内核缓冲区里面的数据拷贝到socket缓冲区的过程。可以通过以下命令查看网卡是否支持scatter-gather特性:
于是,从内核2.4版本开始,对于网卡支持SG-DMA技术的情况下,sendfile() 系统调用过程可以实现CPU的零拷贝,整个过程如下图所示:
这就是所谓的零拷贝(Zero-copy)技术,因为我们没有在内存层面去拷贝数据,也就是说全程没有通过cpu来搬运数据,所有的数据都是由DMA来进行传输的。
零拷贝技术的文件传输方式相比传统文件传输的方式,减少了两次用户态和内核态的切换和数据拷贝次数。所以总体上看,零拷贝技术可以把文件的传输性能提高至少一倍以上。
零拷贝实践总结
Java提供了NIO库中的transferTo方法,go语言中的syscall包中的Sendfile都提供了直接操作底层零拷贝技术的能力。比较火热的kafka开源项目,也利用了零拷贝技术,大幅提升I/0吞吐量,这也是kafka能够处理海量数据的原因之一。
参考一些资料上的性能测试数据,同一个硬件条件下,传统文件传输和零拷贝文件传输性能差异,可以看下图的测试数据图,使用零拷贝能缩短65%的时间。