7 張圖,輕鬆掌握零拷貝原理

前言

大家好,我是龍臺

零拷貝是老生常談的問題啦,大廠非常喜歡問。比如 Kafka 爲什麼快,RocketMQ 爲什麼快等,都涉及到零拷貝知識點。最近技術討論羣幾個夥伴分享了阿里、蝦皮的面試真題,也都涉及到零拷貝。因此本文將跟大家一起來學習零拷貝原理。

  1. 什麼是零拷貝

  2. 傳統的 IO 執行流程

  3. 零拷貝相關的知識點回顧

  4. 零拷貝實現的幾種方式

  5. java 提供的零拷貝方式

1. 什麼是零拷貝

零拷貝字面上的意思包括兩個,“零” 和 “拷貝”:

合起來,那零拷貝就是不需要將數據從一個存儲區域複製到另一個存儲區域咯。

零拷貝是指計算機執行 IO 操作時,CPU 不需要將數據從一個存儲區域複製到另一個存儲區域,從而可以減少上下文切換以及 CPU 的拷貝時間。它是一種I/O操作優化技術。

2. 傳統 IO 的執行流程

做服務端開發的小夥伴,文件下載功能應該實現過不少了吧。如果你實現的是一個 web 程序,前端請求過來,服務端的任務就是:將服務端主機磁盤中的文件從已連接的 socket 發出去。關鍵實現代碼如下:

while((n = read(diskfd, buf, BUF_SIZE)) > 0)
    write(sockfd, buf , n);

傳統的 IO 流程,包括 read 和 write 的過程。

流程圖如下:

從流程圖可以看出,傳統 IO 的讀寫流程,包括了 4 次上下文切換(4 次用戶態和內核態的切換),4 次數據拷貝(兩次 CPU 拷貝以及兩次的 DMA 拷貝),什麼是 DMA 拷貝呢?我們一起來回顧下,零拷貝涉及的操作系統知識點哈。

3. 零拷貝相關的知識點回顧

3.1 內核空間和用戶空間

我們電腦上跑着的應用程序,其實是需要經過操作系統,才能做一些特殊操作,如磁盤文件讀寫、內存的讀寫等等。因爲這些都是比較危險的操作,不可以由應用程序亂來,只能交給底層操作系統來。

因此,操作系統爲每個進程都分配了內存空間,一部分是用戶空間,一部分是內核空間。內核空間是操作系統內核訪問的區域,是受保護的內存空間,而用戶空間是用戶應用程序訪問的內存區域。 以 32 位操作系統爲例,它會爲每一個進程都分配了 4G(2 的 32 次方) 的內存空間。

3.2 什麼是用戶態、內核態

3.3 什麼是上下文切換

CPU 寄存器,是 CPU 內置的容量小、但速度極快的內存。而程序計數器,則是用來存儲 CPU 正在執行的指令位置、或者即將執行的下一條指令位置。它們都是 CPU 在運行任何任務前,必須的依賴環境,因此叫做 CPU 上下文。

它是指,先把前一個任務的 CPU 上下文(也就是 CPU 寄存器和程序計數器)保存起來,然後加載新任務的上下文到這些寄存器和程序計數器,最後再跳轉到程序計數器所指的新位置,運行新任務。

一般我們說的上下文切換,就是指內核(操作系統的核心)在 CPU 上對進程或者線程進行切換。進程從用戶態到內核態的轉變,需要通過系統調用來完成。系統調用的過程,會發生 CPU 上下文的切換

CPU 寄存器裏原來用戶態的指令位置,需要先保存起來。接着,爲了執行內核態代碼,CPU 寄存器需要更新爲內核態指令的新位置。最後纔是跳轉到內核態運行內核任務。

3.4 虛擬內存

現代操作系統使用虛擬內存,即虛擬地址取代物理地址,使用虛擬內存可以有 2 個好處:

正是多個虛擬內存可以指向同一個物理地址,可以把內核空間和用戶空間的虛擬地址映射到同一個物理地址,這樣的話,就可以減少 IO 的數據拷貝次數啦,示意圖如下

3.5 DMA 技術

DMA,英文全稱是 Direct Memory Access,即直接內存訪問。DMA 本質上是一塊主板上獨立的芯片,允許外設設備和內存存儲器之間直接進行 IO 數據傳輸,其過程不需要 CPU 的參與

我們一起來看下 IO 流程,DMA 幫忙做了什麼事情.

可以發現,DMA 做的事情很清晰啦,它主要就是幫忙 CPU 轉發一下 IO 請求,以及拷貝數據。爲什麼需要它的?

主要就是效率,它幫忙 CPU 做事情,這時候,CPU 就可以閒下來去做別的事情,提高了 CPU 的利用效率。大白話解釋就是,CPU 老哥太忙太累啦,所以他找了個小弟(名叫 DMA) ,替他完成一部分的拷貝工作,這樣 CPU 老哥就能着手去做其他事情。

4. 零拷貝實現的幾種方式

零拷貝並不是沒有拷貝數據,而是減少用戶態 / 內核態的切換次數以及 CPU 拷貝的次數。零拷貝實現有多種方式,分別是

4.1 mmap+write 實現的零拷貝

mmap 的函數原型如下:

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

前面一小節,零拷貝相關的知識點回顧,我們介紹了虛擬內存,可以把內核空間和用戶空間的虛擬地址映射到同一個物理地址,從而減少數據拷貝次數!mmap 就是用了虛擬內存這個特點,它將內核中的讀緩衝區與用戶空間的緩衝區進行映射,所有的 IO 都在內核中完成。

mmap+write實現的零拷貝流程如下:

可以發現,mmap+write實現的零拷貝,I/O 發生了 4 次用戶空間與內核空間的上下文切換,以及 3 次數據拷貝。其中 3 次數據拷貝中,包括了 2 次 DMA 拷貝和 1 次 CPU 拷貝

mmap是將讀緩衝區的地址和用戶緩衝區的地址進行映射,內核緩衝區和應用緩衝區共享,所以節省了一次 CPU 拷貝‘’並且用戶進程內存是虛擬的,只是映射到內核的讀緩衝區,可以節省一半的內存空間。

4.2 sendfile 實現的零拷貝

sendfile是 Linux2.1 內核版本後引入的一個系統調用函數,API 如下:

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

sendfile 表示在兩個文件描述符之間傳輸數據,它是在操作系統內核中操作的,避免了數據從內核緩衝區和用戶緩衝區之間的拷貝操作,因此可以使用它來實現零拷貝。

sendfile 實現的零拷貝流程如下:

sendfile 實現的零拷貝

  1. 用戶進程發起 sendfile 系統調用,上下文(切換 1)從用戶態轉向內核態

  2. DMA 控制器,把數據從硬盤中拷貝到內核緩衝區。

  3. CPU 將讀緩衝區中數據拷貝到 socket 緩衝區

  4. DMA 控制器,異步把數據從 socket 緩衝區拷貝到網卡,

  5. 上下文(切換 2)從內核態切換回用戶態,sendfile 調用返回。

可以發現,sendfile實現的零拷貝,I/O 發生了 2 次用戶空間與內核空間的上下文切換,以及 3 次數據拷貝。其中 3 次數據拷貝中,包括了 2 次 DMA 拷貝和 1 次 CPU 拷貝。那能不能把 CPU 拷貝的次數減少到 0 次呢?有的,即帶有DMA收集拷貝功能的sendfile

4.3 sendfile+DMA scatter/gather 實現的零拷貝

linux 2.4 版本之後,對sendfile做了優化升級,引入 SG-DMA 技術,其實就是對 DMA 拷貝加入了scatter/gather操作,它可以直接從內核空間緩衝區中將數據讀取到網卡。使用這個特點搞零拷貝,即還可以多省去一次 CPU 拷貝

sendfile+DMA scatter/gather 實現的零拷貝流程如下:

  1. 用戶進程發起 sendfile 系統調用,上下文(切換 1)從用戶態轉向內核態

  2. DMA 控制器,把數據從硬盤中拷貝到內核緩衝區。

  3. CPU 把內核緩衝區中的文件描述符信息(包括內核緩衝區的內存地址和偏移量)發送到 socket 緩衝區

  4. DMA 控制器根據文件描述符信息,直接把數據從內核緩衝區拷貝到網卡

  5. 上下文(切換 2)從內核態切換回用戶態,sendfile 調用返回。

可以發現,sendfile+DMA scatter/gather實現的零拷貝,I/O 發生了 2 次用戶空間與內核空間的上下文切換,以及 2 次數據拷貝。其中 2 次數據拷貝都是包 DMA 拷貝。這就是真正的 零拷貝(Zero-copy) 技術,全程都沒有通過 CPU 來搬運數據,所有的數據都是通過 DMA 來進行傳輸的。

5. java 提供的零拷貝方式

5.1 Java NIO 對 mmap 的支持

Java NIO 有一個MappedByteBuffer的類,可以用來實現內存映射。它的底層是調用了 Linux 內核的 mmap 的 API。

mmap 的小 demo 如下:

public class MmapTest {

    public static void main(String[] args) {
        try {
            FileChannel readChannel = FileChannel.open(Paths.get("./jay.txt"), StandardOpenOption.READ);
            MappedByteBuffer data = readChannel.map(FileChannel.MapMode.READ_ONLY, 0, 1024 * 1024 * 40);
            FileChannel writeChannel = FileChannel.open(Paths.get("./siting.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
            //數據傳輸
            writeChannel.write(data);
            readChannel.close();
            writeChannel.close();
        }catch (Exception e){
            System.out.println(e.getMessage());
        }
    }
}

5.2 Java NIO 對 sendfile 的支持

FileChannel 的transferTo()/transferFrom(),底層就是 sendfile() 系統調用函數。Kafka 這個開源項目就用到它,平時面試的時候,回答面試官爲什麼這麼快,就可以提到零拷貝sendfile這個點。

@Override
public long transferFrom(FileChannel fileChannel, long position, long count) throws IOException {
   return fileChannel.transferTo(position, count, socketChannel);
}

sendfile 的小 demo 如下:

public class SendFileTest {
    public static void main(String[] args) {
        try {
            FileChannel readChannel = FileChannel.open(Paths.get("./jay.txt"), StandardOpenOption.READ);
            long len = readChannel.size();
            long position = readChannel.position();
            
            FileChannel writeChannel = FileChannel.open(Paths.get("./siting.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
            //數據傳輸
            readChannel.transferTo(position, len, writeChannel);
            readChannel.close();
            writeChannel.close();
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }
}

參考與感謝

[1] 框架篇:小白也能秒懂的 Linux 零拷貝原理: https://juejin.cn/post/6887469050515947528

[2] 深入剖析 Linux IO 原理和幾種零拷貝機制的實現: https://juejin.cn/post/6844903949359644680#heading-11

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/_AJOWfI5RK5jPGMT50BiOw