開篇
例如我們常見的 kafka、nginx 以及 tomcat 等底層都用的這類技術,這里暫且用 kafka 來列舉案例。
當我們從 kafka 讀取數據的時候,我們會調用 read 方法讀取指定的內容,然后調用 write 方法,將字節流寫到 socket 中,那么,我們調用這兩個方法,在 OS 底層發生了什么呢?我這里畫了一個圖,嘗試解釋這個過程。
以下步驟都是黑色線條標識的路線:
-
read 調用導致 用戶態 到 內核態 的一次變化,同時,第一次復制開始:DMA(Direct Memory Access,直接內存存取,即不使用 CPU 拷貝數據到內存,而是 DMA 引擎傳輸數據到內存,用於解放 CPU) 引擎從磁盤讀取指定內容,並將數據放入到內核緩沖區。
-
發生第二次數據拷貝,即:將內核緩沖區的數據拷貝到用戶緩沖區,同時,發生了一次 內核態 到 用戶態 的上下文切換。
-
發生第三次數據拷貝,我們調用 write 方法,系統將 用戶緩沖區 的數據拷貝到 Socket 緩沖區。此時,又發生了一次 用戶態 到 內核態 的上下文切換。
-
第四次拷貝,數據異步的從 Socket 緩沖區,使用 DMA 引擎拷貝到網絡協議引擎。這一段,不需要進行上下文切換。
-
write 方法返回,再次從 內核態 切換 到用戶態。
mmap 內存映射優化
mmap 通過內存映射,將文件映射到內核緩沖區,同時,用戶空間可以共享內核空間的數據。這樣,在進行網絡傳輸時,就可以減少 內核空間 到 用戶空間 的拷貝次數。
如上圖,user buffer 和 kernel buffer 共享 data 數據。如果你想把硬盤的 data 數據傳輸到網絡中,再也不用拷貝到用戶空間,再從用戶空間拷貝到 Socket 緩沖區。
現在,你只需要從 內核緩沖區 拷貝到 Socket 緩沖區即可,這將減少一次內存拷貝(從 4 次變成了 3 次),但不減少上下文切換次數。(這塊需要大家再根據前面的講解好好理解一下)
sendFile 零拷貝優化
其基本原理如下:數據根本不經過用戶態,直接從內核緩沖區進入到 Socket Buffer,同時,由於和用戶態完全無關,就減少了一次上下文切換。看粉色的線條標識。如上圖,我們進行 sendFile 系統調用時,數據被 DMA 引擎從文件復制到內核緩沖區,然后可以直接從內核緩沖區進入到 Socket,這時,是沒有上下文切換的,因為在一個空間。
最后,數據從 Socket 緩沖區進入到協議棧。
此時,數據經過了 2 次拷貝,即:
第一次使用 DMA 引擎從文件拷貝到內核緩沖區,第二次從內核緩沖區將數據拷貝到網絡協議棧;內核緩存區只會拷貝一些 offset 和 length 信息到 SocketBuffer,基本無消耗。
再稍微講講 mmap 和 sendFile 的區別。
- mmap 適合小數據量讀寫,sendFile 適合大文件傳輸。
- mmap 需要 4 次上下文切換,3 次數據拷貝;sendFile 需要 3 次上下文切換,最少 2 次數據拷貝。
- sendFile 可以利用 DMA 方式,減少 CPU 拷貝,mmap 則不能(必須從內核拷貝到 Socket 緩沖區)。
在這個選擇上:rocketMQ 在消費消息時,使用了 mmap。kafka 使用了 sendFile。(可以思考下為什么 kafka 適合使用 sendFile?因為 kafka 大部分場景是使用消息隊列,基本上沒有復雜場景,就是一個數據的流轉,所以適合數據進來直接被消費方讀取走了,在這期間不需要做其他內部業務邏輯)
Java 世界的例子
kafka

tomcat
tomcat 內部在進行文件拷貝的時候,也會使用 transferto 方法。
tomcat 在處理一下心跳保活時,也會調用該 sendFile 方法。
所以,如果你需要優化網絡傳輸的性能,或者文件讀寫的速度,請盡量使用零拷貝。他不僅能較少復制拷貝次數,還能較少上下文切換,緩存行污染。