零拷貝這三個字,一直是服務器網絡編程的關鍵字,任何性能優化都離不開。在 Java 程序員的世界,經常使用的零拷貝有 mmap 和 sendFile。那麼,他們在 OS 裏,究竟是怎麼樣的一個的設計?本文將簡單聊聊 mmap 和 sendFile 這兩個零拷貝。php
初學 Java 時,咱們在學習 IO 和 網絡編程時,會使用如下代碼:html
File file = new File("index.html"); RandomAccessFile raf = new RandomAccessFile(file, "rw"); byte[] arr = new byte[(int) file.length()]; raf.read(arr); Socket socket = new ServerSocket(8080).accept(); socket.getOutputStream().write(arr);
咱們會調用 read 方法讀取 index.html 的內容—— 變成字節數組,而後調用 write 方法,將 index.html 字節流寫到 socket 中,那麼,咱們調用這兩個方法,在 OS 底層發生了什麼呢?我這裏借鑑了一張其餘文字的圖片,嘗試解釋這個過程。java
上圖中,上半部分表示用戶態和內核態的上下文切換。下半部分表示數據複製操做。下面說說他們的步驟:linux
read 調用致使用戶態到內核態的一次變化,同時,第一次複製開始:DMA(Direct Memory Access,直接內存存取,即不使用 CPU 拷貝數據到內存,而是 DMA 引擎傳輸數據到內存,用於解放 CPU) 引擎從磁盤讀取 index.html 文件,並將數據放入到內核緩衝區。程序員
發生第二次數據拷貝,即:將內核緩衝區的數據拷貝到用戶緩衝區,同時,發生了一次用內核態到用戶態的上下文切換。編程
發生第三次數據拷貝,咱們調用 write 方法,系統將用戶緩衝區的數據拷貝到 Socket 緩衝區。此時,又發生了一次用戶態到內核態的上下文切換。數組
第四次拷貝,數據異步的從 Socket 緩衝區,使用 DMA 引擎拷貝到網絡協議引擎。這一段,不須要進行上下文切換。緩存
write 方法返回,再次從內核態切換到用戶態。tomcat
如你所見,複製拷貝操做太多了。如何優化這些流程?性能優化
mmap 經過內存映射,將文件映射到內核緩衝區,同時,用戶空間能夠共享內核空間的數據。這樣,在進行網絡傳輸時,就能夠減小內核空間到用戶控件的拷貝次數。以下圖:
如上圖,user buffer 和 kernel buffer 共享 index.html。若是你想把硬盤的 index.html 傳輸到網絡中,不再用拷貝到用戶空間,再從用戶空間拷貝到 Socket 緩衝區。
如今,你只須要從內核緩衝區拷貝到 Socket 緩衝區便可,這將減小一次內存拷貝(從 4 次變成了 3 次),但不減小上下文切換次數。
那麼,咱們還能繼續優化嗎? Linux 2.1 版本 提供了 sendFile 函數,其基本原理以下:數據根本不通過用戶態,直接從內核緩衝區進入到 Socket Buffer,同時,因爲和用戶態徹底無關,就減小了一次上下文切換。
如上圖,咱們進行 sendFile 系統調用時,數據被 DMA 引擎從文件複製到內核緩衝區,而後調用,而後掉一共 write 方法時,從內核緩衝區進入到 Socket,這時,是沒有上下文切換的,由於在一個用戶空間。
最後,數據從 Socket 緩衝區進入到協議棧。
此時,數據通過了 3 次拷貝,3 次上下文切換。
那麼,還能不能再繼續優化呢? 例如直接從內核緩衝區拷貝到網絡協議棧?
實際上,Linux 在 2.4 版本中,作了一些修改,避免了從內核緩衝區拷貝到 Socket buffer 的操做,直接拷貝到協議棧,從而再一次減小了數據拷貝。具體以下圖:
如今,index.html 要從文件進入到網絡協議棧,只需 2 次拷貝:第一次使用 DMA 引擎從文件拷貝到內核緩衝區,第二次從內核緩衝區將數據拷貝到網絡協議棧;內核緩存區只會拷貝一些 offset 和 length 信息到 SocketBuffer,基本無消耗。
等一下,不是說零拷貝嗎?爲何仍是要 2 次拷貝?
答:首先咱們說零拷貝,是從操做系統的角度來講的。由於內核緩衝區之間,沒有數據是重複的(只有 kernel buffer 有一份數據,sendFile 2.1 版本實際上有 2 份數據,算不上零拷貝)。例如咱們剛開始的例子,內核緩存區和 Socket 緩衝區的數據就是重複的。
而零拷貝不單單帶來更少的數據複製,還能帶來其餘的性能優點,例如更少的上下文切換,更少的 CPU 緩存僞共享以及無 CPU 校驗和計算。
再稍微講講 mmap 和 sendFile 的區別。
在這個選擇上:rocketMQ 在消費消息時,使用了 mmap。kafka 使用了 sendFile。
kafka 在客戶端和 broker 進行數據傳輸時,會使用 transferTo 和 transferFrom 方法,即對應 Linux 的 sendFile。
tomcat 內部在進行文件拷貝的時候,也會使用 transferto 方法。
tomcat 在處理一下心跳保活時,也會調用該 sendFile 方法。
在 pulsar 項目中,下載文件時,也會使用 sendFile。以下圖:
因此,若是你須要優化網絡傳輸的性能,或者文件讀寫的速度,請儘可能使用零拷貝。他不只能較少複製拷貝次數,還能較少上下文切換,緩存行污染。
Zero Copy I: User-Mode Perspective