我理解的零拷貝-原文連接
java
最近作的業務涉及到的 I/O 操做比較多,對於Linux上的 I/O 操做的優化 Zero Copy 早有耳聞,今天打算由上而下(從應用層到底層,固然並不會涉及到內核的細節)的研究一下這個問題。linux
爲了更好的描述 zero copy ,本文將以網絡服務器的簡單過程所涉及的內容展開,該過程經過網絡將存儲在服務端的文件中的數據提供給客戶端。整個過程主要是網絡的 I/O 操做,數據至少被複制了4次,而且幾乎已經執行了許多用戶/內核上下文切換。 以下圖所示,通過了下面四個步驟:緩存
步驟一:操做系統發生 read 系統調用讀取磁盤中的文件內容並將其存儲到內核地址空間緩衝區中。服務器
第二步:將數據從內核緩衝區複製到用戶緩衝區,read 系統調用返回。調用的返回致使了從內核返回到用戶模式的上下文切換,如今,數據存儲在用戶地址空間緩衝區中,它能夠再次開始向下移動。網絡
第三步:write 系統調用致使從用戶模式到內核模式的上下文切換,執行第三個複製,將數據再次放入內核地址空間緩衝區中。可是這一次,數據被放入一個不一樣的緩衝區,這個緩衝區是與套接字相關聯的。架構
第四步:寫系統調用返回,建立第四個上下文切換。並將數據寫入網絡 I/O 中,網絡傳輸中的服務端的操做邏輯到此結束。jvm
從上圖中咱們知道,整個網絡傳輸過程當中數據被複制了多達4次之多,也進行了屢次從用戶態到內核態的切換。那麼有沒有可能減小數據的複製次數,提升網絡 I/O 的效率呢?答案是確定的。socket
那麼到底什麼是零拷貝呢?就是將數據直接從內核態的緩衝區中直接拷貝到 Socket 的緩衝區中,沒有通過用戶態的緩衝區,之因此被叫作零拷貝是相對於用戶態來講的。以下圖所示: 性能
總的來講,從操做系統的角度來看是零拷貝,由於數據不是在內核緩衝區之間複製的。當使用零拷貝時,除了複製避免以外,還用其餘性能優點,例如更少的上下文切換、更少的 CPU 數據緩存污染和沒有 CPU 校驗和計算。優化
NIO 中的 FileChannel 擁有 transferTo 和 transferFrom 兩個方法,可直接把 FileChannel 中的數據拷貝到另一個 Channel,或直接把另一個 Channel 中的數據拷貝到 FileChannel。該接口常被用於高效的網絡/文件的數據傳輸和大文件拷貝。在操做系統支持的狀況下,經過該方法傳輸數據並不須要將源數據從內核態拷貝到用戶態,再從用戶態拷貝到目標通道的內核態,同時也避免了兩次用戶態和內核態間的上下文切換,也即便用了「零拷貝」。
/** * disk-nic零拷貝 */
class ZeroCopyServer {
ServerSocketChannel listener = null;
public static void main(String[] args) {
ZerocopyServer dns = new ZerocopyServer();
dns.mySetup();
dns.readData();
}
protected void mySetup() {
InetSocketAddress listenAddr = new InetSocketAddress(9026);
try {
listener = ServerSocketChannel.open();
ServerSocket ss = listener.socket();
ss.setReuseAddress(true);
ss.bind(listenAddr);
System.out.println("監聽的端口:" + listenAddr.toString());
} catch (IOException e) {
System.out.println("端口綁定失敗 : " + listenAddr.toString() + " 端口可能已經被使用,出錯緣由: " + e.getMessage());
e.printStackTrace();
}
}
private void readData() {
ByteBuffer dst = ByteBuffer.allocate(4096);
try {
while (true) {
SocketChannel conn = listener.accept();
System.out.println("建立的鏈接: " + conn);
conn.configureBlocking(true);
int nread = 0;
while (nread != -1) {
try {
nread = conn.read(dst);
} catch (IOException e) {
e.printStackTrace();
nread = -1;
}
dst.rewind();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
複製代碼
對於 I/O 操做的優化也能夠參考零拷貝的思路來對咱們的系統進行優化,最近了解到 kafka 之因此能夠可以承載高吞吐量跟它強依賴底層操做系統的 page cache 有很大關係,因此在使用 Kafka 並非 jvm 的內存越大越好,跟零拷貝的減小數據在內核態與用戶態之間的拷貝,上下文切換有殊途同歸的操做,對 kafka 還不甚瞭解不敢多說了……
爲了彌補這種性能差別,現代操做系統在愈來愈注重使用內存對磁盤進行 cache。現代操做系統主動將全部空閒內存用做 disk caching ,代價是在內存回收時性能會有所下降。全部對磁盤的讀寫操做都會經過這個統一的 cache。若是不使用直接 I/O,該功能不能輕易關閉。所以即便進程維護了 in-process cache,該數據也可能會被複制到操做系統的 pagecache 中,事實上全部內容都被存儲了兩份。
此外,Kafka 創建在 JVM 之上,任何瞭解 Java 內存使用的人都知道兩點:
受這些因素影響,相比於維護 in-memory cache 或者其餘結構,使用文件系統和 pagecache 顯得更有優點--咱們能夠經過自動訪問全部空閒內存將可用緩存的容量至少翻倍,而且經過存儲緊湊的字節結構而不是獨立的對象,有望將緩存容量再翻一番。 這樣使得32GB的機器緩存容量能夠達到28-30GB,而且不會產生額外的 GC 負擔。此外,即便服務從新啓動,緩存依舊可用,而 in-process cache 則須要在內存中重建(重建一個10GB的緩存可能須要10分鐘),不然進程就要從 cold cache 的狀態開始(這意味着進程最初的性能表現十分糟糕)。 這同時也極大的簡化了代碼,由於全部保持 cache 和文件系統之間一致性的邏輯如今都被放到了 OS 中,這樣作比一次性的進程內緩存更準確、更高效。若是你的磁盤使用更傾向於順序讀取,那麼 read-ahead 能夠有效的使用每次從磁盤中讀取到的有用數據預先填充 cache。
這裏給出了一個很是簡單的設計:相比於維護儘量多的 in-memory cache,而且在空間不足的時候匆忙將數據 flush 到文件系統,咱們把這個過程倒過來。全部數據一開始就被寫入到文件系統的持久化日誌中,而不用在 cache 空間不足的時候 flush 到磁盤。實際上,這代表數據被轉移到了內核的 pagecache 中。
如上圖所示,從宏觀上來看,操做系統的體系架構分爲用戶態和內核態。內核從本質上看是一種軟件——控制計算機的硬件資源,並提供上層應用程序運行的環境。用戶態即上層應用程序的活動空間,應用程序的執行必須依託於內核提供的資源,包括 CPU 資源、存儲資源、I/O 資源等。爲了使上層應用可以訪問到這些資源,內核必須爲上層應用提供訪問的接口:即系統調用。