零拷贝
零拷贝的拷贝指的是什么拷贝
传统读操作
传统的读操作:当应用发起一个读取文件的操作时,请求会先经过内核,然后内核去读取磁盘,进行交互,
数据会从磁盘拷贝到内核的缓冲区中,这个copy动作由DMA完成,整个过程基本上不消耗CPU,
但是当数据拷贝到系统的内存空间后,从系统的内存空间拷贝到应用的空间时,这里就是一个CPU copy的动作,
将数据从内核缓冲区中拷贝到应用缓冲区中这个copy动作时需要消耗CPU的。
如上所述:整个读取过程由两次拷贝动作,一次DMAcopy和一次CPUcopy
DMA
硬件和软件的信息传输,可以使用DMA(Direct Memory Access)来完成
传统的写操作
传统的写操作:应用想将这些数据传递给客户端,必须经过内核,将数据先从应用缓冲区中copy(CPUcopy)到prorocol engine,
并最终将数据发送给客户端。
如上所述:这里又发生2次copy动作,一次是CPU copy另一次是DMA copy。
综上所述:一次完整的传统读写操作,期间要发生四次的数据copy动作,2次CPU 拷贝,2次DMA 拷贝,
应用程序就相当一个中间人的角色。
我们知道:CPU是电脑的处理核心,既然是核心他就应该尽可能的参与到计算中去,而不是来做繁杂又耗时的数据拷贝工作,
所以我们应该避免CPU 拷贝的出现,到这里我想大家应该也明白了,零拷贝中的拷贝是什么拷贝了,没错就是 CPU拷贝。
那有没有办法将其中的2次CPU copy去掉呢?因为我们总是希望CPU能处理更多的事情,而不是浪费在这种无所谓的Copy中去。
答案就是:利用各种硬件以及操作系统内核,进行数据零拷贝。
计算机的零拷贝和Netty的零拷贝理解
计算机的零拷贝
我们看到“零拷贝”是指计算机操作的过程中,CPU不需要为数据在内存之间的拷贝消耗资源。而它通常是指计算机在网络上发送文件时,
不需要将文件内容拷贝到用户空间(User Space)而直接在内核空间(Kernel Space)中传输到网络的方式
从上图中可以清楚的看到,Zero Copy的模式中,避免了数据在用户空间和内存空间之间的拷贝,从而提高了系统的整体性能。
Linux中的sendfile()以及Java NIO中的FileChannel.transferTo()方法都实现了零拷贝的功能,
而在Netty中也通过在FileRegion中包装了NIO的FileChannel.transferTo()方法实现了零拷贝
Netty的零拷贝
- Netty 提供了 CompositeByteBuf 类, 它可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf, 避免了各个 ByteBuf 之间的拷贝.
- 通过 wrap 操作, 我们可以将 byte[] 数组、ByteBuf、ByteBuffer等包装成一个 Netty ByteBuf 对象, 进而避免了拷贝操作
- ByteBuf 支持 slice 操作, 因此可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf, 避免了内存的拷贝
- 通过 FileRegion 包装的FileChannel.tranferTo 实现文件传输, 可以直接将文件缓冲区的数据发送到目标 Channel, 避免了传统通过循环 write 方式导致的内存拷贝问题.
CompositeByteBuf
顾名思义:就是将多个真实的buffer合并成一个抽象的buffer,什么意思呢?就是这个CompositeByteBuffer里面有一个buffer类型的数组,
多个buffer就被抽象成了一个buffer,这样在操作CompositeByteBuffer的时候就像操作一个buffer一样,
从而避免了将多个buffer合并成一个新的buffer造成的内存拷贝。
Wrap
比如我们想将一个byte数组转换成一个ByteBuffer对象,以便后续操作,传统的做法就是将byte数组拷贝进ByteBuffer中
即:
byte[] bytes=...
ByteBuffer byteBuffer = Unpooled.buffer();
byteBuffer.writeBytes(bytes);
很显然上面的操作是包括了一个数据拷贝的操作,为了避免这个操作我们可以通过包装(Wrap)的方式,
将byte数组包装成一个ByteBuffer对象,从而避免数据拷贝。
即:
byte[] bytes = ...
ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes);
可以看到, 我们通过 Unpooled.wrappedBuffer 方法来将 bytes 包装成为一个 UnpooledHeapByteBuf 对象, 而在包装的过程中,
是不会有拷贝操作的. 即最后我们生成的生成的 ByteBuf 对象是和 bytes 数组共用了同一个存储空间,
对 bytes 的修改也会反映到 ByteBuf 对象中
Slice
slice 操作和 wrap 操作刚好相反, Unpooled.wrappedBuffer 可以将多个 ByteBuf 合并为一个,
而 slice 操作可以将一个 ByteBuf 切片 为多个共享一个存储区域的 ByteBuf 对象.
在进行切片的时候,并没有发生内存copy,只是指向了同一块内存的不同部分。
FileRegion
Netty 中使用 FileRegion 实现文件传输的零拷贝, 不过在底层 FileRegion 是依赖于 Java NIO FileChannel.transfer 的零拷贝功能.
传统的文件拷贝:
public static void copyFile(String srcFile, String destFile) throws Exception {
byte[] temp = new byte[1024];
FileInputStream in = new FileInputStream(srcFile);
FileOutputStream out = new FileOutputStream(destFile);
int length;
while ((length = in.read(temp)) != -1) {
out.write(temp, 0, length);
}
in.close();
out.close();
}
我们看到上面的代码块里面,存在内存拷贝,小文件还行,要是大文件拷贝的话,这个内存拷贝发生的次数将会很大。
NIO的FileChannel做文件拷贝:
public static void copyFileWithFileChannel(String srcFileName, String destFileName) throws Exception {
RandomAccessFile srcFile = new RandomAccessFile(srcFileName, "r");
FileChannel srcFileChannel = srcFile.getChannel();
RandomAccessFile destFile = new RandomAccessFile(destFileName, "rw");
FileChannel destFileChannel = destFile.getChannel();
long position = 0;
long count = srcFileChannel.size();
srcFileChannel.transferTo(position, count, destFileChannel);
}
我们可以看到使用FileChannel后,我们可以直接将源文件的内容直接拷贝(TransferTo)到目的文件中,而不需要借助一个临时的Buffer,避免了不必要的内存操作。
FileRegion是如何零拷贝传输一个文件的:
@Override
public void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
RandomAccessFile raf = null;
long length = -1;
try {
// 1. 通过 RandomAccessFile 打开一个文件.
raf = new RandomAccessFile(msg, "r");
length = raf.length();
} catch (Exception e) {
ctx.writeAndFlush("ERR: " + e.getClass().getSimpleName() + ": " + e.getMessage() + '\n');
return;
} finally {
if (length < 0 && raf != null) {
raf.close();
}
}
ctx.write("OK: " + raf.length() + '\n');
if (ctx.pipeline().get(SslHandler.class) == null) {
// SSL not enabled - can use zero-copy file transfer.
// 2. 调用 raf.getChannel() 获取一个 FileChannel.
// 3. 将 FileChannel 封装成一个 DefaultFileRegion
ctx.write(new DefaultFileRegion(raf.getChannel(), 0, length));
} else {
// SSL enabled - cannot use zero-copy file transfer.
ctx.write(new ChunkedFile(raf));
}
ctx.writeAndFlush("\n");
}
可以看到,我们通过RandomAccessFile打开一个文件,然后使用DefaultFileRegion来封装一个FileChannel即:
new DefaultFileRegion(raf.getChannel(), 0, length)
当有了 FileRegion 后, 我们就可以直接通过它将文件的内容直接写入 Channel 中, 而不需要像传统的做法: 拷贝文件内容到临时 buffer, 然后再将 buffer 写入 Channel. 通过这样的零拷贝操作, 无疑对传输大文件很有帮助
参考链接如下:
对于 Netty ByteBuf 的零拷贝(Zero Copy) 的理解
理解Netty中的零拷贝(Zero-Copy)机制