零拷貝(Zero-Copy)
- 傳統I/O : 硬碟—>核心緩衝區—>使用者緩衝區—>核心 Socket 緩衝區—>協議引擎
- sendfile :硬碟—>核心緩衝區—>核心 Socket 緩衝區—>協議引擎
- sendfile(DMA 收集拷貝):硬碟—>核心緩衝區—>協議引擎
零拷貝(Zero-Copy):一種高效的資料傳輸機制
- mmap + write
- sendfile
1、傳統的資料傳輸方式(四次上下文切換,四次拷貝)
從某臺機器將一份資料通過網路傳輸到另一臺機器,通過
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 關聯的緩衝區
- 資料最終經由
這個過程進行了四次上下文切換(模式切換),並且資料被來回拷貝了四次;但是真正消耗資源和浪費時間的是第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()