零復制概念:
“ 零復制”描述了計算機操作,其中CPU 不執行將數據從一個存儲區復制到另一個存儲區的任務。通過網絡傳輸文件時,通常用於節省CPU周期和內存帶寬。
WIKI的定義中,我們看到 “零復制” 是指計算機操作的過程,不需要消耗CPU資源來在內存之間進行數據復制。它通常是指計算機在網絡上發送文件時,不需要將文件的內容復制到用戶空間並將其直接傳輸到內核空間中的網絡的方式。
① 非零副本(傳統的數據復制方法):
→ :CPU Copy(慢)
→ :DMA(直接內存訪問) Copy(快)
下面看傳統數據的復制方法以及其上下文的切換
圖1. 傳統的數據復制方法
圖2. 傳統的上下文切換
-
-
請求的數據量從讀取緩沖區復制到用戶緩沖區,然后 read() 調用返回。調用返回導致另一個上下文從內核切換回用戶模式。現在,數據存儲在用戶地址空間緩沖區中。
-
該 send() 插座調用導致從用戶模式到內核模式的上下文切換。執行第三次復制以再次將數據放入內核地址空間緩沖區。但是,這次,數據被放到了另一個緩沖區中,該緩沖區與目標套接字相關聯。
-
該 send() 系統調用返回,創造了第四上下文切換。獨立且異步地,當 DMA 引擎將數據從內核緩沖區傳遞到協議引擎時,發生第四次復制。
使用中間內核緩沖區(而不是將數據直接傳輸到用戶緩沖區中)似乎無效。但是,將中間內核緩沖區引入了該過程以提高性能。在讀取側使用中間緩沖區可以使內核緩沖區充當“預讀緩存”,而應用程序所需要的數據卻不如內核緩沖區所需的那么多。當請求的數據量小於內核緩沖區大小時,這將顯着提高性能。寫側的中間緩沖區允許寫異步完成。
不幸的是,如果請求的數據大小比內核緩沖區的大小大得多,則此方法本身可能會成為性能瓶頸。數據在最終交付給應用程序之前,已在磁盤,內核緩沖區和用戶緩沖區之間多次復制。
零復制通過消除這些冗余數據副本來提高性能。
② 零復制方法:
從上圖可以清楚地看到,在零復制模式下,避免了用戶空間和內存空間之間的數據復制,從而提高了系統的整體性能。
程序訪問方式
-
Linux內核通過各種系統調用(例如 sys/socket.h 的 sendfile,sendfile64 和 splice)支持零復制。其中一些是在 POSIX 中指定的,因此也存在於 BSD 內核或 IBM AIX 中,其中一些是 Linux 內核 API 特有的。
-
Microsoft Windows 通過 TransmitFile API 支持零復制。
-
如果基礎操作系統也支持零拷貝,則Java輸入流可以通過 java.nio.channels.FileChannel 的 transferTo() 方法支持零拷貝。
1 public abstract long transferTo(long position, long count, WritableByteChannel target) throws IOException;
圖3. 用transferTo()復制數據
圖4. 使用transferTo() 進行上下文切換
-
-
第三份副本發生在DMA引擎將數據從內核套接字緩沖區傳遞到協議引擎時。
這是一個改進:我們將上下文切換的數量從四個減少到了兩個,並將數據副本的數量從四個減少到了三個(其中只有一個涉及 CPU)。但這還不能使我們達到零拷貝的目標。如果基礎網絡接口卡支持收集操作,則可以進一步減少內核完成的數據重復。在 Linux 內核2.4及更高版本中,已修改套接字緩沖區描述符以適應此要求。這種方法不僅減少了多個上下文切換,而且消除了需要CPU參與的重復數據副本。用戶端的用法仍然保持不變,但內在函數已更改:
-
該 transferTo() 方法使文件內容被 DMA 引擎復制到內核緩沖區中。
-
沒有數據復制到套接字緩沖區。而是僅將具有有關數據的位置和長度的信息的描述符附加到套接字緩沖區。DMA 引擎將數據直接從內核緩沖區傳遞到協議引擎,從而消除了剩余的最終 CPU 復制。
transferTo()
與 gather 操作一起使用的數據副本。在linux 2.4及以上版本的內核中(如linux 6或centos 6以上的版本),開發者修改了socket buffer descriptor,使網卡支持 gather operation,通過 kernel 進一步減少數據的拷貝操作。
transferTo()
在默認情況下,Linux 內核的映射/內存分配工具將創建虛擬連續但物理上不相交的內存區域。這意味着從文件系統讀取的內容 sendfile() 會在內部進入內核虛擬內存中 的緩沖區,該緩沖區必須由DMA代碼遷移到網卡的DMA引擎可以讀取的內容。
由於DMA(通常但並非總是)使用物理地址,這意味着您可以將數據緩沖區(復制到內存中專門分配的物理上相鄰的區域,即上面的套接字緩沖區)中,或者以一個one-physical-page-at-a-time 的方式傳輸。
另一方面,如果你的DMA引擎能夠將多個物理上不相交的內存區域聚合到單個數據傳輸中(稱為“scatter-gather”),則無需復制緩沖區,只需傳遞物理地址列表即可(指向內核緩沖區的物理連續子段,即上面的聚合描述符),您不再需要為每個物理頁啟動單獨的DMA傳輸。這通常更快,但是是否可以完成取決於DMA引擎的功能。
Netty支持兩種零拷貝類型:
1)包裝 FileChannel.tranferTo 方法以實現零復制
FileChannel是一個連接到文件的通道,可以通過文件通道讀寫文件。FileChannel無法設置為非阻塞模式,它總是運行在阻塞模式下。
Netty通過將NIO的FileChannel.transferTo()方法包裝在FileRegion中來實現零拷貝(直接拷貝到另一個Channel中,中間不經過應用程序)。
FileRegion是一個接口,默認實現類是:DefaultFileRegion
2)內置到復合緩沖區類型中的透明零復制實現
① 透明零拷貝透明零拷貝
為了使Web應用程序達到最佳性能,你需要減少內存復制操作的數量。你可能有一組可以組合形成完整消息的緩沖區。網絡提供了一個復合緩沖區,使你可以從任何現有數量的緩沖區中創建一個新的緩沖區,而無需復制內存。例如,一條消息可以包含兩個部分:標頭和正文。在模塊化應用程序中,當發送消息時,兩個部分可以由不同的模塊生產和組裝。
+--------+----------+ | header | body | +--------+----------+
如果使用的是 ByteBuffer(JDK的),則必須創建一個新的大緩沖區,以將兩個部分復制到該新緩沖區中。另外,你可以在Nio上執行收集寫操作,但限制將復合緩沖區類型用作ByteBuffers 數組而不是單個緩沖區,從而破壞了抽象並引入了復雜的狀態管理。另外,如果您不從NIO通道進行讀取或寫入,那將毫無用處。
1 // 復合類型與組件類型不兼容。 2 ByteBuffer [] message = new ByteBuffer [] {header,body};
相比之下,ByteBuf(Netty的)不會發出警告,因為它是完全可擴展的並且具有內置的復合緩沖區。
1 // 復合類型與組件類型兼容。 2 ByteBuf message = Unpooled.wrappedBuffer(header,body); 3 4 // 因此,你甚至可以通過將復合類型與常規緩沖區混合來創建復合類型。 5 ByteBuf messageWithFooter = Unpooled.wrappedBuffer(message,footer); 6 7 // 由於復合類型仍然是ByteBuf,因此訪問其內容很容易。 8 // 訪問方法的行為就像訪問一個單獨的緩沖區一樣。 9 // 即使你要訪問的區域跨越多個組件。 10 // 此處讀取的無符號整數位於正文和頁腳中 11 messageWithFooter.getUnsignedInt( 12 messageWithFooter.visibleBytes() - footer.izableBytes() - 1);
② 自動擴容自動擴容
許多協議定義了可變長度的消息,這意味着在構建消息之前,無法確定消息的長度。或者,計算長度的精確值既困難又不便。這就像在構建字符串時一樣。您通常會估計結果字符串的長度,從而允許 StringBuffer 擴展其自身的要求。
1 // 創建一個新的動態緩沖區。在內部,實際緩沖區是“惰性”創建的,以避免潛在地浪費內存空間。 2 ByteBuf b = Unpooled.buffer(4); 3 4 // 首次嘗試寫入時,將創建內部初始容量為4的緩沖區。 5 b.writeByte('1'); 6 7 b.writeByte('2'); 8 b.writeByte('3'); 9 b.writeByte('4'); 10 11 // 當寫入的字節數超過初始容量4時, 12 // 內部緩沖區自動分配有更大的容量 13 b.writeByte('5');
③ 更好的表現
最常用的緩沖區 ByteBuf 的實現是一個非常薄的字節數組包裝器(例如,一個字節)。與 ByteBuffer 不同,它沒有復雜的邊界和索引檢查補償,因此訪問 JVM 優化的緩沖區要簡單得多。與 ByteBuffer 相比,更復雜的緩沖區實現用於拆分或合並緩存,並具有更好的性能。
ByteBuf 具有豐富的操作集,可實現快速的協議優化。例如,ByteBuf 提供了各種操作來訪問無符號值和字符串,以及在緩沖區中搜索某個字節序列。你還可以擴展或包裝現有的緩沖區類型以提供方便的訪問。仍然從ByteBuf接口實現自定義緩沖區,而不是引入不兼容的類型
與 ByteBuffer 相比,不再需要 flip() 方法,它在正常情況下更加高效且響應迅速。
參考: Efficient data transfer through zero copy,Understand Zero-copy in Netty,zero-copy-with-and-without-scatter-gather-operations,JAVA Zero Copy的相關知識,FileChannel的基本操作