什么是零拷贝

维基上是这么描述零拷贝的:零拷贝描述的是CPU不执行拷贝数据从一个存储区域到另外一个存储区域的任务,这一般用于经过网络传输一个文件时以减小CPU周期和内存带宽。html

零拷贝给咱们带来的好处

减小甚至彻底避免没必要要的CPU拷贝,从而让CPU解脱出来去执行其余的任务

减小内存带宽的占用

一般零拷贝技术还可以减小用户空间和操做系统内核空间之间的上下文切换

零拷贝的实现

零拷贝实际的实现并无真正的标准,取决于操做系统如何实现这一点。零拷贝彻底依赖于操做系统。操做系统支持,就有;不支持,就没有。不依赖Java自己。linux

传统I/O

在Java中,咱们能够经过InputStream从源数据中读取数据流到一个缓冲区里,而后再将它们输入到OutputStream里。咱们知道,这种IO方式传输效率是比较低的。那么,当使用上面的代码时操做系统会发生什么状况:

Java零拷贝场景 java零拷贝技术_上下文切换

程序员

JVM发出read() 系统调用。

OS上下文切换到内核模式(第一次上下文切换)并将数据读取到内核空间缓冲区。(第一次拷贝:hardware ----> kernel buffer)

OS内核而后将数据复制到用户空间缓冲区(第二次拷贝: kernel buffer ——> user buffer),而后read系统调用返回。而系统调用的返回又会致使一次内核空间到用户空间的上下文切换(第二次上下文切换)。

JVM处理代码逻辑并发送write()系统调用。

OS上下文切换到内核模式(第三次上下文切换)并从用户空间缓冲区复制数据到内核空间缓冲区(第三次拷贝: user buffer ——> kernel buffer)。

write系统调用返回,致使内核空间到用户空间的再次上下文切换(第四次上下文切换)。将内核空间缓冲区中的数据写到hardware(第四次拷贝: kernel buffer ——> hardware)。

总的来讲,传统的I/O操做进行了4次用户空间与内核空间的上下文切换,以及4次数据拷贝。显然在这个用例中,从内核空间到用户空间内存的复制是彻底没必要要的,由于除了将数据转储到不一样的buffer以外,咱们没有作任何其余的事情。因此,咱们能不能直接从hardware读取数据到kernel buffer后,再从kernel buffer写到目标地点不就行了。为了解决这种没必要要的数据复制,操做系统出现了零拷贝的概念。注意,不一样的操做系统对零拷贝的实现各不相同。在这里咱们介绍linux下的零拷贝实现。web

经过sendfile实现的零拷贝I/O

Java零拷贝场景 java零拷贝技术_java中osend_02

发出sendfile系统调用,致使用户空间到内核空间的上下文切换(第一次上下文切换)。经过DMA将磁盘文件中的内容拷贝到内核空间缓冲区中(第一次拷贝: hard driver ——> kernel buffer)。

而后再将数据从内核空间缓冲区拷贝到内核中与socket相关的缓冲区中(第二次拷贝: kernel buffer ——> socket buffer)。

sendfile系统调用返回,致使内核空间到用户空间的上下文切换(第二次上下文切换)。经过DMA引擎将内核空间socket缓冲区中的数据传递到协议引擎(第三次拷贝: socket buffer ——> protocol engine)。

经过sendfile实现的零拷贝I/O只使用了2次用户空间与内核空间的上下文切换,以及3次数据的拷贝。

你可能会说操做系统仍然须要在内核内存空间中复制数据(kernel buffer —>socket buffer)。 是的,但从操做系统的角度来看,这已是零拷贝,由于没有数据从内核空间复制到用户空间。 内核须要复制的缘由是由于通用硬件DMA访问须要连续的内存空间(所以须要缓冲区)。 可是,若是硬件支持scatter-and-gather,这是能够避免的。apache

带有DMA收集拷贝功能的sendfile实现的I/O

Java零拷贝场景 java零拷贝技术_上下文切换_03

发出sendfile系统调用,致使用户空间到内核空间的上下文切换(第一次上下文切换)。经过DMA引擎将磁盘文件中的内容拷贝到内核空间缓冲区中(第一次拷贝: hard drive ——> kernel buffer)。

没有数据拷贝到socket缓冲区。取而代之的是只有相应的描述符信息会被拷贝到相应的socket缓冲区当中。该描述符包含了两方面的信息:a)kernel buffer的内存地址;b)kernel buffer的偏移量。

sendfile系统调用返回,致使内核空间到用户空间的上下文切换(第二次上下文切换)。DMA gather copy根据socket缓冲区中描述符提供的位置和偏移量信息直接将内核空间缓冲区中的数据拷贝到协议引擎上(第二次拷贝: kernel buffer ——> protocol engine),这样就避免了最后一次CPU数据拷贝。

带有DMA收集拷贝功能的sendfile实现的I/O只使用了2次用户空间与内核空间的上下文切换,以及2次数据的拷贝,并且这2次的数据拷贝都是非CPU拷贝。这样一来咱们就实现了最理想的零拷贝I/O传输了,不须要任何一次的CPU拷贝,以及最少的上下文切换。编程

许多Web服务器都支持零拷贝,如Tomcat和Apache。 例如Apache的相关文档能够在这里找到,但默认状况下关闭。

注意:Java的NIO经过transferTo()提供了这个功能。缓存

传统I/O用户空间缓冲区中存有数据,所以应用程序可以对此数据进行修改等操做;而sendfile零拷贝消除了全部内核空间缓冲区与用户空间缓冲区之间的数据拷贝过程,所以sendfile零拷贝I/O的实现是完成在内核空间中完成的,这对于应用程序来讲就没法对数据进行操做了。为了解决这个问题,Linux提供了mmap零拷贝来实现咱们的需求。服务器

经过mmap实现的零拷贝I/O

mmap(内存映射)是一个比sendfile昂贵但优于传统I/O的方法。

Java零拷贝场景 java零拷贝技术_上下文切换_04

网络

发出mmap系统调用,致使用户空间到内核空间的上下文切换(第一次上下文切换)。经过DMA引擎将磁盘文件中的内容拷贝到内核空间缓冲区中(第一次拷贝: hard drive ——> kernel buffer)。

mmap系统调用返回,致使内核空间到用户空间的上下文切换(第二次上下文切换)。接着用户空间和内核空间共享这个缓冲区,而不须要将数据从内核空间拷贝到用户空间。由于用户空间和内核空间共享了这个缓冲区数据,因此用户空间就能够像在操做本身缓冲区中数据通常操做这个由内核空间共享的缓冲区数据。

发出write系统调用,致使用户空间到内核空间的上下文切换(第三次上下文切换)。将数据从内核空间缓冲区拷贝到内核空间socket相关联的缓冲区(第二次拷贝: kernel buffer ——> socket buffer)。

write系统调用返回,致使内核空间到用户空间的上下文切换(第四次上下文切换)。经过DMA引擎将内核空间socket缓冲区中的数据传递到协议引擎(第三次拷贝: socket buffer ——> protocol engine)

经过mmap实现的零拷贝I/O进行了4次用户空间与内核空间的上下文切换,以及3次数据拷贝。其中3次数据拷贝中包括了2次DMA拷贝和1次CPU拷贝。明显,它与传统I/O相比仅仅少了1次内核空间缓冲区和用户空间缓冲区之间的CPU拷贝。这样的好处是,咱们能够将整个文件或者整个文件的一部分映射到内存当中,用户直接对内存中对文件进行操做,而后是由操做系统来进行相关的页面请求并将内存的修改写入到文件当中。咱们的应用程序只须要处理内存的数据,这样能够实现很是迅速的I/O操做。并发

说了这么多,那么Java NIO中对零拷贝的使用有哪些呢?

NIO DirectByteBuffer

Java NIO引入了用于通道的缓冲区的ByteBuffer。 ByteBuffer有三个主要的实现:

HeapByteBuffer

在调用ByteBuffer.allocate()时使用。 它被称为堆,由于它保存在JVM的堆空间中,所以你能够得到全部优点,如GC支持和缓存优化。 可是,它不是页面对齐的,这意味着若是你须要经过JNI与本地代码交谈,JVM将不得不复制到对齐的缓冲区空间。

DirectByteBuffer

在调用ByteBuffer.allocateDirect()时使用。 JVM将使用malloc()在堆空间以外分配内存空间。 由于它不是由JVM管理的,因此你的内存空间是页面对齐的,不受GC影响,这使得它成为处理本地代码的完美选择。 然而,你要C程序员同样,本身管理这个内存,必须本身分配和释放内存来防止内存泄漏。

MappedByteBuffer

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

总结

零拷贝是操做系统底层的一种实现,咱们在网络编程中,利用操做系统这一特性,能够大大提升数据传输的效率。这也是目前网络编程框架中都会采用的方式,理解好零拷贝,有助于咱们进一步学习Netty等网络通讯框架的底层原理。