1. 程式人生 > 實用技巧 >零拷貝

零拷貝

零拷貝(Zero-Copy)是一個大家耳熟能詳的概念,那麼,具體有哪些框架會使用到零拷貝呢?在思考這個問題之前,讓我們先一起探尋一下零拷貝機制的底層原理。

- 概念篇 -

1、零拷貝是什麼?

"零拷貝"中的"拷貝"是指作業系統在I/O操作中,將資料從一個記憶體區域複製到另外一個記憶體區域,而"零"並不是指0次複製, 更多的是指在使用者態和核心態之間的複製是0次。

2、零拷貝給我們帶來的好處

•減少甚至完全避免不必要的 CPU 拷貝,從而讓 CPU 解脫出來去執行其他的任務;

•減少記憶體頻寬的佔用;

•通常零拷貝技術還能夠減少使用者空間和作業系統核心空間之間的上下文切換。

3、作業系統中誰負責IO拷貝?

DMA 負責核心間的 IO 傳輸,CPU 負責核心和應用間的 IO 傳輸。

兩種拷貝型別:

(1)CPU COPY

通過計算機的組成原理我們知道, 記憶體的讀寫操作是需要 CPU 的協調資料匯流排,地址匯流排和控制匯流排來完成的因此在"拷貝"發生的時候,往往需要 CPU 暫停現有的處理邏輯,來協助記憶體的讀寫,這種我們稱為 CPU COPY。CPU COPY不但佔用了 CPU 資源,還佔用了匯流排的頻寬。

(2)DMA COPY

DMA(DIRECT MEMORY ACCESS) 是現代計算機的重要功能,它有一個重要特點:當需要與外設進行資料交換時, CPU 只需要初始化這個動作便可以繼續執行其他指令,剩下的資料傳輸的動作完全由DMA來完成可以看到 DMA COPY 是可以避免大量的 CPU 中斷的。

4、拷貝過程中會發生什麼?

從核心態到使用者態時會發生上下文切換,上下文切換時指由使用者態切換到核心態, 以及由核心態切換到使用者態。

現在我們對零拷貝有一定的概念基礎了,接下來,讓我們深入去了解一下 Linux 作業系統與 IO 複製之間的來龍去脈。

- 原理篇 -

1、記憶體管理

Linux 記憶體管理結構的歷史:在Linux 核心2.4版本之前,記憶體管理結構中 page cache 和 buffer cache 是分開的,分別是兩個獨立的。

從 Linux 核心2.4 版本開始作業系統在記憶體管理機制進行了優化,才支援了零拷貝機制。

2、Linux記憶體管理結構

Linux 核心的檔案 Cache 管理機制來進行實現的,在 Linux 的實現中,檔案 Cache 分為兩個層面,一是 Page Cache,另一個 Buffer Cache,每一個 Page Cache 包含若干 Buffer Cache。

記憶體管理系統和 VFS 只與 Page Cache 互動,記憶體管理系統負責維護每項 Page Cache 的分配和回收,同時在使用 memory map 方式訪問時負責建立對映。

VFS 負責 Page Cache 與使用者空間的資料交換。而具體檔案系統則一般只與 Buffer Cache 互動,它們負責在外圍儲存裝置和 Buffer Cache 之間交換資料。

標註:VFS(virtual File System) 的作用就是採用標準的 Unix 系統呼叫讀寫位於不同物理介質上的不同檔案系統,即為各類檔案系統提供了一個統一的操作介面和應用程式設計介面。VFS 是一個可以讓open()、read()、write()等系統呼叫不用關心底層的儲存介質和檔案系統型別就可以工作的粘合層。

3、應用上下文與核心上下文共享記憶體互動過程

將 Cache 項對映到使用者空間,使得應用程式可以像使用記憶體指標一樣訪問檔案,Memory map 訪問 Cache 的方式在核心中是採用請求頁面機制實現的:

•當我們應用程式呼叫 mmap(下圖中1),陷入到核心中後呼叫 dommappgoff (圖中2)該函式從應用程式的地址空間中分配一段區域作為對映的記憶體地址,並使用一個 VMA(vmareastruct)結構代表該區域,之後就返回到應用程式(圖中3);

•然後當應用程式訪問 mmap 所返回的地址指標時(圖中4),由於虛實對映尚未建立,會觸發缺頁中斷(圖中5);

•之後系統會呼叫缺頁中斷處理函式(圖中6),在缺頁中斷處理函式中,核心通過相應區域的 VMA 結構判斷出該區域屬於檔案對映,於是呼叫具體檔案系統的介面讀入相應的 Page Cache 項(圖中7、8、9),並填寫相應的虛實對映表;

•經過這些步驟之後,應用程式就可以正常訪問相應的記憶體區域了。

- IO 零拷貝 -

1、存在多次拷貝的原因

作業系統為了保護系統不被應用程式有意或無意地破壞,為作業系統設定了使用者態和核心態兩種狀態,使用者態想要獲取系統資源(例如訪問硬碟),必須通過系統呼叫進入到核心態, 由核心態獲取到系統資源,再切換回使用者態返回應用程式。

出於 "readahead cache" 和非同步寫入等等效能優化的需要, 作業系統在核心態中也增加了一個"核心緩衝區"(kernel buffer)。讀取資料時並不是直接把資料讀取到應用程式的 buffer,而先讀取到 kernel buffer,再由 kernel buffer 複製到應用程式的 buffer。因此,資料在被應用程式使用之前,可能需要被多次拷貝。

2、非零拷貝IO流程

總結所有系統中, 不管是 WEB 應用伺服器, FTP 伺服器,資料庫伺服器, 靜態檔案伺服器等等, 所有涉及到資料傳輸的場景,無非就一種:

——從硬碟上讀取檔案資料, 傳送到網路上去。

這個場景我們簡化為一個模型:

File.read(fileDesc, buf, len);

Socket.send(socket, buf, len);

為了方便描述,上面這兩行程式碼, 我們給它起個名字:read-send模型。

作業系統在實現這個 read-send 模型時,需要有以下步驟:

  1. 應用程式開始讀檔案的操作;

  2. 應用程式發起系統呼叫, 從使用者態切換到核心態(第一次上下文切換);

  3. 核心態中把資料從硬碟檔案讀取到核心中間緩衝區(kernel buf);

  4. 資料從核心中間緩衝區(kernel buf)複製到(使用者態)應用程式緩衝區(app buf),從核心態切換回到使用者態(第二次上下文切換);

  5. 應用程式開始傳送資料到網路上;

  6. 應用程式發起系統呼叫,從使用者態切換到核心態(第三次上下文切換);

  7. 核心中把資料從應用程式(app buf)的緩衝區複製到socket的緩衝區(socket);

  8. 核心中再把資料從socket的緩衝區(socket buf)傳送的網絡卡的緩衝區(NIC buf)上;

  9. 從核心態切換回到使用者態(第四次上下文切換)。

如下圖表示:

由上圖可以很清晰地看到, 一次 read-send 涉及到了四次拷貝:

  1. 硬碟拷貝到核心緩衝區(DMA COPY);

  2. 核心緩衝區拷貝到應用程式緩衝區(CPU COPY);

  3. 應用程式緩衝區拷貝到socket緩衝區(CPU COPY);

  4. socket buf拷貝到網絡卡的buf(DMA COPY)。

其中涉及到2次 CPU 中斷, 還有4次的上下文切換。很明顯,第2次和第3次的的 copy 只是把資料複製到 app buffer 又原封不動的複製回來, 為此帶來了兩次的CPU COPY和兩次上下文切換, 是完全沒有必要的。

Linux 的零拷貝技術就是為了優化掉這兩次不必要的拷貝。

3、sendFile 系統呼叫的IO流程

Linux 核心2.1開始引入一個叫 sendFile 系統呼叫,這個系統呼叫可以在核心態內把資料從核心緩衝區直接複製到套接字(SOCKET)緩衝區內, 從而可以減少上下文的切換和不必要資料的複製。

這個系統呼叫其實就是一個高階 I/O 函式, 函式簽名如下:

#include<sys/sendfile.h>

ssize_t senfile(int out_fd,int in_fd,off_t* offset,size_t count);

  1. out_fd 是寫出的檔案描述符,而且必須是一個 socket;

  2. in_fd 是讀取內容的檔案描述符,必須是一個真實的檔案, 不能是管道或 socket;

  3. offset 是開始讀的位置;

  4. count 是將要讀取的位元組數。

有了sendFile這個系統呼叫後, 我們 read-send 模型就可以簡化為:

  1. 應用程式開始讀檔案的操作;

  2. 應用程式發起系統呼叫, 從使用者態切換到核心態(第一次上下文切換);

  3. 核心態中把資料從硬碟檔案讀取到核心中間緩衝區;

  4. 通過 sendFile,在核心態中把資料從核心緩衝區複製到socket的緩衝區;

  5. 核心中再把資料從 socket 的緩衝區傳送的網絡卡的 buf 上;

  6. 從核心態切換到使用者態(第二次上下文切換)。

如下圖所示:

涉及到資料拷貝變成:

  1. 硬碟拷貝到核心緩衝區(DMA COPY);

  2. 核心緩衝區拷貝到socket緩衝區(CPU COPY);

  3. socket 緩衝區拷貝到網絡卡的buf(DMA COPY)。

可以看到,一次 read-send 模型中, 利用 sendFile 系統呼叫後, 可以將4次資料拷貝減少到3次, 4次上下文切換減少到2次, 2次 CPU 中斷減少到1次。

相對傳統 I/O, 這種零拷貝技術通過減少兩次上下文切換, 1次CPU COPY, 可以將I/O 效能提高50%以上(網路資料, 未親測)。

開篇的概念中說到, 所謂的零拷貝的"零", 是指使用者態和核心態之間的拷貝次數為0, 從這個定義上來說, 現在的這個零拷貝技術已經是真正的"零"了。

然而, 對效能追求極致的偉大的科學家和工程師們並不滿足於此,精益求精的他們對中間第2次的CPU COPY依舊耿耿於懷, 想盡千方百計要去掉這一次沒有必要的資料拷貝和 CPU 中斷。

4、零拷貝的IO流程

支援 scatter-gather 特性的 sendFile 的 IO 流程:

Linux在核心2.4以後的版本中, Linux 核心對 socket 緩衝區描述符做了優化。通過這次優化, sendFile 系統呼叫可以在只複製 kernel buffer 的少量元資訊的基礎上, 把資料直接從 kernel buffer 複製到網絡卡的 buffer 中去,從而避免了從"核心緩衝區"拷貝到"socket緩衝區"的這一次拷貝。

這個優化後的 sendFile, 我們稱之為支援 scatter-gather 特性的 sendFile。

在支援 scatter-gather 特性的 sendFile 的支撐下, 我們的 read-send 模型可以優化為:

  1. 應用程式開始讀檔案的操作;

  2. 應用程式發起系統呼叫, 從使用者態進入到核心態(第一次上下文切換);

  3. 核心態中把資料從硬碟檔案讀取到核心中間緩衝區;

  4. 核心態中把資料在核心緩衝區的位置(offset)和資料大小(size)兩個資訊追加(append)到socket的緩衝區中去;

  5. 網絡卡的buf上根據socekt緩衝區的offset和size從核心緩衝區中直接拷貝資料;

  6. 從核心態返回到使用者態(第二次上下文切換);

這個過程如下圖所示:

最後資料拷貝變成只有兩次DMA COPY:

    1. 硬碟拷貝到核心緩衝區(DMA COPY);

    2. 核心緩衝區拷貝到網絡卡的 buf(DMA COPY);

Java NIO 零拷貝示例

  1 檔案複製

public void copyFile(String from, String to) throws IOException {
    FileChannel fromChannel = new RandomAccessFile(from, "rw").getChannel();
    FileChannel toChannel = new RandomAccessFile(to, "rw").getChannel();

    long position = 0;
    long count = fromChannel.size();

    fromChannel.transferTo(position, count, toChannel);

    fromChannel.close();
    toChannel.close();
}

  2 socket 傳送檔案

 public void testSendfile() throws IOException {
        String host = "192.168.77.130";
        int port = 9026;
        SocketAddress sad = new InetSocketAddress(host, port);
        SocketChannel sc = SocketChannel.open();
        sc.connect(sad);


        String fname = "src/main/java/com/my/App.data";
        FileChannel fc = new FileInputStream(fname).getChannel();
        long start = System.nanoTime();
        long nsent = 0, curnset = 0;
        curnset = fc.transferTo(0, fc.size(), sc);
        System.out.println("傳送的總位元組數:" + curnset + " 耗時(ns):" + (System.nanoTime() - start));
        try {
            sc.close();
            fc.close();
        } catch (IOException e) {
            System.out.println(e);
        }
    }

參考網址:https://mp.weixin.qq.com/s/mWPjFbCVzvuAW3Y9lEQbGg