所谓的零拷贝是指将数据直接从磁盘文件复制到网卡,而不需要经由应用之手。零拷贝大大提高了应用程序的性能,减少了内核和用户模式之间的上下文切换。对Linux操作系统而言,零拷贝技术依赖于底层的sendfile()方法实现,对于Java语言,FileChannel.transferTo()方法的底层实现就是sendfile()方法。

单纯从概念上理解“零拷贝”比较抽象,这里简单地介绍一下它。考虑这样一种常用的情形:你需要将静态内容(类似图片、文件)展示给用户。这个情形就意味着需要先将静态内容从磁盘中复制出来放到一个内存buf中,然后将这个buf通过套接字(Socket)传输给用户,进而用户获得静态内容。这看起来再正常不过了,但实际上这是很低效的流程,把上面的这种情形抽象成下面的过程:

read(file, tmp_buf, len);
write(socket, tmp_buf, len);

首先调用read()将静态内容(这里假设为文件A)读取到tmp_buf,然后调用write()将tmp_buf写入Socket,如下图:

kafka发送失败监控 kafka sendfile_数据

在这个过程中,文件A经历了4次复制过程:

  1. 调用read()时,文件A中的内容被复制到了内核模式下的Read Buffer中
  2. CPU控制将内核模式数据复制到用户模式下
  3. 调用write()时,将用户模式下的内容复制到内核模式下的Socket Buffer中
  4. 将内核模式下的Socket Buffer的数据复制到网卡设备中传送

从上面的过程可以看出,数据平白无故地从内核模式到用户模式“走了一圈”,浪费了2次复制过程:第一次是从内核模式复制到用户模式;第二次是用户模式再复制回内核模式,即上面4次过程中的第2步和第3步。而且在上面的过程中,内核和用户模式的上下文的切换也是4次。

如果采用了零拷贝技术,那么应用程序可以直接请求内核把磁盘中的数据传输给Socket,如下图:

kafka发送失败监控 kafka sendfile_kafka_02

零拷贝技术通过DMA(Direct Memory Access)技术将文件内容复制到内核模式下的Read Buffer中。不过没有数据被复制到Socket Buffer,相反只有包含数据的位置和长度的信息的文件描述符被加到Socket Buffer中。DMA引擎直接将数据从内核模式中传递到网卡设备(协议引擎)。这里数据只经历了2次复制就从磁盘中传送出去了,并且上下文切换也变成了2次。零拷贝是针对内核模式而言的,数据在内核模式下实现了零拷贝。