- 傳統I/O : 硬盤—>內核緩沖區—>用戶緩沖區—>內核 Socket 緩沖區—>協議引擎
- sendfile :硬盤—>內核緩沖區—>內核 Socket 緩沖區—>協議引擎
-
sendfile(DMA 收集拷貝):硬盤— >內核緩沖區—>協議引擎
零拷貝(Zero-Copy):一種高效的數據傳輸機制
- mmap + write
- sendfile
1、傳統的數據傳輸方式(四次上下文切換,四次拷貝)
從某台機器將一份數據通過網絡傳輸到另一台機器,通過Java語言簡單描述就是:
public static void transfer() throws IOException { Socket socket = new Socket(HOST, PORT); InputStream in = new FileInputStream(FILE_PATH); OutputStream out = new DataOutputStream(socket.getOutputStream()); byte[] buffer = new byte[1024]; while (in.read(buffer) != -1) { // 將數據寫到 Socket out.write(buffer); } out.close(); socket.close(); in.close(); }
雖然代碼操作看起來很簡單,但是深入到操作系統層面,就會發現實際的微觀操作相當復雜;具體步驟:
- JVM 向 OS 發出 read() 系統調用,觸發上下文切換,從用戶態切換到內核態
- 從外部存儲(如:磁盤)讀取文件內容,通過直接內存訪問(DMA --- Direct Memory Access)存入內核地址空間的緩沖區
- 將數據從內核緩沖區拷貝到用戶空間緩沖區,read() 系統調用返回,並從內核態切換回用戶態
- JVM 向 OS 發出 write() 系統調用,觸發上下文切換,從用戶態切換到內核態
- 將數據從用戶緩沖區拷貝到內核中與目的地 Socket 關聯的緩沖區
- 數據最終經由 Socket 通過 DMA 傳送到硬件(如:網卡)緩沖區,write() 系統調用返回,並從內核態切換回用戶態
|
|
這個過程進行了四次上下文切換(模式切換),並且數據被來回拷貝了四次;但是真正消耗資源和浪費時間的是第2、3次;因為這兩次都需要經過 CPU Copy 而且還需要內核態和用戶態之間的來回切換。如果忽略系統的調用細節,整個過程可以通過下圖表示:
上下文切換是CPU密集型的工作,數據拷貝是 I/O 密集型的工作
如果一次傳輸工作就像上面那樣復雜的話,效率是相當低下的;零拷貝機制的目標就是消除冗余的上下文切換和數據拷貝,提高效率
2、零拷貝的數據傳輸方式
2.1、mmap + write (內存映射)(四次上下文切換,三次數據拷貝)
替代原來的 read + write 方式,mmap 是一種內存映射文件的方式;mmap 通過內存映射,將文件映射到內核緩沖區;
同時,用戶空間可以共享內核空間的數據(mmap 允許程序直接在用戶態中訪問內核空間中的數據,這樣能避免一次無意義的 Copy);建立共享映射后,就不需要從內核緩沖區拷貝到用戶緩沖區了,這就避免了一次拷貝了
- 進行映射拷貝,觸發上下文切換,從用戶態切換到內核態
- 建立用戶緩沖區和內核緩沖區的映射,從內核態切換回用戶態
- 進行數據發送,把數據通過 Socket 發送出去,從用戶態切換到內核態
-
直接把內核緩沖區的數據拷貝到 Socket 緩沖區中,然后拷貝到網絡協議引擎里發送出去,系統調用返回,並從內核態切換回用戶態
|
|
2.2、sendfile(兩次上下文切換,最少兩次數據拷貝)
sendfile() 系統調用在兩個文件描述符之間直接傳遞數據(完全在內核中操作),從而避免了數據在內核緩沖區和用戶緩沖區之間的拷貝,操作效率很高
Linux 2.1 版本提供了 sendFile() 函數:數據根本不經過用戶態,直接從內核緩沖區進入到 Socket Buffer 中;同時由於完全和用戶態無關,就減少了一次上下文切換
- sendfile() 系統調用,利用DMA 引擎將數據拷貝到內核緩沖區,從用戶態切換到內核態
- 數據被拷貝到 Socket 緩沖區
- DMA 引擎將數據從內核 Socket 緩沖區中拷貝到協議引擎中
- 系統調用返回,並從內核態切換回用戶態
|
|
Linux 2.4 后,Socket 緩沖區做了調整,DMA 帶收集功能,DMA 可以直接將內核緩沖區數據直接傳輸到協議引擎,消滅最后一次拷貝
- sendfile() 系統調用,利用 DMA 引擎將數據拷貝到內核緩沖區,從用戶態切換到內核態
- 將帶有文件位置和長度信息的緩沖區描述符添加到 Socket 緩沖區,此過程不需要將數據從內核緩沖區拷貝到 Socket 緩沖區中
- DMA 引擎直接將數據從內核緩沖區中拷貝到協議引擎中,這樣避免了最后一次數據拷貝
- 系統調用返回,並從內核態切換回用戶態
|
|
2.3、mmap 和 sendfile 的區別
- 都是 Linux 內核提供,實現零拷貝的 API
- mmap 適合小數據量讀寫,sendFile() 適合大文件傳輸
- mmap 需要 4 次上下文切換,3 次數據拷貝
- sendFile() 需要 3 次上下文切換,最少 2 次數據拷貝
- sendFile() 可以利用DMA 方式,減少CPU 拷貝;mmap 則不能(必須從內存緩沖區拷貝到Socket 緩沖區)
基於此基礎,RocketMQ 使用了 mmap;Kafka 使用了 sendFile()