1. 程式人生 > >JavaNIO和零拷貝(Zero Copy)

JavaNIO和零拷貝(Zero Copy)

今天在學習netty的時候,突然瞭解到一個新的概念,零拷貝(Zero Copy),涉及到作業系統中的一些知識,深感自己的知識淺薄,因此特地去學習了一番,如果有謬誤,請大家批評指出。

一.Linux作業系統中的零拷貝

1.1先從Linux的普通I/O過程說起

這裡寫圖片描述

這是一個從磁碟檔案中讀取並且通過Socket寫出的過程,對應的系統呼叫如下。

read(file, tmp_buf, len);
write(socket, tmp_buf, len);
  1. 程式使用read()系統呼叫,系統由使用者態轉換為核心態,磁碟中的資料由DMA(Direct memory access)的方式讀取到核心讀緩衝區(kernel buffer)
    。DMA過程中CPU不需要參與資料的讀寫,而是DMA處理器直接將硬碟資料通過匯流排傳輸到記憶體中。
  2. 系統由核心態轉為使用者態,當程式要讀的資料已經完全存入核心讀緩衝區以後,程式會將資料由核心讀緩衝區,寫入到使用者緩衝區,這個過程需要CPU參與資料的讀寫。
  3. 程式使用write()系統呼叫,系統由使用者態切換到核心態,資料從使用者緩衝區寫入到網路緩衝區(Socket Buffer),這個過程需要CPU參與資料的讀寫。
  4. 系統由核心態切換到使用者態,網路緩衝區的資料通過DMA的方式傳輸到網絡卡的驅動(儲存緩衝區)中(protocol engine)

可以看到,普通的拷貝過程經歷了四次核心態和使用者態的切換(上下文切換),兩次CPU從記憶體中進行資料的讀寫過程,

這種拷貝過程相對來說比較消耗系統資源。

1.2記憶體對映方式I/O

這裡寫圖片描述

tmp_buf = mmap(file, len);
write(socket, tmp_buf, len);

這是使用的系統呼叫方法,這種方式的I/O原理就是將使用者緩衝區(user buffer)的記憶體地址和核心緩衝區(kernel buffer)的記憶體地址做一個對映,也就是說系統在使用者態可以直接讀取並操作核心空間的資料。

  1. mmap()系統呼叫首先會使用DMA的方式將磁碟資料讀取到核心緩衝區,然後通過記憶體對映的方式,使使用者緩衝區和核心讀緩衝區的記憶體地址為同一記憶體地址,也就是說不需要CPU再講資料從核心讀緩衝區複製到使用者緩衝區。
  2. 當使用write()系統呼叫的時候,cpu將核心緩衝區(等同於使用者緩衝區)的資料直接寫入到網路傳送緩衝區(socket buffer),然後通過DMA的方式將資料傳入到網絡卡驅動程式中準備傳送。

可以看到這種記憶體對映的方式減少了CPU的讀寫次數,但是使用者態到核心態的切換(上下文切換)依舊有四次,同時需要注意在進行這種記憶體對映的時候,有可能會出現併發執行緒操作同一塊記憶體區域而導致的嚴重的資料不一致問題,所以需要進行合理的併發程式設計來解決這些問題。

1.3核心空間內部傳輸I/O

這裡寫圖片描述

sendfile(socket, file, len);

通過sendfile()系統呼叫,可以做到核心空間內部直接進行I/O傳輸。

  1. sendfile()系統呼叫也會引起使用者態到核心態的切換,與記憶體對映方式不同的是,使用者空間此時是無法看到或修改資料內容,也就是說這是一次完全意義上的資料傳輸過程。
  2. 從磁碟讀取到記憶體是DMA的方式,從核心讀緩衝區讀取到網路傳送緩衝區,依舊需要CPU參與拷貝,而從網路傳送緩衝區到網絡卡中的緩衝區依舊是DMA方式。

依舊有一次CPU進行資料拷貝,兩次使用者態和核心態的切換操作,相比較於記憶體對映的方式有了很大的進步,但問題是程式不能對資料進行修改,而只是單純地進行了一次資料的傳輸過程。

1.4理想狀態下的零拷貝I/O

這裡寫圖片描述

依舊是系統呼叫sendfile()

sendfile(socket, file, len);

可以看到,這是真正意義上的零拷貝,因為其間CPU已經不參與資料的拷貝過程,也就是說完全通過其他硬體和中斷的方式來實現資料的讀寫過程嗎,但是這樣的過程需要硬體的支援才能實現。

藉助於硬體上的幫助,我們是可以辦到的。之前我們是把頁快取的資料拷貝到socket快取中,實際上,我們僅僅需要把緩衝區描述符傳到socket緩衝區,再把資料長度傳過去,這樣DMA控制器直接將頁快取中的資料打包傳送到網路中就可以了。

  1. 系統呼叫sendfile()發起後,磁碟資料通過DMA方式讀取到核心緩衝區,核心緩衝區中的資料通過DMA聚合網路緩衝區,然後一齊發送到網絡卡中。

可以看到在這種模式下,是沒有一次CPU進行資料拷貝的,所以就做到了真正意義上的零拷貝,雖然和前一種是同一個系統呼叫,但是這種模式實現起來需要硬體的支援,但對於基於作業系統的使用者來講,作業系統已經遮蔽了這種差異,它會根據不同的硬體平臺來實現這個系統呼叫

1.5splice()系統呼叫

splice() 系統呼叫和 sendfile() 非常類似,使用者應用程式必須擁有兩個已經開啟的檔案描述符,一個用於表示輸入裝置,一個用於表示輸出裝置。與 sendfile() 不同的是,splice() 允許任意兩個檔案之間互相連線,而並不只是檔案到 socket 進行資料傳輸。對於從一個檔案描述符傳送資料到 socket 這種特例來說,一直都是使用 sendfile() 這個系統呼叫,而 splice 一直以來就只是一種機制,它並不僅限於 sendfile() 的功能。也就是說,sendfile() 只是 splice() 的一個子集,在 Linux 2.6.23 中,sendfile() 這種機制的實現已經沒有了,但是這個 API 以及相應的功能還存在,只不過 API 以及相應的功能是利用了 splice() 這種機制來實現的。

總體來講splice()是Linux 2.6.23 核心版本中替換sendfile()系統呼叫的一個方法,它不僅支援檔案到Socket的直接傳輸,也支援檔案到檔案的直接傳輸I/O,但是其底層的傳輸過程和sendfile()並無區別,也就是上面那兩張圖。

二.JavaNIO中的零拷貝

真是沒想到對於作業系統中的零拷貝技術要佔這麼多內容,但是不說又不行,因為Java中的零拷貝也是通過作業系統的系統呼叫來實現的。

2.1NIO中記憶體對映方式I/O

首先要說明的是,JavaNIO中的Channel(通道)就相當於作業系統中的核心緩衝區,有可能是讀緩衝區,也有可能是網路緩衝區,而Buffer就相當於作業系統中的使用者緩衝區。

我們來看一段程式碼:

        File file = new File("test.zip");
        RandomAccessFile raf = new RandomAccessFile(file, "rw");
        FileChannel fileChannel = raf.getChannel();
        MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());

NIO中的FileChannel.map()方法其實就是採用了作業系統中的記憶體對映方式,將核心緩衝區的記憶體和使用者緩衝區的記憶體做了一個地址對映。

這種方式適合讀取大檔案,同時也能對檔案內容進行更改,但是如果其後要通過SocketChannel傳送,還是需要CPU進行資料的拷貝。

        processData();
        // 資料處理完成以後,打卡一個SocketChannel
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("", 1234));
        // 這時依舊需要CPU將核心緩衝區的內容拷貝到網路緩衝區
        socketChannel.write(buffer);

2.2NIO中的零拷貝

        File file = new File("test.zip");
        RandomAccessFile raf = new RandomAccessFile(file, "rw");
        FileChannel fileChannel = raf.getChannel();
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("", 1234));
        // 直接使用了transferTo()進行通道間的資料傳輸
        fileChannel.transferTo(0, fileChannel.size(), socketChannel);

這裡寫圖片描述

這種方式就是NIO中的零拷貝,我們來分析一下其中原理:

  1. transferTo()方法直接將當前通道內容傳輸到另一個通道,沒有涉及到Buffer的任何操作,NIO中的Buffer是JVM堆或者堆外記憶體,但不論如何他們都是作業系統核心空間的記憶體。也就是說這種方式不會有核心緩衝區到使用者緩衝區的讀寫問題。
  2. transferTo()的實現方式就是通過系統呼叫sendfile()(當然這是Linux中的系統呼叫),根據我們上面所寫說這個過程是效率遠高於從核心緩衝區到使用者緩衝區的讀寫的。

同理transferFrom()也是這種實現方式。