Java高效NIO以內存零拷貝

用戶態和內核態

Linux操做系統的體系架構分爲用戶態(用戶空間)和內核態(內核)。內核從本質上是一種軟件:控制計算機的硬件資源,併爲上層應用程序提供運行環境。
用戶態即上層應用程序的活動空間,應用程序的執行必須依託於內核提供的資源,包括CPU資源、存儲資源、I/O資源等。爲了使上層應用可以訪問到這些資源,內核必須爲上層應用提供訪問的接口,即系統調用
Linux系統體系結構.PNG緩存

系統的資源是有限的,若是不加以管理,必然形成資源過多消耗和訪問衝突。爲了控制關鍵資源的訪問,Linux把程序劃分爲不一樣的執行等級,即特權的概念。x86架構的CPU提供了0到3四個特權級,數字越小,特權越高,Linux操做系統中主要採用了0和3兩個特權級,分別對應的就是內核態用戶態
用戶態的進程能夠執行的操做和訪問的資源都會受到限制;內核態的進程則能夠執行任何操做而且在資源的使用上沒有限制。用戶程序開始時運行於用戶態,但在執行的過程當中,一些操做須要在內核權限下才能執行,就須要經過系統調用把系統從用戶態切換到內核態。好比C語言的內存分配函數malloc(),是經過sbrk()系統調用來分配內存,從malloc到sbrk()的調用就涉及從用戶態到內核態的切換,相似函數printf()調用的是wirte()系統調用。安全

標準I/O(BufferIO)

標準IO又被稱做緩存IO,是大多數文件系統的默認IO操做。在Linux的緩存IO機制中,數據先從磁盤複製到內核空間的緩衝區,而後從內核空間緩衝區複製到應用程序的地址空間。架構

Buffer IO Model.PNG

讀操做:操做系統檢查內核的緩衝區有沒有須要的數據,若是已經緩存了,那麼就直接從內核換成拷貝到用戶程序換成,並返回;不然從磁盤中讀取,先緩存在內核緩存區,再返回。函數

寫操做:將數據從用戶空間複製到內核空間的緩存中。這時對用戶程序來講寫操做就已經完成,至於何時再寫到磁盤中由操做系統決定,除非顯示地調用了sync同步命令。性能

緩存IO的優勢優化

  1. 在必定程度上分離了內核空間和用戶空間,保護系統自己的運行安全
  2. 能夠減小讀盤的次數,從而提升性能

緩存IO的缺點this

在緩存IO機制中,DMA方式能夠將數據直接從磁盤讀到內核緩存中,或者將數據從緩存直接寫回到磁盤上,但不能直接在應用程序地址空間和磁盤之間進行數據傳輸。所以,數據在傳輸過程當中須要在應用程序地址空間和內核緩存空間之間進行屢次數據拷貝操做,這些數據拷貝操做所帶來的CPU以及內存開銷是很是大的。

直接內存訪問(DMA)

傳統的IO操做,由CPU進行控制,CPU經過系統總線與其餘部件鏈接並進行數據傳輸。CPU須要暫停正在執行的程序,去處理IO操做,待數據傳輸處理完後,再繼續執行以前被暫停的工做,此方式IO過程當中消耗大量CPU時間,效率低,適合少許數據傳輸。spa

DMA(Direct Memory Access,直接內存訪問)技術主要是爲了解決批量數據輸入輸出問題,是指外部設備不須要經過CPU而直接與系統內存交互數據的接口技術。DMA工做模式下,在數據準備開始傳輸時,CPU把總線控制權交給DMA控制器,由DMA控制器完成數據的傳輸工做,再把總線控制器交還給CPU。操作系統

傳統IO文件拷貝

有了以上概念,咱們再來看下IO操做的流程,以應用程序(用戶態)從磁盤中拷貝一個文件爲場景進行說明。3d

手到擒來,Java代碼以下:

BufferedInputStream bis = new BufferedInputStream(new FileInputStream(inFile));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(outFile))){
byte[] buf = new byte[1024];
while ((bis.read(buf)) != -1) {
    bos.write(buf);
}

數據流程圖以下:
傳統IO流程.PNG

  1. 應用程序調用read()系統調用,系統切換到Kernel上下文,底層採用DMA讀取磁盤的文件,把內容保存到Kernel地址空間讀緩存區。
  2. 因爲應用程序沒法訪問Kernel地址空間,若是應用程序要訪問數據,須要把內容從Kernel地址空間拷貝到用戶緩存區地址中,Kernel完成數據拷貝後,read()方法返回,系統切換回用戶上下文,此時數據位於應用程序緩存區。
  3. 應用程序再調用write()系統調用,把數據寫入文件。調用write()後,系統切換到Kernel上下文,並把用戶緩存區的數據拷貝到Kernel中的寫入緩存區。
  4. write()方法返回,系統再次切換回用戶上下文。此後由DMA控制器把數據寫入磁盤設備,完成數據寫入。

在上述過程當中,發生了4次數據拷貝和4次系統上下文切換,其中第1次和第4次數據拷貝由DMA控制,不須要CPU參與,第2次和第3次須要CPU的參與。

NIO文件拷貝

因爲在上述過程當中,應用程序並不修改傳輸的數據,因此數據在Kernel和用戶緩存間的來回拷貝以及系統上下文的屢次切換,是能否能夠進行優化,去掉第二、3兩次數據拷貝,是否存在一種「管道」把Read Buffer和Write Buffer直接接在一塊兒?

固然有,NIO中的FileChannel.transferTo()方法給咱們提供了這種實現,Java Doc描述以下(有刪減):

/**
* Transfers bytes from this channel's file to the given writable byte
* channel.
*
* <p> An attempt is made to read up to <tt>count</tt> bytes starting at
* the given <tt>position</tt> in this channel's file and write them to the
* target channel. 
*/
public abstract long transferTo(long position, long count,
                            WritableByteChannel target)
throws IOException;

從Java Doc可知,上述方法就是把兩個Channel對接起來,文件拷貝的具體實現代碼以下:

FileChannel inputChannel = new FileInputStream(inFile).getChannel();
FileChannel outChannel = new FileOutputStream(outFile).getChannel();
//Transfers bytes from this channel's file to the given writable byte channel
inputChannel.transferTo(0, fileChannelInput.size(), outChannel);

數據流程圖以下:
NIO流程.PNG

經過FileChannel的transferTo()方法,實現了把數據中一個可讀的文件管道直接傳輸到另外一個可寫管道,消除了Kernel和用戶緩存間的數據拷貝和系統上下文切換。在Linux底層,方法被傳遞到sendfile()系統調用,實現把數據從一個文件描述符傳輸到了另外一個文件描述符。

Socket IO內存零拷貝

在Linux 2.4以及更高版本的內核中,Socket緩衝區描述符支持gather操做。內核只向Socket傳遞數據的FD(File Descriptor),而不實際拷貝數據,這種方式不但減小上下文切換,同時消除了須要CPU參與的數據拷貝過程。

NIO Socket流程.PNG

此模式下,用戶側仍是調用FileChannel.transferTo()方法,可是Kernel內部實現發生了變化:

  1. transferTo方法調用觸發DMA引擎將文件上下文信息拷貝到內核緩衝區。
  2. 數據不會被拷貝到Socket緩衝區,只有數據的描述符被拷貝到Socket緩衝區。
  3. DMA引擎直接根據FD把數據從內核緩衝區拷貝到NIC緩存,減小了最後一次須要消耗CPU的拷貝操做。
相關文章
相關標籤/搜索