Linux 中的零拷貝

Linux 中的零拷貝

最近做的業務涉及到的 I/O 操作比較多,對於Linux上的 I/O 操作的優化 Zero Copy 早有耳聞,今天打算由上而下(從應用層到底層,當然並不會涉及到內核的細節)的研究一下這個問題。

什麼是零拷貝

為了更好的描述 zero copy ,本文將以網絡服務器的簡單過程所涉及的內容展開,該過程通過網絡將存儲在服務端的文件中的數據提供給客戶端。整個過程主要是網絡的 I/O 操作,數據至少被複制了4次,並且幾乎已經執行了許多用戶/內核上下文切換。 如下圖所示,經過了下面四個步驟:

Linux 中的零拷貝

網絡 I/O 傳輸

步驟一:操作系統發生 read 系統調用讀取磁盤中的文件內容並將其存儲到內核地址空間緩衝區中。

第二步:將數據從內核緩衝區複製到用戶緩衝區,read 系統調用返回。調用的返回導致了從內核返回到用戶模式的上下文切換,現在,數據存儲在用戶地址空間緩衝區中,它可以再次開始向下移動。

第三步:write 系統調用導致從用戶模式到內核模式的上下文切換,執行第三個複製,將數據再次放入內核地址空間緩衝區中。但是這一次,數據被放入一個不同的緩衝區,這個緩衝區是與套接字相關聯的。

第四步:寫系統調用返回,創建第四個上下文切換。並將數據寫入網絡 I/O 中,網絡傳輸中的服務端的操作邏輯到此結束。

從上圖中我們知道,整個網絡傳輸過程中數據被複制了多達4次之多,也進行了多次從用戶態到內核態的切換。那麼有沒有可能減少數據的複製次數,提高網絡 I/O 的效率呢?答案是肯定的。

那麼到底什麼是零拷貝呢?就是將數據直接從內核態的緩衝區中直接拷貝到 Socket 的緩衝區中,沒有經過用戶態的緩衝區,之所以被叫做零拷貝是相對於用戶態來說的。如下圖所示:

Linux 中的零拷貝

網絡 I/O 傳輸

總的來說,從操作系統的角度來看是零拷貝,因為數據不是在內核緩衝區之間複製的。當使用零拷貝時,除了複製避免之外,還用其他性能優勢,例如更少的上下文切換、更少的 CPU 數據緩存汙染和沒有 CPU 校驗和計算。

零拷貝的 Java 實現

NIO 中的 FileChannel 擁有 transferTo 和 transferFrom 兩個方法,可直接把 FileChannel 中的數據拷貝到另外一個 Channel,或直接把另外一個 Channel 中的數據拷貝到 FileChannel。該接口常被用於高效的網絡/文件的數據傳輸和大文件拷貝。在操作系統支持的情況下,通過該方法傳輸數據並不需要將源數據從內核態拷貝到用戶態,再從用戶態拷貝到目標通道的內核態,同時也避免了兩次用戶態和內核態間的上下文切換,也即使用了“零拷貝”。

說點題外話

對於 I/O 操作的優化也可以參考零拷貝的思路來對我們的系統進行優化,最近了解到 kafka 之所以可以能夠承載高吞吐量跟它強依賴底層操作系統的 page cache 有很大關係,所以在使用 Kafka 並不是 jvm 的內存越大越好,跟零拷貝的減少數據在內核態與用戶態之間的拷貝,上下文切換有異曲同工的操作,對 kafka 還不甚瞭解不敢多說了……

Kafka 官網看到的

為了彌補這種性能差異,現代操作系統在越來越注重使用內存對磁盤進行 cache。現代操作系統主動將所有空閒內存用作 disk caching ,代價是在內存回收時性能會有所降低。所有對磁盤的讀寫操作都會通過這個統一的 cache。如果不使用直接 I/O,該功能不能輕易關閉。因此即使進程維護了 in-process cache,該數據也可能會被複制到操作系統的 pagecache 中,事實上所有內容都被存儲了兩份。

此外,Kafka 建立在 JVM 之上,任何瞭解 Java 內存使用的人都知道兩點:

  • 對象的內存開銷非常高,通常是所存儲的數據的兩倍(甚至更多)。
  • 隨著堆中數據的增加,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 中。

關於文中多次出現的用戶態,內核態

Linux 中的零拷貝

如上圖所示,從宏觀上來看,操作系統的體系架構分為用戶態和內核態。內核從本質上看是一種軟件——控制計算機的硬件資源,並提供上層應用程序運行的環境。用戶態即上層應用程序的活動空間,應用程序的執行必須依託於內核提供的資源,包括 CPU 資源、存儲資源、I/O 資源等。為了使上層應用能夠訪問到這些資源,內核必須為上層應用提供訪問的接口:即系統調用。

參考鏈接

維基百科-零拷貝
Linux 零拷貝原理


分享到:


相關文章: