Zero Copy(零拷贝)
缓冲区是所有I/O的基础,I/O无非就是把数据移进或移出缓冲区。下面看一个java进程发起read请求加载数据大致的流程图:
进程发起read请求之后,内核接收到read请求之后,会先检查内核空间中是否已经存在进程所需要的数据,如果已经存在,则直接把数据copy给进程的缓冲区;如果没有,内核随即向磁盘控制器发出命令,要求从磁盘读取数据,磁盘控制器把数据直接写入内核read缓冲区,这一步通过DMA完成;接下来就是内核将数据copy到进程的缓冲区;
如果进程发起 write网络请求,同样需要把用户缓冲区里面的数据copy到内核的socket缓冲区里面,然后再通过DMA把数据copy到网卡中,发送出去;
这样每次都需要把内核空间的数据拷贝到用户空间中,零拷贝的出现就是为了解决这种问题。
关于零拷贝提供了两种方式分别是:mmap+write方式,sendfile方式。
先介绍一下虚拟内存,所有现代操作系统都使用虚拟内存,使用虚拟的地址取代物理地址,这样做的好处是:
- 一个以上的虚拟地址可以指向同一个物理内存地址,
- 虚拟内存空间可大于实际可用的物理地址;
利用第一个特性可以把内核空间地址和用户空间的虚拟地址映射到同一个物理地址,这样DMA就可以填充对内核和用户空间进程同时可见的缓冲区了,大致如下图所示:
这样就省去了内核与用户空间的往来拷贝,java也利用操作系统的此特性来提升性能。
mmap+write方式
使用 mmap+write 方式代替原来的 read+write 方式,mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系;这样就可以省掉原来内核read缓冲区copy数据到用户缓冲区,但是还是需要内核read缓冲区将数据copy到内核socket缓冲区,大致如下图所示:
sendfile方式
sendfile系统调用在内核版本2.1中被引入,目的是简化通过网络在两个通道之间进行的数据传输过程。sendfile系统调用的引入,不仅减少了数据复制,还减少了上下文切换的次数,大致如下图所示:
数据传送只发生在内核空间,所以减少了一次上下文切换;但是还是存在一次copy,能不能把这一次copy也省略掉,Linux2.4内核中做了改进,将Kernel buffer中对应的数据描述信息(内存地址,偏移量)记录到相应的socket缓冲区当中,这样连内核空间中的一次cpu copy也省掉了。
Java零拷贝
1. MappedByteBuffer
java nio提供的FileChannel提供了map()方法,该方法可以在一个打开的文件和MappedByteBuffer之间建立一个虚拟内存映射,MappedByteBuffer继承于ByteBuffer,类似于一个基于内存的缓冲区,只不过该对象的数据元素存储在磁盘的一个文件中;调用get()方法会从磁盘中获取数据,此数据反映该文件当前的内容,调用put()方法会更新磁盘上的文件,并且对文件做的修改对其他线程也是可见的。
2. Channel-to-Channel传输
经常需要从一个位置将文件传输到另外一个位置,Java类库通过java.nio.channels.FileChannel提供的transferTo()方法用来提高传输的效率,可以通过这个方法把一个channel中读取到的字节传输到另一个channel,不再需要数据流经应用程序。
下图展示了通过transferTo实现数据传输的路径:
Java中使用zero copy的数据传输的方法:
1 | public void transferTo(long position,long count,WritableByteChannel target); |
DMA(Direct Memory Access,直接存储器访问)
DMA(Direct Memory Access,直接内存存取) 是所有现代电脑的重要特色,它允许不同速度的硬件装置来沟通,而不需要依赖于 CPU 的大量中断负载。DMA负责将数据从一个地址空间复制到另外一个地址空间。
很多Web应用程序都会提供大量的静态内容,其数量多到相当于读完整个磁盘的数据再将同样的数据写回响应套接字(socket)。此动作看似只需较少的 CPU 活动,但它的效率非常低:内核首先从磁盘文件读取数据,然后从内核空间将数据传到用户空间,应用程序又将数据从用户空间返回到内核空间然后传输给socket。实际上,应用程序就相当于是个低效的中间者,从磁盘拿数据放到socket。
下图展示了数据从文件到socket的内部流程:

每次数据在内核空间和用户空间传输就一次拷贝过程,这是需要占用一定的CPU周期和内存资源的。幸运的是可以通过一个叫zero copy的技术来消除这些拷贝过程。使用了zero copy技术的应用程序的数据传输过程就是内核从磁盘文件读取数据直接传输到socket中,不再经过应用程序这个中间者。zero copy大大改善了应用程序的性能并且减少了用户态和内核态之间的切换次数。
在linux或者unix系统上,Java类库通过java.nio.channels.FileChannel的transferTo()方法来应用zero copy。可以通过这个方法把一个channel中读取到的字节传输到另一个channel,不再需要数据流经应用程序。
下图展示了通过transferTo实现数据传输的路径:

Java中使用zero copy的数据传输的方法:
1 | public void transferTo(long position,long count,WritableByteChannel target); |
transferTo()方法将数据从一个channel传输到另一个可写的channel上,其内部实现依赖于操作系统对zero copy技术的支持。在unix操作系统和各种linux的发型版本中,这种功能最终是通过sendfile()系统调用实现。
使用transferTo()方式所经历的步骤:
1、transferTo调用会引起DMA将文件内容复制到读缓冲区(内核空间的缓冲区),然后数据从这个缓冲区复制到另一个与socket输出相关的内核缓冲区中。
2、第三次数据复制就是DMA把socket关联的缓冲区中的数据复制到协议引擎上发送到网络上。
这次改善,将数据的复制次数从四次减少到三次(只有一次用到cpu资源)。但这并没有达到零复制的目标。如果底层网络适配器支持收集操作的话,可以进一步减少内核对数据的复制次数。在内核为2.4或者以上版本的linux系统上,socket缓冲区描述符将被用来满足这个需求。这个方式不仅减少了内核用户态间的切换,而且也省去了那次需要cpu参与的复制过程。从用户角度来看依旧是调用transferTo()方法,但是其本质发生了变化:
1、调用transferTo方法后数据被DMA从文件复制到了内核的一个缓冲区中。
2、数据不再被复制到socket关联的缓冲区中了,仅仅是将一个描述符(包含了数据的位置和长度等信息)追加到socket关联的缓冲区中。DMA直接将内核中的缓冲区中的数据传输给协议引擎,消除了仅剩的一次需要cpu周期的数据复制。
如下图:

Java nio中提供的FileChannel和SocketChannel实现零拷贝传输数据示例代码:
1 | public long copy(String srcFilename,String destFilename) throws Exception{ |
转载自:
http://blog.csdn.net/fyxxq/article/details/20000045
http://www.cnblogs.com/metoy/p/4033366.html
https://maimai.cn/article/detail?fid=1236304197&efid=u4GUhDloTNP1da_BcFd-4Q