1. 程式人生 > 實用技巧 >零拷貝(Zero-Copy)

零拷貝(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()