什么是零拷贝?
零拷贝(英语: Zero-copy) 技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽。
➢零拷贝技术可以减少数据拷贝和共享总线操作的次数,消除传输数据在存储器之间不必要的中间拷贝次数,从而有效地提高数据传输效率;
➢零拷贝技术减少了用户进程地址空间和内核地址空间之间因为上下文切换而带来的开销。
传统的拷贝
一次文件从网络上下载服务器下载到本地的过程
read(file, tmp_buf, len);
write(socket, tmp_buf, len);
文件从磁盘读取到发送的流程:上面代表上下文切换,下面代表数据拷贝操作。涉及到4次上下文切换 ,4次数据拷贝(2次cpu拷贝,2次DMA 拷贝)。
流程:
- 一次 read 的系统调用,使用户空间切换到内核空间,数据从硬盘读取到内核缓冲区。这次拷贝是内存直接拷贝,由 DMA 引擎完成;
- 数据从内核缓存区拷贝到用户缓冲区,read 调用返回。返回导致上下文从内核空间切换到用户空间;
- 系统 write 调用导致,上下文从用户空间切回内核空间。数据再次从用户空间拷贝到内核的socket 缓冲区。这个缓冲区不同于内核缓冲区,是和当前 socket相关的。
- write 系统调用返回,发生一次上下文切换。数据从socket 缓冲区拷贝到协议引擎,这次拷贝由 DMA 引擎完成。
零拷贝的实现
jvm 无法操作 kernel,要减少拷贝,需要 kernel 暴露 api。
mmap
data loaded from disk is stored in a kernel buffer by DMA copy. Then the pages of the application buffer are mapped to the kernel buffer, so that the data copy between kernel buffers and application buffers are omitted.
第一种实现 零拷贝的方法是用 mmap 代替 read 系统调用。这种方式减少了数据拷贝,没有减少上下文的切换。
tmp_buf = mmap(file, len);
write(socket, tmp_buf, len);
3次拷贝: 1次cpu copy,2次DMA copy;以及4次上下文切换;
流程:
- mmap 的一次系统调用导致上下文从用户空间切换到内核空间。数据通过 DMA引擎,从硬盘直接拷贝到内核缓冲区。这块缓冲区是内核空间和用户空间共享的一片区域;
- write 系统调用导致一次数据拷贝,数据从内核空间拷贝到内核的 socket 缓冲区;
- DMA 引擎 把数据从 socket buffer 拷贝到协议引擎;
sendFile
when calling the sendfile() system call, data are fetched from disk and copied into a kernel buffer by DMA copy. Then data are copied directly from the kernel buffer to the socket buffer. Once all data are copied into the socket buffer, the sendfile() system call will return to indicate the completion of data transfer from the kernel buffer to socket buffer. Then, data will be copied to the buffer on the network card and transferred to the network.
kernel 2.1 版本后, sendfile 系统调用减少数据拷贝也减少了上下文切换。
sendfile(socket, file, len);
3次拷贝,1次CPU copy 2次DMA copy;以及2次上下文切换
流程:
1:sendfile 系统调用导致文件内容被DMA引擎复制到内核缓冲区中。然后内核将数据复制到与套接字关联的内核缓冲区中。
2 :第三次复制发生在DMA引擎将数据从内核套接字缓冲区传递到协议引擎时。
java 语言中的零拷贝
linux 提供的零拷贝技术,Java 支持2种(内存映射mmap、sendfile);
NIO MappedByteBuffer
**
MappedByteBuffer mappedByteBuffer = new RandomAccessFile(file, "r")
.getChannel()
.map(FileChannel.MapMode.READ_ONLY, 0, len);
**底层就是调用Linux mmap()实现的。
NIO中的FileChannel.map()方法其实就是采用了操作系统中的内存映射方式,底层就是调用Linux mmap()实现的。
将内核缓冲区的内存和用户缓冲区的内存做了一个地址映射。这种方式适合读取大文件,同时也能对文件内容进行更改,但是如果其后要通过SocketChannel发送,还是需要CPU进行数据的拷贝。
**
NIO sendfile
**
- FileChannel.transferTo()方法直接将当前通道内容传输到另一个通道,没有涉及到Buffer的任何操作,NIO中 的Buffer是JVM堆或者堆外内存,但不论如何他们都是操作系统内核空间的内存
- transferTo()的实现方式就是通过系统调用sendfile() (当然这是Linux中的系统调用)
class ZeroCopyFile {
public void copyFile(File src, File dest) {
try (FileChannel srcChannel = new FileInputStream(src).getChannel();
FileChannel destChannel = new FileInputStream(dest).getChannel()) {
srcChannel.transferTo(0, srcChannel.size(), destChannel);
} catch (IOException e) {
e.printStackTrace();
}
}
}
Java NIO提供的FileChannel.transferTo 和 transferFrom 并不保证一定能使用零拷贝。实际上是否能使用零拷贝与操作系统相关,如果操作系统提供 sendfile 这样的零拷贝系统调用,则这两个方法会通过这样的系统调用充分利用零拷贝的优势,否则并不能通过这两个方法本身实现零拷贝。
扩展阅读
参考: 1.零拷贝
2.java 中的零拷贝