零拷贝

首发于我的个人博客

概念

零拷贝(zero copy)指的是当拷贝发生时,CPU并不参与实际的拷贝过程(也可以指拷贝数据这个过程),CPU可以切换到其他线程,数据的拷贝过程异步进行,异步过程通常要由硬件DMA实现。采用传统的读写操作将磁盘中的数据发送到网络中,通常经历2次用户态/内核态的切换,并且读和写操作CPU分别要参与一次拷贝过程。

DMA

DMA可以让CPU从数据拷贝中解放出来,这样IO就可以异步进行。CPU只需在DMA中初始化几个参数,接着CPU就可以干其它事情,而IO依旧在发生中。CPU告知DMA内存地址、读取的字节数和驱动的端口号。当DMA完成它的工作时,就会发生一个中断信号给CPU,这时数据就出现在期望的内存中。这样CPU就不必轮询IO的完成或参与到IO的流程(被称为programmed input/output)中。
DMA

问题

考虑以下代码:

// read a file to tmp_buf buffer
read(file, tmp_buf, len);
// write tem_buf's data to socket
write(socket, tmp_buf, len);

这段代码会进行以下操作:

  1. read()会进行一次系统调用且执行一次上下文切换到内核态。这个过程有DMA将磁盘中的数据复制到内核的缓冲区之中。
  2. 数据从内核缓冲区复制到用户空间,read()调用返回,系统切回到用户态。
  3. write()进行一次系统调用,并且切换到内核态。第三次拷贝发生了,数据再次被拷贝到内核空间buffer,这个buffer关联一个socket。
  4. write()调用返回,并且切回到用户态。第四次拷贝由DMA执行,它将内核缓冲区的数据拷贝到协议引擎(protocol engine)中。

这个过程进行了四次上下文切换,CPU要参与两次拷贝过程。用户空间的拷贝过程都是没有必要的。

mmap

mmap将一个文件或设备映射到内存中。

#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,
                  int fd, off_t offset);

mmap允许程序直接在用户态中访问内核空间中的数据,这样就避免了一次无意义的拷贝。
mmap
当向socket写入数据时,数据还是要拷贝到socket缓冲区中,再拷贝到协议引擎中。

sendfile

#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

sendfile()可以在两个文件描述符之间(fd)拷贝数据,它在内核态完成所以比read/write组合更加高效。in_fd是一个可以被mmap映射的文件。在Linux2.6.33之前,out_fd必须是个socket。sendfile返回传输的字节数。

sendfile

sendfile可以将文件直接向socket传递,DMA将数据复制到内核空间后,再拷贝到socket buffer,然后DMA将数据传递给协议引擎。Linux2.4之后,DMA可以直接将内核缓冲区数据直接传输到协议引擎,消灭最后一次拷贝。

sendfile

Java NIO transferTO

Java中java.nio.FileChannel提供了一个transferTo方法,在Unix/Linux中会被传递到sendfile()。ByteBuffer.allocateDirect()可以在JVM堆外分配,这样就不受GC的影响,也可以不再JVM与OS之间复制。FileChannel与SocketChannel都是WritableChannel,所以可以作为target传入。

public void transferTo(long position, long count, WritableByteChannel target);
阅读 726

推荐阅读