框架篇:Linux零拷貝機制和FileChannel

cscw發表於2020-10-27

前言

大白話解釋,零拷貝就是沒有把資料從一個儲存區域拷貝到另一個儲存區域。但是沒有資料的複製,怎麼可能實現資料的傳輸呢?其實我們在java NIO、netty、kafka遇到的零拷貝,並不是不復制資料,而是減少不必要的資料拷貝次數,從而提升程式碼效能

  • 零拷貝的好處
  • 核心空間和使用者空間
  • 緩衝區和虛擬記憶體
  • 傳統的 I/O
  • mmap+write 實現的零拷貝
  • sendfile 實現的零拷貝
  • 帶有DMA收集拷貝功能的sendfile實現的零拷貝
  • java提供的零拷貝方式

關注公眾號,一起交流 :潛行前行

零拷貝的好處

  • 減少或避免不必要的CPU資料拷貝,從而釋放CPU去執行其他任務
  • 零拷貝機制能減少使用者空間和作業系統核心空間的上下文切換
  • 減少記憶體的佔用

核心空間和使用者空間

  • 核心空間:Linux自身使用的空間;主要提供程式排程、記憶體分配、連線硬體資源等功能
  • 使用者空間:提供給各個程式程式的空間;使用者空間不具有訪問核心空間資源的許可權,如果應用程式需要使用到核心空間的資源,則需要通過系統呼叫來完成:從使用者空間切換到核心空間,完成相關操作後再從核心空間切換回使用者空間

緩衝區和虛擬記憶體

  • 直接記憶體訪問(Direct Memory Access)(DMA)
    • 直接記憶體訪問:DMA允許外設裝置和記憶體儲存器之間直接進行IO資料傳輸,其過程不需要CPU的參與

  • 緩衝區 是所有I/O的基礎,I/O 無非就是把資料移進或移出緩衝區
    • 程式發起read請求,核心先檢查核心空間緩衝區是否存在程式所需資料,如果已經存在,則直接copy資料到程式的記憶體區。如果沒有,系統則向磁碟請求資料,通過DMA寫入核心的read緩衝衝區,接著再將核心緩衝區資料copy到程式的記憶體區
    • 程式發起write請求,則是把程式的記憶體區資料copy到核心的write緩衝區,然後再通過DMA把核心緩衝區資料刷回磁碟或者網路卡中
  • 虛擬記憶體:現代作業系統都使用虛擬記憶體,有如下兩個好處
    • 一個以上的虛擬地址可以指向同一個實體記憶體地址
    • 虛擬記憶體空間可大於實際可用的實體地址
  • 利用第一點特性可以把核心空間地址和使用者空間的虛擬地址對映到同一個實體地址,這樣DMA就可以填充(讀寫)對核心和使用者空間程式同時可見的緩衝區了;大致如下

傳統的 I/O

#include <unistd>
ssize_t write(int filedes, void *buf, size_t nbytes);
ssize_t read(int filedes, void *buf, size_t nbytes);
  • 如java在linux系統上,讀取一個磁碟檔案,併傳送到遠端端的服務

  • 1)發出read系統呼叫,會導致使用者空間到核心空間的上下文切換,然後再通過DMA將檔案中的資料從磁碟上讀取到核心空間緩衝區
  • 2)接著將核心空間緩衝區的資料拷貝到使用者空間程式記憶體,然後read系統呼叫返回。而系統呼叫的返回又會導致一次核心空間到使用者空間的上下文切換
  • 3)write系統呼叫,則再次導致使用者空間到核心空間的上下文切換,將使用者空間的程式裡的記憶體資料複製到核心空間的socket緩衝區(也是核心緩衝區,不過是給socket使用的),然後write系統呼叫返回,再次觸發上下文切換
  • 4)至於socket緩衝區到網路卡的資料傳輸則是獨立非同步的過程,也就是說write系統呼叫的返回並不保證資料被傳輸到網路卡

一共有四次使用者空間與核心空間的上下文切換。四次資料copy,分別是兩次CPU資料複製,兩次DMA資料複製

mmap+write實現的零拷貝

#include <sys/mman.h>
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset)

  • 1)發出mmap系統呼叫,導致使用者空間到核心空間的上下文切換。然後通過DMA引擎將磁碟檔案中的資料複製到核心空間緩衝區
  • 2)mmap系統呼叫返回,導致核心空間到使用者空間的上下文切換
  • 3)這裡不需要將資料從核心空間複製到使用者空間,因為使用者空間和核心空間共享了這個緩衝區
  • 4)發出write系統呼叫,導致使用者空間到核心空間的上下文切換。將資料從核心空間緩衝區複製到核心空間socket緩衝區;write系統呼叫返回,導致核心空間到使用者空間的上下文切換
  • 5)非同步,DMA引擎將socket緩衝區中的資料copy到網路卡

通過mmap實現的零拷貝I/O進行了4次使用者空間與核心空間的上下文切換,以及3次資料拷貝;其中3次資料拷貝中包括了2次DMA拷貝和1次CPU拷貝

sendfile實現的零拷貝

#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

  • 1)發出sendfile系統呼叫,導致使用者空間到核心空間的上下文切換,然後通過DMA引擎將磁碟檔案中的內容複製到核心空間緩衝區中,接著再將資料從核心空間緩衝區複製到socket相關的緩衝區
  • 2)sendfile系統呼叫返回,導致核心空間到使用者空間的上下文切換。DMA非同步將核心空間socket緩衝區中的資料傳遞到網路卡

通過sendfile實現的零拷貝I/O使用了2次使用者空間與核心空間的上下文切換,以及3次資料的拷貝。其中3次資料拷貝中包括了2次DMA拷貝和1次CPU拷貝

帶有DMA收集拷貝功能的sendfile實現的零拷貝

  • 從Linux 2.4版本開始,作業系統提供scatter和gather的SG-DMA方式,直接從核心空間緩衝區中將資料讀取到網路卡,無需將核心空間緩衝區的資料再複製一份到socket緩衝區

  • 1)發出sendfile系統呼叫,導致使用者空間到核心空間的上下文切換。通過DMA引擎將磁碟檔案中的內容複製到核心空間緩衝區
  • 2)這裡沒把資料複製到socket緩衝區;取而代之的是,相應的描述符資訊被複制到socket緩衝區。該描述符包含了兩種的資訊:A)核心緩衝區的記憶體地址、B)核心緩衝區的偏移量
  • 3)sendfile系統呼叫返回,導致核心空間到使用者空間的上下文切換。DMA根據socket緩衝區的描述符提供的地址和偏移量直接將核心緩衝區中的資料複製到網路卡

帶有DMA收集拷貝功能的sendfile實現的I/O使用了2次使用者空間與核心空間的上下文切換,以及2次資料的拷貝,而且這2次的資料拷貝都是非CPU拷貝。這樣一來我們就實現了最理想的零拷貝I/O傳輸了,不需要任何一次的CPU拷貝,以及最少的上下文切換

java提供的零拷貝方式

  • java NIO的零拷貝實現是基於mmap+write方式
  • FileChannel的map方法產生的MappedByteBuffer
    FileChannel提供了map()方法,該方法可以在一個開啟的檔案和MappedByteBuffer之間建立一個虛擬記憶體對映,MappedByteBuffer繼承於ByteBuffer;該緩衝器的記憶體是一個檔案的記憶體對映區域。map方法底層是通過mmap實現的,因此將檔案記憶體從磁碟讀取到核心緩衝區後,使用者空間和核心空間共享該緩衝區。用法如下
public void main(String[] args){
    try {
        FileChannel readChannel = FileChannel.open(Paths.get("./cscw.txt"), StandardOpenOption.READ);
        FileChannel writeChannel = FileChannel.open(Paths.get("./siting.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
        MappedByteBuffer data = readChannel.map(FileChannel.MapMode.READ_ONLY, 0, 1024 * 1024 * 40);
       	//資料傳輸
        writeChannel.write(data);
        readChannel.close();
        writeChannel.close();
    }catch (Exception e){
        System.out.println(e.getMessage());
    }
}
  • FileChannel的transferTo、transferFrom
    如果作業系統底層支援的話,transferTo、transferFrom也會使用相關的零拷貝技術來實現資料的傳輸。用法如下
public void main(String[] args) {
    try {
        FileChannel readChannel = FileChannel.open(Paths.get("./cscw.txt"), StandardOpenOption.READ);
        FileChannel writeChannel = FileChannel.open(Paths.get("./siting.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
        long len = readChannel.size();
        long position = readChannel.position();
        //資料傳輸
        readChannel.transferTo(position, len, writeChannel);
        //效果和transferTo 一樣的
        //writeChannel.transferFrom(readChannel, position, len, );
        readChannel.close();
        writeChannel.close();
    } catch (Exception e) {
        System.out.println(e.getMessage());
    }
}

歡迎指正文中錯誤

關注公眾號,一起交流

參考文章

相關文章