Java NIO中的Zero-copy(零拷贝)技术

  |   0 评论   |   4,559 浏览

Zero-copy技术介绍

零复制(英语:Zero-copy;也译零拷贝)技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域,从而可以减少上下文切换及CPU的拷贝时间,通常用于通过网络传输文件时节省CPU周期和内存带宽。

假如我们要实现这样的功能:将文件中的字节复制到套接字中


read(file, tmp_buf, len);
write(socket, tmp_buf, len);

主要就是读取文件内容到buffer中,之后将buffer数据发送到socket。

传统的数据拷贝流程

如下图,上方是上下文切换流程,下方是数据拷贝流程

6.jpg


过程如下

  1. read()函数调用导致了一次用户态到内核态的上下文切换。系统内部的sys_read()调用被用于把数据读取出来。第一次复制是以DMA引擎的方式呈现的,DMA把数据读取出来并存入kernel address space buffer。

  2. 数据被读取返回后导致了上下文从内核态切换到用户态,现在数据存在user address space buffer中。

  3. send()操作再次把上下文从用户态切换到内核态。并且数据被从用户缓存拷贝到kernel address space buffer中。

  4. send()操作返回,这时又从内核态返回到用户态,并且发生最后一次数据拷贝,数据从kernel buffer拷贝到protocol engine中。

这个过程当中一共出现了4次数据拷贝和4次用户态-内核态用户态-内核态上下文切换(每一次系统调用都是两次上下文切换:用户态->内核态->用户态)。

transferTo()方式

仔细检查上面的流程,其实第二次和第三次复制是不必要的(从buffer到应用程序、从应用程序到buffer),应用程序并没有改变数据内容,只是将其返回到socket buffer中。Java中提供了transferTo()方法,可以让你实现数据直接从read buffer输到 socket buffer。

transferTo() 方法将数据从一个文件channel传输到一个可写channel。在内部它依赖于操作系统对 Zero-copy 的支持,在UNIX/Linux系统上, transferTo() 实际会调用 sendfile() 这个系统函数,将数据从一个文件描述符传输到另一个。

下图展示了使用 transferTo()时的数据拷贝情况

3.gif

下图展示了使用 transferTo()时的上下文切换情况

4.gif

过程如下

  1. transferTo()调用使文件内容通过DMA的方式被复制到read buffer。然后将数据复制到与输出的socket相关的buffer中。

  2. 第三次复制发生在DMA将数据复制到protocol engine。

这已经有了改进,我们已将上下文切换次数从四次减少到两次,并将数据拷贝的次数从四次减少到三次(其中只有一次涉及CPU操作)

继续优化

但这还没有达到zero-copy的目标。如果网卡支持 gather operations 内核就可以进一步减少数据拷贝。在 Linux kernels 2.4 及更新的版本,socket 描述符已经为适应这个需求做了变化。这种方法不仅减少了2次上下文切换,还消除了需要CPU参与的重复数据拷贝。

下图展示了使用收集操作的transferTo()方法时的数据拷贝情况

5.gif

过程如下

  1. transferTo方法调用使文件内容通过DMA引擎被复制到kernel buffer

  2. 无数据被复制到socket buffer。只是描述了需要被写入的数据的位置和长度。DMA引擎直接把数据从kernel buffer复制到protocol engine

现在整个过程只有两次上下文切换和两次数据拷贝。

性能对比


从上图中可以看到,与传统方法相比,使用transferTo()API可减少大约65%的时间。这对于需要将大量数据从一个I / O通道复制到另一个I / O通道的应用程序(如Web服务器)来说可以显著提高性能。

mmap方式

mmap允许代码将文件映射到内核内存并直接访问它,就好像它在用户空间中一样,从而避免了不必要的复制。 作为折衷,这仍然涉及4次上下文切换。 但是,由于操作系统将某个文件块映射到内存中,因此您可以获得操作系统的虚拟内存管理的所有好处:可以高效地缓存热内容,并且所有数据都是page-aligned的,因此不需要进行缓冲区复制即可写回内容。

7.png

然而,这不是没有代价的 - 虽然mmap确实避免了额外的拷贝,但并不保证代码总是更快 - 这依赖于操作系统的实现,可能会有相当多的设置和拆卸开销(因为它需要查找 该空间并将其保留在TLB中,并确保在解映射后对其进行刷新),并且由于内核现在需要从硬件(如磁盘)读取以更新内存空间和TLB,因此页面错误的代价变得更加高。 因此,如果性能是至关重要的,则性能测试是必不可少的,因为滥用mmap()可能会产生比简单复制更差的性能。

Java中相应的类是来自NIO包的MappedByteBuffer。它实际上是DirectByteBuffer的变体,尽管它们之间没有直接关系。

Java NIO DirectByteBuffer

Java NIO用于引进ByteBuffer类来表示Channel的Buffer,ByteBuffer主要有以下3种实现

HeapByteBuffer

当调用ByteBuffer.allocate()时使用。它被称为堆是因为它保存在JVM的堆空间中,可以因此获得JVM的所有好处,例如GC支持和缓存优化。但是,它不是page aligned的,这意味着如果您需要通过JNI与Native代码交互,JVM将不得不复制到aligned buffer space。

DirectByteBuffer

当调用ByteBuffer.allocateDirect()时使用。JVM将使用malloc()在堆外分配内存。因为它不是由JVM管理的,所以你的内存空间是页面对齐的,并且不受GC控制,这使得它成为使用Native代码时的完美选择。然而,随后你将必须自己分配和释放内存以防止内存泄漏。

MappedByteBuffer

当调用FileChannel.map()时使用。与DirectByteBuffer类似,这也是分配在JVM堆外内存。它基本上时OS mmap()系统调用的包装函数,以便代码直接操作映射的物理内存数据。


参考

http://xcorpion.tech/2016/09/10/It-s-all-about-buffers-zero-copy-mmap-and-Java-NIO/

https://www.ibm.com/developerworks/linux/library/j-zerocopy/

https://www.linuxjournal.com/article/6345





读后有收获可以支付宝请作者喝咖啡