文章來自:https://blog.csdn.net/weixin_37782390/article/details/103833306
零拷貝這三個字,一直是服務器網絡編程的關鍵字,任何性能優化都離不開。在 Java 程序員的世界,常用的零拷貝有 mmap 和 sendFile。那么,他們在 OS 里,到底是怎么樣的一個的設計?本文將簡單聊聊 mmap 和 sendFile 這兩個零拷貝。
一、傳統IO的劣勢
初學 Java 時,我們在學習 IO 和 網絡編程時,會使用以下代碼:
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 底層發生了什么呢?我這里借鑒了一張其他文章的圖片,嘗試解釋這個過程。
上圖中,上半部分表示用戶態和內核態的上下文切換。下半部分表示數據復制操作。下面說說他們的步驟:
-
read 調用導致用戶態到內核態的一次變化,同時,第一次復制開始:DMA(Direct Memory Access,直接內存存取,即不使用 CPU 拷貝數據到內存,而是 DMA 引擎傳輸數據到內存,用於解放 CPU) 引擎從磁盤讀取 index.html 文件,並將數據放入到內核緩沖區。
-
發生第二次數據拷貝,即:將內核緩沖區的數據拷貝到用戶緩沖區,同時,發生了一次用內核態到用戶態的上下文切換。
-
發生第三次數據拷貝,我們調用 write 方法,系統將用戶緩沖區的數據拷貝到 Socket 緩沖區。此時,又發生了一次用戶態到內核態的上下文切換。
-
第四次拷貝,數據異步的從 Socket 緩沖區,使用 DMA 引擎拷貝到網絡協議引擎。這一段,不需要進行上下文切換。
-
write 方法返回,再次從內核態切換到用戶態。
如你所見,復制拷貝操作太多了。如何優化這些流程?
二、mmap 優化
mmap 通過內存映射,將文件映射到內核緩沖區,同時,用戶空間可以共享內核空間的數據。這樣,在進行網絡傳輸時,就可以減少內核空間到用戶空間的拷貝次數。如下圖:
如上圖,user buffer 和 kernel buffer 共享 index.html。如果你想把硬盤的 index.html 傳輸到網絡中,再也不用拷貝到用戶空間,再從用戶空間拷貝到 Socket 緩沖區。
現在,你只需要從內核緩沖區拷貝到 Socket 緩沖區即可,這將減少一次內存拷貝(從 4 次變成了 3 次),但不減少上下文切換次數。
三、sendFile
那么,我們還能繼續優化嗎? 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 的區別。
- mmap 適合小數據量讀寫,sendFile 適合大文件傳輸。
- mmap 需要 4 次上下文切換,3 次數據拷貝;sendFile 需要 3 次上下文切換,最少 2 次數據拷貝。
- sendFile 可以利用 DMA 方式,減少 CPU 拷貝,mmap 則不能(必須從內核拷貝到 Socket 緩沖區)。
在這個選擇上:rocketMQ 在消費消息時,使用了 mmap。kafka 使用了 sendFile。
四、Java中的例子
kafka 在客戶端和 broker 進行數據傳輸時,會使用 transferTo 和 transferFrom 方法,即對應 Linux 的 sendFile。
tomcat 內部在進行文件拷貝的時候,也會使用 transferto 方法。
tomcat 在處理一下心跳保活時,也會調用該 sendFile 方法。
所以,如果你需要優化網絡傳輸的性能,或者文件讀寫的速度,請盡量使用零拷貝。它不僅能較少復制拷貝次數,還能較少上下文切換,緩存行污染。