零拷贝Zero Copy

795 阅读2分钟

Zero Copy

设想一个场景,我们需要把本地的一个文件通过socket来发送到网络上,一般最常见的写法是

File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);

虽然程序看起来很短,但是其实执行了四次的拷贝操作。下面的图显示了拷贝的过程

普通的拷贝流程

7fe2d34533a2762e4a2a3f0bdd5f7209

  1. read() 会使得系统从user mode切换到kernel model。在内部会调用一个sys_read()的函数来读取文件。这是第一次拷贝,第一次拷贝是由DMA来进行的。拷贝来的数据会储存在kernel的buffer中。

  2. 第二次拷贝是kernel的buffer中的数据会拷贝到用户的buffer,也就是应用层的buffer。此时的状态从kernel mode切换为user mode

  3. 第三次拷贝也就是在调用read()函数的时候,状态又从user mode转换为kernel mode。也是buffer之间进行了相互的转换。

  4. 当第三部完成后,read()将会返回,系统用非阻塞的方法将socket buffer拷贝到网络协议的NIC buffer来准备发送。

使用中间段的kernel buffer充当了一个readahead cache的角色,当应用没有要求buffer size那么大的空间的时候,在写端可以允许异步写从而提升效率。

零拷贝

根据上图可以看到第二次和第三次的拷贝其实是不必要的。有一种方式允许直接从read buffer拷贝到socket buffer. transferTo()方法如下

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

transferTo()允许数据从file channel传输到其它可以被写的channel。根据不同的OS,内部会有不同的调用方法。例如在UNIX中,会调用sendfile()如下,

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

因此我们伪代码可以改造为

transferTo(position, count, writableChannel);
Socket.send(socket, buf, len);

改造完后,数据流变为

由此,我们可以省掉2次的拷贝。

更近一步

虽然省略了两次拷贝,但是还是依靠不够有效率。在拷贝的时候,只传入数据的descriptor来表示数据的开始位置和长度,做到了真正意义的零拷贝。

参考

developer.ibm.com/languages/j…

my.oschina.net/u/2411391/b…