目录
前述
什么是“零拷贝”?
目的与好处
详述(Linux中的零拷贝)
普通拷贝(I/O)操作
sendfile方式的零拷贝
带有DMA收集拷贝功能的sendfile实现的I/O
注意⚠️
新问题
解决
mmap(内存映射)方式
NIO中的零拷贝
Channel
transferTo()——sendFile
map()/MappedByteBuffer——mmap
总结
前述
什么是“零拷贝”?
拷贝数据从一块区域到另一块区域不需要cpu来执行。
目的与好处
提升cpu的效率,使cpu可以做更多,更有意义的事,而不是来处理io这种单一简单的操作
减少用户态空间与内核态空间的切换,减少拷贝数据的次数
详述(Linux中的零拷贝)
Unix/Linux
的体系架构分为内核空间(kernal space)与用户空间(application space)内核态(内核空间):内核控制着计算机的硬件资源,为上层应用程序提供运行环境,为应用程序的执行提供着必要的cpu、存储、IO资源等。为了使应用程序访问使用到这些资源,内核就提供了资源访问的接口:系统调用。
用户态(用户空间):应用程序的活动空间
普通拷贝(I/O)操作
- 应用程序(例如jvm)发出read()请求
- 系统由用户态切换到内核态(第一次切换),通过DMA(Direct memory access)的方式,将磁盘中的数据拷贝到内核缓冲区(第一次拷贝)
- 系统由内核态切换到用户态(第二次切换),并由CPU将内核缓冲区数据复制到用户缓冲区(第二次拷贝),返回read给应用程序
- 应用程序根据反回的数据进行逻辑处理后,发出write()请求
- 系统由用户态切换到内核态(第三次切换),并由CPU将用户缓冲区数据复制到内核缓冲区(第三次拷贝)
- 通过DMA方式将内核缓冲区数据写入到磁盘(第四次拷贝),并由内核态切换到用户态(第四次切换),返回write
如上,普通的拷贝,要经历4次用户态与内核态切换,4次数据拷贝(其中2次cpu拷贝)。
其中两次的cpu拷贝都是由于用户态与内核态之间数据复制:内核态——用户态——内核态
如果我们在用户态期间不需要修改数据,那么直接:内核态——内核态
这样就大大减少了用户态与内核态之间的切换,以及cpu拷贝操作。
sendfile方式的零拷贝
- 发出sendfile()调用,用户态切换到内核态(第一次切换),通过DMA引擎将磁盘中的数据拷贝到内核缓冲区(第一次拷贝)
- 利用CPU将内核态数据复制到内核中相关的socket缓冲区(第二次拷贝)
- sendfile()返回,内核态切换到用户态(第二次切换),同时利用DMA引擎将socket缓冲区数据发送到网卡的缓冲区(第三次拷贝)。
使用sendfile()方式,有2次切换,3次拷贝,但依旧还有一次的CPU拷贝,但当对传统拷贝方式,已经有较大的提升!
其中我们也发现,在内核态中,我们还是需要一次拷贝,来拷贝到socket缓冲区,这是因为通用硬件DMA访问需要连续的内存空间,因此需要一个socket缓冲区。
那么对于这一次CPU拷贝,我们能不能也减少呢?技术的产生便是为解决问题服务的,所以也自然有就解决方案的!
带有DMA收集拷贝功能的sendfile实现的I/O
- 发出sendfile()调用,用户态切换到内核态(第一次切换),通过DMA引擎将磁盘中的数据拷贝到内核缓冲区(第一次拷贝)
- 不会将socket缓冲区数据拷贝到socket缓冲区,而是将相应的描述符信息(a.kernel buffer的内存地址,b.kernel buffer的偏移量)拷贝到socket储存到socke缓冲区
- sendfile()返回,内核态切换到用户态(第二次切换),同时利用DMA gather copy,根据socket缓冲区中描述符提供的位置和偏移量信息直接将内核空间缓冲区中的数据拷贝到网卡上(第二次拷贝)
带有DMA收集拷贝共的sendfile方式,2次切换,2次拷贝
没有了CPU拷贝,这样也便达到了真正意义上的零拷贝!
注意⚠️
零拷贝实际的实现并没有真正的标准,取决于操作系统如何实现这一点。零拷贝完全依赖于操作系统。操作系统支持,就有;不支持,就没有。不依赖Java本身。
许多Web服务器都支持零拷贝,如Tomcat和Apache
Java的NIO通过transferTo()提供了这个功能
新问题
普通拷贝的方式,因为会拷贝到用户态缓冲区,所以应用程序可以在读取数据后进行修改等操作,但目前的sendfile()零拷贝方式均是在内核态中完成,所以如果需要在拷贝期间对数据进行操作,就不行了。为了解决这种问题,mmap(内存映射)的拷贝方式来实现我们的需求。
解决
mmap(内存映射)方式
- 发出mmap()调用,用户态切换到内核态(第一次切换),通过DMA引擎将磁盘中的数据拷贝到内核缓冲区(第一次拷贝)
- mmap()返回,内核态切换到用户态(第二次切换),将用户态缓冲区的内存地址与内核态缓冲区的内存地址做映射,这样内存态缓冲区可以读取并操作内核态,相当于用户态共享内核态这块的缓冲区
- write调用,用户态切换到内核态(第三次切换),利用CPU将内核态数据复制到内核中相关的socket缓冲区(第二次拷贝)
- write返回,内核态切换到用户态(第四次切换),同时利用DMA引擎将socket缓冲区数据发送到网卡的缓冲区(第三次拷贝)
通过mmap拷贝,共4次切换,3次拷贝,其中1次CPU拷贝。
与sendfile拷贝方式相比多了切换次数与拷贝次数,但与传统I/O拷贝相比,却少一次CPU拷贝,并且应用程序也可以对数据进行操作。
所以mmap拷贝方式逊于sendfile方式,但却优于普通拷贝方式
NIO中的零拷贝
上面的注意⚠️中也提到:零拷贝完全依赖于操作系统。操作系统支持,就有;不支持,就没有。不依赖Java本身。
所以java NIO中的零拷贝,其实也是调用系统中的零拷贝方式实现的。
Channel
说到NIO不得不提到channel(通道),这里大致说明一下channel
因为NIO中所有的I/O都是从channel(channel)开始的
- 从通道——读数据:创建一个buff(缓冲区),然后请求channel(通道)读取数据
- 从通道——写数据:创建一个buff(缓冲区),填充数据,然后请求channel(通道)写入数据
通常我们的的I/O都是对流进行操作,而NIO中是对channel进行操作
channel与流的区别如下:
- 流是单向的(只能读或写,所以之前我们用流进行IO操作的时候需要分别创建一个输入流和一个输出流)。而一个channel也可写
- channel可异步读写
- 就像上面对channel的介绍,channel(通道)总是基于缓冲区Buffer来读写。
对于不同情况的I/O,channel提供了几种不同的实现
- FileChannel: 用于文件的数据读写
- DatagramChannel: 用于UDP的数据读写
- SocketChannel: 用于TCP的数据读写,一般是客户端实现
- ServerSocketChannel: 允许我们监听TCP链接请求,每个请求会创建会一个SocketChannel,一般是服务器实现
transferTo()——sendFile
Nio
中的channel提供来transferTo()
方法,可以将一个channel
里面的字节直接复制到另一个可以写入字节的channel
中,此方法比从通道读取并写入目标通道的简单循环更有效。操作系统可以直接从文件系统缓存向目标通道传输字节,而无需实际复制它们。transferto内部封装的是sendFile的系统调用
代码示例如下:
SocketChannel socketChannel = SocketChannel.open(); socketChannel.connect(new InetSocketAddress("localhost",8899)); String fileName = "/home/Desktop/text-2019.2.8.tar.gz"; FileInputStream fileInputStream = new FileInputStream(fileName); FileChannel channel = fileInputStream.getChannel(); long transfer = channel.transferTo(0, channel.size(), socketChannel);
流程图示例如下:
流程可以参考sendfile的工作流程。
如果DMA带有收集拷贝功能,那调用transferto方法,则会启用DMA收集拷贝功能的sendfile
流程示例图如下:
可以参考上面带有DMA收集拷贝功能的sendfile实现的I/O的工作流程
但是transferTo()方法因为内部封装了sendfile(),所以无法对文件进行操作,对于无需操作数据的场景适用
map()/MappedByteBuffer——mmap
channel中的map()方法,内部封装了mmap系统调用,是由MappedByteBuffer实现的
所以它的工作流程与特性,与mmap方式是一样的
对于想操作数据又希望提升效率的场景,map()比较适用
流程示例图如下:
代码示例如下:
FileChannel channel = new FileInputStream("").getChannel(); //map方法接受3个参数 //FileChannel.MapMode mode: //Read-only:只读 //Read_write:对结果缓冲区所做的更改最终会传播到文件中;它们可能会或可能不会被映射到同一文件的其他程序 看到。 //privarte:对结果缓冲区所做的更改不会传播到文件,并且对于映射了同一文件的其他程序不可见;相反,它将导 致被修改部分缓冲区独自拷贝一份到用户空间。 //position:映射区域的起始位置。 //size:要映射区域的大小。 MappedByteBuffer map = channel.map(FileChanne.MapMode.READ_ONLY, 0, 2);
channel的map()方法封装的是mmap系统调用,所以会将用户态内存与内核态中文件的内存地址映射,并获取用户态内存地址,构造并返回一个MappedByteBuffer类,里面有各种对文件操作的的API
由于MappedByteBuffer是堆外内存,不受新生代的Minor GC控制,只有在发生Full GC时,才被收回。
DirectByteBuffer继承MappedByteBuffer,实现了DirectBuffer,它改善了对于内存管理的情况,维护了一个Cleaner对象,通过该对象,既可以通过Full GC回收内存,也可以通过主动调用clean()回收内存。
总结
本次对零拷贝进行了较为深入的了解,结合多篇网上的博客,写下了本篇自己对于零拷白的理解,如果你有疑惑,或者问题,欢迎留言,及时交流
参考文档:
https://blog.csdn.net/weixin_38950807/article/details/91374912
https://blog.csdn.net/u013096088/article/details/79122671
https://blog.csdn.net/cringkong/article/details/80274148
https://www.cnblogs.com/snailclimb/p/9086335.html