零拷貝(Zero-Copy)


  • 傳統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.1mmap + write (內存映射)(四次上下文切換,三次數據拷貝)

替代原來的 read + write 方式,mmap 是一種內存映射文件的方式;mmap 通過內存映射,將文件映射到內核緩沖區;

同時,用戶空間可以共享內核空間的數據mmap 允許程序直接在用戶態中訪問內核空間中的數據,這樣能避免一次無意義的 Copy);建立共享映射后,就不需要從內核緩沖區拷貝到用戶緩沖區了,這就避免了一次拷貝了

  • 進行映射拷貝,觸發上下文切換,從用戶態切換到內核態
  • 建立用戶緩沖區和內核緩沖區的映射,從內核態切換回用戶態
  • 進行數據發送,把數據通過 Socket 發送出去,從用戶態切換到內核態
  • 直接把內核緩沖區的數據拷貝到 Socket 緩沖區中,然后拷貝到網絡協議引擎里發送出去,系統調用返回,並從內核態切換回用戶態

       

 

 

   

2.2sendfile(兩次上下文切換,最少兩次數據拷貝)

sendfile() 系統調用在兩個文件描述符之間直接傳遞數據(完全在內核中操作),從而避免了數據在內核緩沖區和用戶緩沖區之間的拷貝,操作效率很高

Linux 2.1 版本提供了 sendFile() 函數:數據根本不經過用戶態,直接從內核緩沖區進入到 Socket Buffer 中;同時由於完全和用戶態無關,就減少了一次上下文切換

  • sendfile() 系統調用,利用DMA 引擎將數據拷貝到內核緩沖區,從用戶態切換到內核態
  • 數據被拷貝到 Socket 緩沖區
  • DMA 引擎將數據從內核 Socket 緩沖區中拷貝到協議引擎中
  • 系統調用返回,並從內核態切換回用戶態

 

 

   

Linux 2.4 后,Socket 緩沖區做了調整,DMA 帶收集功能,DMA 可以直接將內核緩沖區數據直接傳輸到協議引擎,消滅最后一次拷貝

  • sendfile() 系統調用,利用 DMA 引擎將數據拷貝到內核緩沖區,從用戶態切換到內核態
  • 將帶有文件位置和長度信息的緩沖區描述符添加到 Socket 緩沖區,此過程不需要將數據從內核緩沖區拷貝到 Socket 緩沖區中
  • DMA 引擎直接將數據從內核緩沖區中拷貝到協議引擎中,這樣避免了最后一次數據拷貝
  • 系統調用返回,並從內核態切換回用戶態

   

2.3mmap sendfile 的區別

  • 都是 Linux 內核提供,實現零拷貝的 API
  • mmap 適合小數據量讀寫,sendFile() 適合大文件傳輸
  • mmap 需要 4 次上下文切換,3 次數據拷貝
  • sendFile() 需要 3 次上下文切換,最少 2 次數據拷貝
  • sendFile() 可以利用DMA 方式,減少CPU 拷貝;mmap 則不能(必須從內存緩沖區拷貝到Socket 緩沖區)

基於此基礎,RocketMQ 使用了 mmapKafka 使用了 sendFile()


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM