7 張圖,輕鬆掌握零拷貝原理
前言
大家好,我是龍臺。
零拷貝是老生常談的問題啦,大廠非常喜歡問。比如 Kafka 爲什麼快,RocketMQ 爲什麼快等,都涉及到零拷貝知識點。最近技術討論羣幾個夥伴分享了阿里、蝦皮的面試真題,也都涉及到零拷貝。因此本文將跟大家一起來學習零拷貝原理。
-
什麼是零拷貝
-
傳統的 IO 執行流程
-
零拷貝相關的知識點回顧
-
零拷貝實現的幾種方式
-
java 提供的零拷貝方式
1. 什麼是零拷貝
零拷貝字面上的意思包括兩個,“零” 和 “拷貝”:
-
“拷貝”:就是指數據從一個存儲區域轉移到另一個存儲區域。
-
“零” :表示次數爲 0,它表示拷貝數據的次數爲 0。
合起來,那零拷貝就是不需要將數據從一個存儲區域複製到另一個存儲區域咯。
零拷貝是指計算機執行 IO 操作時,CPU 不需要將數據從一個存儲區域複製到另一個存儲區域,從而可以減少上下文切換以及 CPU 的拷貝時間。它是一種
I/O
操作優化技術。
2. 傳統 IO 的執行流程
做服務端開發的小夥伴,文件下載功能應該實現過不少了吧。如果你實現的是一個 web 程序,前端請求過來,服務端的任務就是:將服務端主機磁盤中的文件從已連接的 socket 發出去。關鍵實現代碼如下:
while((n = read(diskfd, buf, BUF_SIZE)) > 0)
write(sockfd, buf , n);
傳統的 IO 流程,包括 read 和 write 的過程。
-
read
:把數據從磁盤讀取到內核緩衝區,再拷貝到用戶緩衝區 -
write
:先把數據寫入到 socket 緩衝區,最後寫入網卡設備。
流程圖如下:
-
用戶應用進程調用 read 函數,向操作系統發起 IO 調用,上下文從用戶態轉爲內核態(切換 1)
-
DMA 控制器把數據從磁盤中,讀取到內核緩衝區。
-
CPU 把內核緩衝區數據,拷貝到用戶應用緩衝區,上下文從內核態轉爲用戶態(切換 2),read 函數返回
-
用戶應用進程通過 write 函數,發起 IO 調用,上下文從用戶態轉爲內核態(切換 3)
-
CPU 將用戶緩衝區中的數據,拷貝到 socket 緩衝區
-
DMA 控制器把數據從 socket 緩衝區,拷貝到網卡設備,上下文從內核態切換回用戶態(切換 4),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 上下文的切換。
CPU 寄存器裏原來用戶態的指令位置,需要先保存起來。接着,爲了執行內核態代碼,CPU 寄存器需要更新爲內核態指令的新位置。最後纔是跳轉到內核態運行內核任務。
3.4 虛擬內存
現代操作系統使用虛擬內存,即虛擬地址取代物理地址,使用虛擬內存可以有 2 個好處:
-
虛擬內存空間可以遠遠大於物理內存空間
-
多個虛擬內存可以指向同一個物理地址
正是多個虛擬內存可以指向同一個物理地址,可以把內核空間和用戶空間的虛擬地址映射到同一個物理地址,這樣的話,就可以減少 IO 的數據拷貝次數啦,示意圖如下
3.5 DMA 技術
DMA,英文全稱是 Direct Memory Access,即直接內存訪問。DMA 本質上是一塊主板上獨立的芯片,允許外設設備和內存存儲器之間直接進行 IO 數據傳輸,其過程不需要 CPU 的參與。
我們一起來看下 IO 流程,DMA 幫忙做了什麼事情.
-
用戶應用進程調用 read 函數,向操作系統發起 IO 調用,進入阻塞狀態,等待數據返回。
-
CPU 收到指令後,對 DMA 控制器發起指令調度。
-
DMA 收到 IO 請求後,將請求發送給磁盤;
-
磁盤將數據放入磁盤控制緩衝區,並通知 DMA
-
DMA 將數據從磁盤控制器緩衝區拷貝到內核緩衝區。
-
DMA 向 CPU 發出數據讀完的信號,把工作交換給 CPU,由 CPU 負責將數據從內核緩衝區拷貝到用戶緩衝區。
-
用戶應用進程由內核態切換回用戶態,解除阻塞狀態
可以發現,DMA 做的事情很清晰啦,它主要就是幫忙 CPU 轉發一下 IO 請求,以及拷貝數據。爲什麼需要它的?
主要就是效率,它幫忙 CPU 做事情,這時候,CPU 就可以閒下來去做別的事情,提高了 CPU 的利用效率。大白話解釋就是,CPU 老哥太忙太累啦,所以他找了個小弟(名叫 DMA) ,替他完成一部分的拷貝工作,這樣 CPU 老哥就能着手去做其他事情。
4. 零拷貝實現的幾種方式
零拷貝並不是沒有拷貝數據,而是減少用戶態 / 內核態的切換次數以及 CPU 拷貝的次數。零拷貝實現有多種方式,分別是
-
mmap+write
-
sendfile
-
帶有 DMA 收集拷貝功能的 sendfile
4.1 mmap+write 實現的零拷貝
mmap 的函數原型如下:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
-
addr:指定映射的虛擬內存地址
-
length:映射的長度
-
prot:映射內存的保護模式
-
flags:指定映射的類型
-
fd: 進行映射的文件句柄
-
offset: 文件偏移量
前面一小節,零拷貝相關的知識點回顧,我們介紹了虛擬內存,可以把內核空間和用戶空間的虛擬地址映射到同一個物理地址,從而減少數據拷貝次數!mmap 就是用了虛擬內存這個特點,它將內核中的讀緩衝區與用戶空間的緩衝區進行映射,所有的 IO 都在內核中完成。
mmap+write
實現的零拷貝流程如下:
-
用戶進程通過
mmap方法
向操作系統內核發起 IO 調用,上下文從用戶態切換爲內核態。 -
CPU 利用 DMA 控制器,把數據從硬盤中拷貝到內核緩衝區。
-
上下文從內核態切換回用戶態,mmap 方法返回。
-
用戶進程通過
write
方法向操作系統內核發起 IO 調用,上下文從用戶態切換爲內核態。 -
CPU 將內核緩衝區的數據拷貝到的 socket 緩衝區。
-
CPU 利用 DMA 控制器,把數據從 socket 緩衝區拷貝到網卡,上下文從內核態切換回用戶態,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);
-
out_fd: 爲待寫入內容的文件描述符,一個 socket 描述符。,
-
in_fd: 爲待讀出內容的文件描述符,必須是真實的文件,不能是 socket 和管道。
-
offset:指定從讀入文件的哪個位置開始讀,如果爲 NULL,表示文件的默認起始位置。
-
count:指定在 fdout 和 fdin 之間傳輸的字節數。
sendfile 表示在兩個文件描述符之間傳輸數據,它是在操作系統內核中操作的,避免了數據從內核緩衝區和用戶緩衝區之間的拷貝操作,因此可以使用它來實現零拷貝。
sendfile 實現的零拷貝流程如下:
sendfile 實現的零拷貝
-
用戶進程發起 sendfile 系統調用,上下文(切換 1)從用戶態轉向內核態
-
DMA 控制器,把數據從硬盤中拷貝到內核緩衝區。
-
CPU 將讀緩衝區中數據拷貝到 socket 緩衝區
-
DMA 控制器,異步把數據從 socket 緩衝區拷貝到網卡,
-
上下文(切換 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 實現的零拷貝流程如下:
-
用戶進程發起 sendfile 系統調用,上下文(切換 1)從用戶態轉向內核態
-
DMA 控制器,把數據從硬盤中拷貝到內核緩衝區。
-
CPU 把內核緩衝區中的文件描述符信息(包括內核緩衝區的內存地址和偏移量)發送到 socket 緩衝區
-
DMA 控制器根據文件描述符信息,直接把數據從內核緩衝區拷貝到網卡
-
上下文(切換 2)從內核態切換回用戶態,sendfile 調用返回。
可以發現,sendfile+DMA scatter/gather
實現的零拷貝,I/O 發生了 2 次用戶空間與內核空間的上下文切換,以及 2 次數據拷貝。其中 2 次數據拷貝都是包 DMA 拷貝。這就是真正的 零拷貝(Zero-copy) 技術,全程都沒有通過 CPU 來搬運數據,所有的數據都是通過 DMA 來進行傳輸的。
5. java 提供的零拷貝方式
-
Java NIO 對 mmap 的支持
-
Java NIO 對 sendfile 的支持
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());
}
}
}
參考與感謝
-
框架篇:小白也能秒懂的 Linux 零拷貝原理 [1]
-
深入剖析 Linux IO 原理和幾種零拷貝機制的實現 [2]
[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