首发于我的个人博客
概念
零拷贝(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)中。
问题
考虑以下代码:
// read a file to tmp_buf buffer
read(file, tmp_buf, len);
// write tem_buf's data to socket
write(socket, tmp_buf, len);
这段代码会进行以下操作:
- read() 会进行一次系统调用且执行一次上下文切换到内核态。这个过程有 DMA 将磁盘中的数据复制到内核的缓冲区之中。
- 数据从内核缓冲区复制到用户空间,read() 调用返回,系统切回到用户态。
- write() 进行一次系统调用,并且切换到内核态。第三次拷贝发生了,数据再次被拷贝到内核空间 buffer,这个 buffer 关联一个 socket。
- 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 允许程序直接在用户态中访问内核空间中的数据,这样就避免了一次无意义的拷贝。
当向 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 可以将文件直接向 socket 传递,DMA 将数据复制到内核空间后,再拷贝到 socket buffer,然后 DMA 将数据传递给协议引擎。Linux2.4 之后,DMA 可以直接将内核缓冲区数据直接传输到协议引擎,消灭最后一次拷贝。
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);