Zero Copy
设想一个场景,我们需要把本地的一个文件通过socket来发送到网络上,一般最常见的写法是
File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);
虽然程序看起来很短,但是其实执行了四次的拷贝操作。下面的图显示了拷贝的过程
普通的拷贝流程
-
read()会使得系统从user mode切换到kernel model。在内部会调用一个sys_read()的函数来读取文件。这是第一次拷贝,第一次拷贝是由DMA来进行的。拷贝来的数据会储存在kernel的buffer中。 -
第二次拷贝是kernel的buffer中的数据会拷贝到用户的buffer,也就是应用层的buffer。此时的状态从kernel mode切换为user mode
-
第三次拷贝也就是在调用
read()函数的时候,状态又从user mode转换为kernel mode。也是buffer之间进行了相互的转换。 -
当第三部完成后,
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来表示数据的开始位置和长度,做到了真正意义的零拷贝。