(轉載)零拷貝的原理及Java實現

在談論Kafka高性能時不得不提到零拷貝。Kafka通過採用零拷貝大大提供了應用性能,減少了內核和用戶模式之間的上下文切換次數。那麼什麼是零拷貝,如何實現零拷貝呢?

什麼是零拷貝?

WIKI中對其有如下定義:

"Zero-copy" describes computer operations in which the CPU does not perform the task of copying data from one memory area to another.

從WIKI的定義中,我們看到“零拷貝”是指計算機操作的過程中,CPU不需要爲數據在內存之間的拷貝消耗資源。而它通常是指計算機在網絡上發送文件時,不需要將文件內容拷貝到用戶空間(User Space)而直接在內核空間(Kernel Space)中傳輸到網絡的方式。

零拷貝給我們帶來的好處

  • 減少甚至完全避免不必要的CPU拷貝,從而讓CPU解脫出來去執行其他的任務
  • 減少內存帶寬的佔用
  • 通常零拷貝技術還能夠減少用戶空間和操作系統內核空間之間的上下文切換

零拷貝的實現

零拷貝實際的實現並沒有真正的標準,取決於操作系統如何實現這一點。零拷貝完全依賴於操作系統。操作系統支持,就有;不支持,就沒有。不依賴Java本身。

傳統I/O

在Java中,我們可以通過InputStream從源數據中讀取數據流到一個緩衝區裏,然後再將它們輸入到OutputStream裏。我們知道,這種IO方式傳輸效率是比較低的。那麼,當使用上面的代碼時操作系統會發生什麼情況:

這是一個從磁盤文件讀取並且通過socket寫出的過程,對應的系統調用如下:

read(file,tmp_buf,len)
write(socket,tmp_buf,len)

這是使用的系統調用方法,這種方式的I/O原理就是將用戶緩衝區(user buffer)的內存地址和內核緩衝區(kernel buffer)的內存地址做一個映射,也就是說系統在用戶態可以直接讀取並操作內核空間的數據。

  1. mmap()系統調用首先會使用DMA的方式將磁盤數據讀取到內核緩衝區,然後通過內存映射的方式,使用戶緩衝區和內核讀緩衝區的內存地址爲同一內存地址,也就是說不需要CPU再講數據從內核讀緩衝區複製到用戶緩衝區。
  2. 當使用write()系統調用的時候,cpu將內核緩衝區(等同於用戶緩衝區)的數據直接寫入到網絡發送緩衝區(socket buffer),然後通過DMA的方式將數據傳入到網卡驅動程序中準備發送。

可以看到這種內存映射的方式減少了CPU的讀寫次數,但是用戶態到內核態的切換(上下文切換)依舊有四次,同時需要注意在進行這種內存映射的時候,有可能會出現併發線程操作同一塊內存區域而導致的嚴重的數據不一致問題,所以需要進行合理的併發編程來解決這些問題。

 

通過sendfile實現的零拷貝I/O

sendfile(socket, file, len);

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

  1. sendfile()系統調用也會引起用戶態到內核態的切換,與內存映射方式不同的是,用戶空間此時是無法看到或修改數據內容,也就是說這是一次完全意義上的數據傳輸過程。
  2. 從磁盤讀取到內存是DMA的方式,從內核讀緩衝區讀取到網絡發送緩衝區,依舊需要CPU參與拷貝,而從網絡發送緩衝區到網卡中的緩衝區依舊是DMA方式。

依舊有一次CPU進行數據拷貝,兩次用戶態和內核態的切換操作,相比較於內存映射的方式有了很大的進步,但問題是程序不能對數據進行修改,而只是單純地進行了一次數據的傳輸過程。

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

  


 

依舊是系統調用sendfile()

sendfile(socket, file, len);

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

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

  1. 系統調用sendfile()發起後,磁盤數據通過DMA方式讀取到內核緩衝區,內核緩衝區中的數據通過DMA聚合網絡緩衝區,然後一齊發送到網卡中。

        可以看到在這種模式下,是沒有一次CPU進行數據拷貝的,所以就做到了真正意義上的零拷貝,雖然和前一種是同一個系統調用,但是這種模式實現起來需要硬件的支持,但對於基於操作系統的用戶來講,操作系統已經屏蔽了這種差異,它會根據不同的硬件平臺來實現這個系統調用

Java的實現

NIO的零拷貝

 

  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的零拷貝由transferTo()方法實現。transferTo()方法將數據從FileChannel對象傳送到可寫的字節通道(如Socket Channel等)。在內部實現中,由native方法transferTo0()來實現,它依賴底層操作系統的支持。在UNIX和Linux系統中,調用這個方法將會引起sendfile()系統調用。

使用場景一般是:

  • 較大,讀寫較慢,追求速度
  • M內存不足,不能加載太大數據
  • 帶寬不夠,即存在其他程序或線程存在大量的IO操作,導致帶寬本來就小

以上都建立在不需要進行數據文件操作的情況下,如果既需要這樣的速度,也需要進行數據操作怎麼辦?
那麼使用NIO的直接內存!

NIO的直接內存

  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());

首先,它的作用位置處於傳統IO(BIO)與零拷貝之間,爲何這麼說?

  • IO,可以把磁盤的文件經過內核空間,讀到JVM空間,然後進行各種操作,最後再寫到磁盤或是發送到網絡,效率較慢但支持數據文件操作。
  • 零拷貝則是直接在內核空間完成文件讀取並轉到磁盤(或發送到網絡)。由於它沒有讀取文件數據到JVM這一環,因此程序無法操作該文件數據,儘管效率很高!

而直接內存則介於兩者之間,效率一般且可操作文件數據。直接內存(mmap技術)將文件直接映射到內核空間的內存,返回==一個操作地址(address)==,它解決了文件數據需要拷貝到JVM才能進行操作的窘境。而是直接在內核空間直接進行操作,省去了內核空間拷貝到用戶空間這一步操作。

NIO的直接內存是由==MappedByteBuffer==實現的。核心即是map()方法,該方法把文件映射到內存中,獲得內存地址addr,然後通過這個addr構造MappedByteBuffer類,以暴露各種文件操作API。

由於MappedByteBuffer申請的是堆外內存,因此不受Minor GC控制,只能在發生Full GC時才能被回收。而==DirectByteBuffer==改善了這一情況,它是MappedByteBuffer類的子類,同時它實現了DirectBuffer接口,維護一個Cleaner對象來完成內存回收。因此它既可以通過Full GC來回收內存,也可以調用clean()方法來進行回收。

另外,直接內存的大小可通過jvm參數來設置:-XX:MaxDirectMemorySize。

NIO的MappedByteBuffer還有一個兄弟叫做HeapByteBuffer。顧名思義,它用來在堆中申請內存,本質是一個數組。由於它位於堆中,因此可受GC管控,易於回收。

參考
https://blog.csdn.net/localhost01/article/details/83422888
https://blog.csdn.net/cringkong/article/details/80274148

 

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