I/O操作—计算机的零拷贝和Netty的零拷贝

零拷贝

零拷贝的拷贝指的是什么拷贝

传统读操作

在这里插入图片描述

传统的读操作:当应用发起一个读取文件的操作时,请求会先经过内核,然后内核去读取磁盘,进行交互,
数据会从磁盘拷贝到内核的缓冲区中,这个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)机制

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值