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)機制

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章