零拷贝, DMA(Direct Memory Access) copy, 即直接内存拷贝(不使用CPU拷贝), 是网络编程的关键,很多IO相关的性能优化都离不开。
传统数据copy方式
经过了4次拷贝和3次状态切换, 其中有2次DMA Copy
步骤
- 从硬盘中读取数据, 使用DMA Copy拷贝到内核buffer中
- 再使用CPU Copy将数据从内核buffer中拷贝到用户buffer
- 再使用CPU Copy将数据从用户buffer拷贝到socket buffer
- 再使用DMA Copy将数据从socket buffer拷贝到协议栈中
mmap优化
经过了3次拷贝和3次状态切换, 其中有2次DMA copy
原理
memory map(内存映射), 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户控件的拷贝次数。
步骤
- 从硬盘中读取数据, 使用DMA Copy拷贝到内核buffer中
- 使用mmap将文件映射到内核buffer中和用户buffer共享, 无需再次拷贝到用户buffer中
- 再使用CPU Copy将数据从用户buffer拷贝到socket buffer
- 再使用DMA Copy将数据从socket buffer拷贝到协议栈中
sendFile优化
Linux 2.4 -
经过了3次拷贝和2次状态切换, 其中有2次DMA copy
原理
Linux在2.4版本之前提供的 sendFile 函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer,同时,由于和用户态完全无关,就减少了一次上下文切换。
步骤
- 从硬盘中读取数据, 使用DMA Copy拷贝到内核buffer中
- 再使用CPU Copy将数据从内核buffer直接拷贝到socket buffer
- 再使用DMA Copy将数据从socket buffer拷贝到协议栈中
Linux 2.4 +
经过了2次拷贝, 其中近似没有CPU copy (可以看作完全实现了零拷贝)
原理
相对于之前Linux版本中的sendFile做了一些修改,极大地避免了从内核缓冲区CPU Copy到 Socket buffer 的操作(这里其实会有一次cpu拷贝,但是拷贝的信息很少, 主要是一些描述信息, 比如lenght , offset, 这些信息会从kernel buffer拷贝到socket buffer, 但是过程中的消耗非常低,可以忽略),直接拷贝到协议栈,从而再一次减少了数据拷贝。
Java Nio 零拷贝
使用
transferTo()
/transferFrom()
函数
原理
/*
* <p> This method is potentially much more efficient than a simple loop
* that reads from the source channel and writes to this channel. Many
* operating systems can transfer bytes directly from the source channel
* into the filesystem cache without actually copying them. </p>
*
*/
public abstract long transferFrom(ReadableByteChannel src, long position, long count) throws IOException;
许多操作系统可以直接将字节从源通道转移到文件系统缓存中,而无需实际复制它们, 而transferTo()
/transferFrom()
函数底层使用MappedByteBuffer
来实现, 可以让文件直接在内存(堆外内存)中进行修改, 操作系统不需要再拷贝一次, MappedByteBuffer
操作内存相关代码请点击查看。
代码
public static void transferFrom() {
File sourceFile = new File("./source.log");
File targetFile = new File("./target1.log");
try (
FileInputStream fileInputStream = new FileInputStream(sourceFile);
FileChannel fileInputStreamChannel = fileInputStream.getChannel();
FileOutputStream fileOutputStream = new FileOutputStream(targetFile);
FileChannel fileOutputStreamChannel = fileOutputStream.getChannel();
) {
fileOutputStreamChannel.transferFrom(fileInputStreamChannel, 0, fileInputStreamChannel.size());
} catch (Exception e) {
e.printStackTrace();
}
}