1、什么是零拷貝
維基上是這么描述零拷貝的:零拷貝描述的是CPU不執行拷貝數據從一個存儲區域到另一個存儲區域的任務,這通常用于通過網絡傳輸一個文件時以減少CPU周期和內存帶寬。
2、零拷貝給我們帶來的好處
- 1、減少甚至完全避免不要的CPU拷貝,讓CPU解脫出來去執行其他的任務
- 2、減少內存帶寬的占用
- 3、通常零拷貝技術還能夠減少用戶空間和操作系統內核空間的上下文切換
3、 零拷貝的實現
- 實際的實現并沒有真正的標準,取決于操作系統如何實現這一點,零拷貝本質完全依賴操作系統,操作系統支持就有,不支持就沒有,不依賴Java本身(HeapByteBuffer,DirectByteBuffer),NIO只是解決部分拷貝過程、
- 零拷貝實現了內存的數據重用性 減少拷貝過程次數來提高性能
4、傳統I/O
-
在Java中,我們可以通過InputStream從源數據中讀取數據流到一個緩沖區里,然后再將它們輸入到OutputStream里。我們知道,這種IO方式傳輸效率是比較低的。那么,當使用上面的代碼時操作系統會發生什么情況:
傳統I/O
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下的零拷貝實現。
5、通過sendfile實現的零拷貝I/O
- sendfile
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,這是可以避免的。
6、帶有DMA收集拷貝功能的sendfile實現的I/O
1、發出sendfile系統調用,導致用戶空間到內核空間的上下文切換(第一次上下文切換)。通過DMA引擎將磁盤文件中的內容拷貝到內核空間緩沖區中(第一次拷貝: hard drive —> kernel buffer)。
2、沒有數據拷貝到socket緩沖區。取而代之的是只有相應的描述符信息會被拷貝到相應的socket緩沖區當中。該描述符包含了兩方面的信息:a)kernel buffer的內存地址;b)kernel buffer的偏移量。
3、sendfile系統調用返回,導致內核空間到用戶空間的上下文切換(第二次上下文切換)。DMA gather copy根據socket緩沖區中描述符提供的位置和偏移量信息直接將內核空間緩沖區中的數據拷貝到協議引擎上(第二次拷貝: kernel buffer ——> protocol engine),這樣就避免了最后一次CPU數據拷貝。
4、帶有DMA收集拷貝功能的sendfile實現的I/O只使用了2次用戶空間與內核空間的上下文切換,以及2次數據的拷貝,而且這2次的數據拷貝都是非CPU拷貝。這樣一來我們就實現了最理想的零拷貝I/O傳輸了,不需要任何一次的CPU拷貝,以及最少的上下文切換。
許多Web服務器都支持零拷貝,如Tomcat和Apache。 例如Apache的相關文檔可以在這里找到,但默認情況下關閉。
注意:Java的NIO通過transferTo()提供了這個功能。
- 傳統I/O用戶空間緩沖區中存有數據,因此應用程序能夠對此數據進行修改等操作;而sendfile零拷貝消除了所有內核空間緩沖區與用戶空間緩沖區之間的數據拷貝過程,因此sendfile零拷貝I/O的實現是完成在內核空間中完成的,這對于應用程序來說就無法對數據進行操作了。為了解決這個問題,Linux提供了mmap零拷貝來實現我們的需求。
7 通過mmap實現的零拷貝I/O
-
mmap(內存映射)是一個比sendfile昂貴但優于傳統I/O的方法。
mmap 1、發出mmap系統調用,導致用戶空間到內核空間的上下文切換(第一次上下文切換)。通過DMA引擎將磁盤文件中的內容拷貝到內核空間緩沖區中(第一次拷貝: hard drive ——> kernel buffer)。
2、mmap系統調用返回,導致內核空間到用戶空間的上下文切換(第二次上下文切換)。接著用戶空間和內核空間共享這個緩沖區,而不需要將數據從內核空間拷貝到用戶空間。因為用戶空間和內核空間共享了這個緩沖區數據,所以用戶空間就可以像在操作自己緩沖區中數據一般操作這個由內核空間共享的緩沖區數據。
3、發出write系統調用,導致用戶空間到內核空間的上下文切換(第三次上下文切換)。將數據從內核空間緩沖區拷貝到內核空間socket相關聯的緩沖區(第二次拷貝: kernel buffer ——> socket buffer)。
4、write系統調用返回,導致內核空間到用戶空間的上下文切換(第四次上下文切換)。通過DMA引擎將內核空間socket緩沖區中的數據傳遞到協議引擎(第三次拷貝: socket buffer ——> protocol engine)
通過mmap實現的零拷貝I/O進行了4次用戶空間與內核空間的上下文切換,以及3次數據拷貝。其中3次數據拷貝中包括了2次DMA拷貝和1次CPU拷貝。明顯,它與傳統I/O相比僅僅少了1次內核空間緩沖區和用戶空間緩沖區之間的CPU拷貝。這樣的好處是,我們可以將整個文件或者整個文件的一部分映射到內存當中,用戶直接對內存中對文件進行操作,然后是由操作系統來進行相關的頁面請求并將內存的修改寫入到文件當中。我們的應用程序只需要處理內存的數據,這樣可以實現非常迅速的I/O操作。
8、NIO DirectByteBuffer
- Java NIO引入了用于通道的緩沖區的ByteBuffer。 ByteBuffer有三個主要的實現:HeapByteBuffer、DirectByteBuffer、MappedByteBuffer
類名 | 類描述說明 |
---|---|
HeapByteBuffer | 堆緩沖區 |
DirectByteBuffer | 直接緩沖區 |
MappedByteBuffer | 映射緩沖區 |
HeapByteBuffer
-
在調用ByteBuffer.allocate()時使用。 它被稱為堆,因為它保存在JVM的堆空間中,因此你可以獲得所有優勢,如GC支持和緩存優化。 但是,它不是頁面對齊的,這意味著如果你需要通過JNI與本地代碼交談,JVM將不得不復制到對齊的緩沖區空間。
HeapByteBuffer
DirectByteBuffer
-
在調用ByteBuffer.allocateDirect()時使用。 JVM將使用malloc()在堆空間之外分配內存空間。 因為它不是由JVM管理的,所以你的內存空間是頁面對齊的,不受GC影響,這使得它成為處理本地代碼的完美選擇。 然而,你要C程序員一樣,自己管理這個內存,必須自己分配和釋放內存來防止內存泄漏。
DirectByteBuffer
MappedByteBuffer
-
在調用FileChannel.map()時使用。 與DirectByteBuffer類似,這也是JVM堆外部的情況。 它基本上作為OS mmap()系統調用的包裝函數,以便代碼直接操作映射的物理內存數據
MappedByteBuffer
9 總結
- 零拷貝是操作系統底層的一種實現,我們在網絡編程中,利用操作系統這一特性,可以大大提高數據傳輸的效率。這也是目前網絡編程框架中都會采用的方式,理解好零拷貝,有助于我們進一步學習Netty等網絡通信框架的底層原理。
- 以上文章來自 Java NIO學習筆記四(零拷貝詳解)進行個人整理
推薦看堆外內存、零拷貝、DirectByteBuffer