NIO系列(三) 零拷貝

1.介紹

  在通過IO進行數據讀寫時(例如從文件讀取數據),需要進行多次的數據拷貝,有些拷貝是通過DMA的方式進行的,有些拷貝是CPU 需要從來源把每一片段的資料複製到暫存器,然後把它們再次寫回到新的地方,這種方式效率較低。那所謂的零拷貝就是指在進行IO讀寫時,儘量減少拷貝次數,尤其是cpu拷貝。
  零拷貝主要是由操作系統來支持,和java api無關。

2.概念

   在詳細介紹零拷貝前,先需要了解以下個概念:DMA、NIO Gather & Scatter 和mmap
2.1 DMA
   直接內存訪問(Direct Memory Access,DMA)是計算機科學中的一種內存訪問技術。它允許某些電腦內部的硬件子系統(電腦外設),可以獨立地直接讀寫系統內存,而不需中央處理器(CPU)介入處理 。在同等程度的處理器負擔下,DMA是一種快速的數據傳送方式。很多硬件的系統會使用DMA,包含硬盤控制器、繪圖顯卡、網卡和聲卡。

2.2 Gather & Scatter
  分散讀取(Scatter)指從Channel中讀取的數據“分散”到多個Buffer中。按照緩衝區的順序,從Channel中讀取的數據依次將Buffer填滿。
  聚集寫入(Gather)指將多個Buffer中的數據“聚集”到Channel中。按照緩衝區的順序,寫入position和limit之間的數據到Channel中去。

2.3 mmap
  mmap是一種內存映射文件的方法,即將一個文件或者其它對象映射到進程的地址空間,實現文件磁盤地址和進程虛擬地址空間中一段虛擬地址的一一對映關係。實現這樣的映射關係後,進程就可以採用指針的方式讀寫操作這一段內存,而系統會自動回寫髒頁面到對應的文件磁盤上,即完成了對文件的操作而不必再調用read,write等系統調用函數。相反,內核空間對這段區域的修改也直接反映用戶空間,從而可以實現不同進程間的文件共享。如下圖所示:
在這裏插入圖片描述

3.傳統IO

  考慮如下場景:從磁盤讀取數據然後把數據通過網絡發送,此場景通過傳統IO實現,僞代碼如下:

	InputStream inputStream = new FileInputStream("xxxx/xxx.txt");
	DataOutputStream outputStream = new DataOutputStream(socket.getOutputStream());
	while((readCount = inputStream.read(buffer)) >= 0){
       outputStream.write(buffer);
    }

   這些代碼在操作系統層面執行的過程如下圖:
在這裏插入圖片描述
  1) JVM發出read() 系統調用。
  2) OS上下文切換到內核模式(第一次上下文切換)並將數據讀取到內核空間緩衝區。(第一次拷貝:hardware —-> kernel buffer)
  3) OS內核然後將數據複製到用戶空間緩衝區(第二次拷貝: kernel buffer ——> user buffer),然後read系統調用返回。而系統調用的返回又會導致一次內核空間到用戶空間的上下文切換(第二次上下文切換)。
  4) JVM處理代碼邏輯併發送write()系統調用。
  5) OS上下文切換到內核模式(第三次上下文切換)並從用戶空間緩衝區複製數據到內核空間緩衝區(第三次拷貝: user buffer ——> kernel buffer)。
  6) write系統調用返回,導致內核空間到用戶空間的再次上下文切換(第四次上下文切換)。將內核空間緩衝區中的數據寫到hardware(第四次拷貝: kernel buffer ——> hardware)。

  總的來說,傳統的I/O操作進行了4次用戶空間與內核空間的上下文切換,以及4次數據拷貝。顯然在這個用例中,從內核空間到用戶空間內存的複製是完全不必要的,因爲除了將數據轉儲到不同的buffer之外,我們沒有做任何其他的事情。所以,我們能不能直接從hardware讀取數據到kernel buffer後,再從kernel buffer寫到目標地點不就好了。爲了解決這種不必要的數據複製,操作系統出現了零拷貝的概念。注意,不同的操作系統對零拷貝的實現各不相同。在這裏我們介紹linux下的零拷貝實現。

4. mmap零拷貝方式

  通過mmap方式實現上述的場景,僞代碼如下:

	MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0,fileChannel.size());
	DataOutputStream outputStream = new DataOutputStream(socket.getOutputStream());
	outputStream.write(mappedByteBuffer);

  操作系統層面的流程如下:
在這裏插入圖片描述
  通過上圖看到,一共發生了 4 次的上下文切換,3 次的 I/O 拷貝,包括 2 次 DMA 拷貝和 1 次的 I/O 拷貝,相比於傳統 IO 減少了一次CPU拷貝。使用 mmap() 讀取文件時,只會發生第一次從磁盤數據拷貝到 OS 文件系統緩衝區的操作。

5. SendFile零拷貝方式

  通過SendFile方式實現上述的場景,僞代碼如下:

	SocketChannel socketChannel = SocketChannel.open();
	FileChannel fileChannel = new FileInputStream("xxxx/xxx.txt").getChannel();
	fileChannel.transferTo(0,fileChannel.size(),socketChannel);

  linux2.4版本前操作系統層面的流程如下:
在這裏插入圖片描述
  1) 發出sendfile系統調用,導致用戶空間到內核空間的上下文切換(第一次上下文切換)。通過DMA將磁盤文件中的內容拷貝到內核空間緩衝區中(第一次拷貝: hard driver ——> kernel buffer)。
  2) 然後再將數據從內核空間緩衝區拷貝到內核中與socket相關的緩衝區中(第二次拷貝: kernel buffer ——> socket buffer)。
  3) sendfile系統調用返回,導致內核空間到用戶空間的上下文切換(第二次上下文切換)。通過DMA引擎將內核空間socket緩衝區中的數據傳遞到協議引擎(第三次拷貝: socket buffer ——> protocol engine)。

  通過sendfile實現的零拷貝I/O只使用了2次用戶空間與內核空間的上下文切換,以及3次數據的拷貝。你可能會說操作系統仍然需要在內核內存空間中複製數據(kernel buffer —>socket buffer)。 是的,但從操作系統的角度來看,這已經是零拷貝,因爲沒有數據從內核空間複製到用戶空間。 內核需要複製的原因是因爲通用硬件DMA訪問需要連續的內存空間(因此需要緩衝區)。 但是,如果硬件支持scatter-and-gather,這是可以避免的。

linux2.4版本後(支持scatter-and-gather)操作系統層面的流程如下:
在這裏插入圖片描述
  1) 發出sendfile系統調用,導致用戶空間到內核空間的上下文切換(第一次上下文切換)。通過DMA引擎將磁盤文件中的內容拷貝到內核空間緩衝區中(第一次拷貝: hard drive —> kernel buffer)。
  2) 沒有數據拷貝到socket緩衝區。取而代之的是隻有相應的描述符信息會被拷貝到相應的socket緩衝區當中。該描述符包含了兩方面的信息:a)kernel buffer的內存地址;b)kernel buffer的偏移量。(注意:這個時候kernel buffer存儲了所有的數據內容,socket buffer存儲了數據的位置索引,後續protocol engine進行dma拷貝時,會從兩個buffer去讀,這也就是nio的gather語法)
  3) sendfile系統調用返回,導致內核空間到用戶空間的上下文切換(第二次上下文切換)。DMA gather copy根據socket緩衝區中描述符提供的位置和偏移量信息直接將內核空間緩衝區中的數據拷貝到協議引擎上(第二次拷貝: kernel buffer ——> protocol engine),這樣就避免了最後一次CPU數據拷貝。
  4) 帶有DMA收集拷貝功能的sendfile實現的I/O只使用了2次用戶空間與內核空間的上下文切換,以及2次數據的拷貝,而且這2次的數據拷貝都是非CPU拷貝。這樣一來我們就實現了最理想的零拷貝I/O傳輸了,不需要任何一次的CPU拷貝,以及最少的上下文切換。

6.總結

  零拷貝是操作系統底層的一種實現,我們在網絡編程中,利用操作系統這一特性,可以大大提高數據傳輸的效率。這也是目前網絡編程框架中都會採用的方式,理解好零拷貝,有助於我們進一步學習Netty等網絡通信框架的底層原理。

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