零拷贝与java中的零拷贝
https://www.jianshu.com/p/497e7640b57c
https://xie.infoq.cn/article/f9f75882748a15f628d8dee6b
什么是零拷贝?
零拷贝(英语: Zero-copy) 技术是指计算机执行操作时,CPU不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省CPU周期和内存带宽。
➢零拷贝技术可以减少数据拷贝和共享总线操作的次数,消除传输数据在存储器之间不必要的中间拷贝次数,从而有效地提高数据传输效率
➢零拷贝技术减少了用户进程地址空间和内核地址空间之间因为上:下文切换而带来的开销可以看出没有说不需要拷贝,只是说减少冗余[不必要]的拷贝
传统IO流程
比如:读取文件,再用socket发送出去
传统方式实现:
先读取、再发送,实际经过1~4四次copy。
buffer = File.read
Socket.send(buffer)
1、第一次:将磁盘文件,读取到操作系统内核缓冲区;
2、第二次:将内核缓冲区的数据,copy到application应用程序的buffer;
3、第三步:将application应用程序buffer中的数据,copy到socket网络发送缓冲区(属于操作系统内核的缓冲区);
4、第四次:将socket buffer的数据,copy到网卡,由网卡进行网络传输。
传统方式,读取磁盘文件并进行网络发送,经过的四次数据copy是非常繁琐的。实际IO读写,需要进行IO中断,需要CPU响应中断(带来上下文切换),尽管后来引入DMA来接管CPU的中断请求,但四次copy是存在“不必要的拷贝”的。
重新思考传统IO方式,会注意到实际上并不需要第二个和第三个数据副本。应用程序除了缓存数据并将其传输回套接字缓冲区之外什么都不做。相反,数据可以直接从读缓冲区传输到套接字缓冲区。
显然,第二次和第三次数据copy 其实在这种场景下没有什么帮助反而带来开销,这也正是零拷贝出现的背景和意义。
传统数据传送所消耗的成本:4次拷贝,4次上下文切换。
4次拷贝,其中两次是DMA copy,两次是CPU copy。如下图所示
拷贝是个IO过程,需要系统调用。
注意一点的是 内核从磁盘上面读取数据 是 不消耗CPU时间的,是通过磁盘控制器完成;称之为DMA Copy。
网卡发送也用DMA
Linux支持的(常见)零拷贝
一、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.
DMA加载磁盘数据到kernel buffer后,应用程序缓冲区(application buffers)和内核缓冲区(kernel buffer)进行映射,数据再应用缓冲区和内核缓存区的改变就能省略。
mmap内存映射将会经历:3次拷贝: 1次cpu copy,2次DMA copy;
以及4次上下文切换
二、sendfile
linux 2.1支持的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.
当调用sendfile()时,DMA将磁盘数据复制到kernel buffer,然后将内核中的kernel buffer直接拷贝到socket buffer;
一旦数据全都拷贝到socket buffer,sendfile()系统调用将会return、代表数据转化的完成。
socket buffer里的数据就能在网络传输了。
sendfile会经历:3次拷贝,1次CPU copy 2次DMA copy;
以及2次上下文切换
三、Sendfile With DMA Scatter/Gather Copy
Then by using the DMA scatter/gather operation, the network interface card can gather all the data from different memory locations and store the assembled packet in the network card buffer.
Scatter/Gather可以看作是sendfile的增强版,批量sendfile。
Scatter/Gather会经历2次拷贝: 0次cpu copy,2次DMA copy
IO请求批量化
DMA scatter/gather:需要DMA控制器支持的。
DMA工作流程:cpu发送IO请求给DMA,DMA然后读取数据。
IO请求:相当于可以看作包含一个物理地址。
从一系列物理地址(10)读数据:普通的DMA (10请求)
dma scatter/gather:一次给10个物理地址, 一个请求就可以(批量处理)。
4、splice
Linux 2.6.17 支持splice
it does not need to copy data between kernel space and user space.
When using this approach, data are copied from disk to kernel buffer first. Then the splice() system call allows data to move between different buffers in kernel space without the copy to user space.
Unlike the method sendfile() with DMA scatter/gather copy, splice() does not need support from hardware.
数据从磁盘读取到OS内核缓冲区后,在内核缓冲区直接可将其转成内核空间其他数据buffer,而不需要拷贝到用户空间。
如下图所示,从磁盘读取到内核buffer后,在内核空间直接与socket buffer建立pipe管道。
和sendfile()不同的是,splice()不需要硬件支持。
注意splice和sendfile的不同,sendfile是将磁盘数据加载到kernel buffer后,需要一次CPU copy,拷贝到socket buffer。
而splice是更进一步,连这个CPU copy也不需要了,直接将两个内核空间的buffer进行set up pipe。
splice会经历 2次拷贝: 0次cpu copy 2次DMA copy;
以及2次上下文切换
Linux零拷贝机制对比
无论是传统IO方式,还是引入零拷贝之后,2次DMA copy 是都少不了的。因为两次DMA都是依赖硬件完成的。
Java-NIO提供的内存映射 MappedByteBuffer
- 首先要说明的是,JavaNlO中 的Channel (通道)就相当于操作系统中的内核缓冲区,有可能是读缓冲区,也有可能是网络缓冲区,而Buffer就相当于操作系统中的用户缓冲区。
MappedByteBuffer mappedByteBuffer = new RandomAccessFile(file, "r")
.getChannel()
.map(FileChannel.MapMode.READ_ONLY, 0, len);
底层就是调用Linux mmap()实现的。
NIO中的FileChannel.map()方法其实就是采用了操作系统中的内存映射方式,底层就是调用Linux mmap()实现的。
将内核缓冲区的内存和用户缓冲区的内存做了一个地址映射。这种方式适合读取大文件,同时也能对文件内容进行更改,但是如果其后要通过SocketChannel发送,还是需要CPU进行数据的拷贝。
使用MappedByteBuffer,小文件,效率不高;一个进程访问,效率也不高。
MappedByteBuffer只能通过调用FileChannel的map()取得,再没有其他方式。
FileChannel.map()是抽象方法,具体实现是在 FileChannelImpl.c 可自行查看JDK源码,其map0()方法就是调用了Linux内核的mmap的API。
使用 MappedByteBuffer类要注意的是:mmap的文件映射,在full gc时才会进行释放。当close时,需要手动清除内存映射文件,可以反射调用sun.misc.Cleaner方法。
Java-NIO提供的sendfile
- FileChannel.transferTo()方法直接将当前通道内容传输到另一个通道,没有涉及到Buffer的任何操作,NIO中 的Buffer是JVM堆或者堆外内存,但不论如何他们都是操作系统内核空间的内存
- transferTo()的实现方式就是通过系统调用sendfile() (当然这是Linux中的系统调用)
//使用sendfile:读取磁盘文件,并网络发送
FileChannel sourceChannel = new RandomAccessFile(source, "rw").getChannel();
SocketChannel socketChannel = SocketChannel.open(sa);
sourceChannel.transferTo(0, sourceChannel.size(), socketChannel);
ZeroCopyFile实现文件复制
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 这样的零拷贝系统调用,则这两个方法会通过这样的系统调用充分利用零拷贝的优势,否则并不能通过这两个方法本身实现零拷贝。