零複製技術 Zero-Copy 是指計算機執行操作時,可以直接從源(如檔案或網路套接字)將數據傳輸到目標緩衝區, 而不需要 CPU 先將資料從某處記憶體複製到另一個特定區域,從而減少上下文切換以及 CPU 的複製時間。
1 I/O 中斷原理
在 DMA 技術出現之前,應用程式與磁碟之間的 I/O 操作都是透過 CPU 的中斷完成的。
使用者程序向 CPU 發起 read 系統呼叫讀取資料,由使用者態切換為核心態,然後一直阻塞等待資料的返回。
CPU 在接收到指令以後對磁碟發起 I/O 請求,將磁碟資料先放入磁碟控制器緩衝區。
資料準備完成以後,磁碟向 CPU 發起 I/O 中斷。
CPU 收到 I/O 中斷以後將磁碟緩衝區中的資料複製到核心緩衝區,然後再從核心緩衝區複製到使用者緩衝區。
使用者程序由核心態切換回使用者態,解除阻塞狀態,然後等待 CPU 的下一個執行時間鍾。
可以看到,整個資料的傳輸過程,都要需要 CPU 親自參與搬運資料的過程,而且這個過程,CPU 是不能做其他事情的。
假如透過千兆網絡卡或者磁碟傳輸大量資料時,使用 CPU 來複製的話,對 CPU 資源是種極大的消耗。
2 DMA 方式
DMA 的全稱是直接記憶體訪問(Direct Memory Access),是一種硬體裝置繞開 CPU 獨立直接訪問記憶體的機制。
目前支援 DMA 的硬體包括:網絡卡、音效卡、顯示卡、磁碟控制器等。
基於 DMA 訪問方式,系統主記憶體於硬碟或網絡卡之間的數據傳輸可以繞開 CPU 的全程排程,資料搬運的工作交給 DMA 控制器,在傳輸過程中,CPU 可以繼續處理其他的工作,提升系統的資源利用率。
使用者程序向 CPU 發起 read 系統呼叫讀取資料,使用者態切換為核心態,然後一直阻塞等待資料的返回。
CPU 在接收到指令以後對 DMA 磁碟控制器發起排程指令。
DMA 磁碟控制器對磁碟發起 I/O 請求,將磁碟資料先複製到磁碟控制器緩衝區,CPU 全程不參與此過程。
資料讀取完成後,DMA 磁碟控制器會接受到磁碟的通知,將資料從磁碟控制器緩衝區複製到核心緩衝區。
DMA 磁碟控制器向 CPU 發出資料讀完的訊號,由 CPU 負責將資料從核心緩衝區複製到使用者緩衝區。
使用者程序由核心態切換回使用者態,解除阻塞狀態,然後等待 CPU 的下一個執行時間鍾。
3 傳統 I/O 方式
爲了理解零複製技術的思路,首先了解一下傳統 I/O 方式存在的問題。
Linux 系統中,傳統的訪問方式是透過 write() 和 read() 兩個系統呼叫實現的,透過 read() 函式讀取檔案到到快取區中,然後透過 write() 方法把快取中的資料輸出到網路埠 。
下圖分別對應傳統 I/O 操作的資料讀寫流程,整個過程涉及 4 次上下文切換、 2 次 CPU 複製、2 次 DMA 複製總共 4 次複製。
4 次上下文切換
因為發生了兩次系統呼叫,一次是 read()
,一次是 write()
,使用者程式向核心發起系統呼叫時,CPU 將使用者程序從使用者態切換到核心態;當系統呼叫返回時,CPU 將使用者程序從核心態切換回使用者態。
4 次 資料複製
第一次 DMA 複製,將磁碟上的資料複製到作業系統核心的緩衝區 ;
第二次 CPU 複製,將核心緩衝區的資料複製到使用者的緩衝區裡;
第三次 CPU 複製,將使用者緩衝區的資料,複製到核心 Socket 緩衝區;
第四次 DMA 複製,將核心 Socket 緩衝區的資料複製到網絡卡的緩衝區。
綜上,想要提升檔案傳輸的效能,因此我們需要減少「上下文切換」和「資料複製」的次數。
3 零複製方式
零複製技術實現的方式通常有 2 種:mmap + write 、sendfile、sendfile + DMA scatter-gather 。
3.1 mmap + write
mmap 是 Linux 提供的一種記憶體對映檔案的機制,它實現了將核心中讀緩衝區地址與使用者空間緩衝區地址進行對映,從而實現核心緩衝區與使用者緩衝區的共享。
基於 mmap + write 系統呼叫的零複製方式,整個複製過程會發生 4 次上下文切換,1 次 CPU 複製和 2 次 DMA 複製。
使用者程式讀寫資料的流程如下:
使用者程序透過 mmap() 函式向核心發起系統呼叫,上下文從使用者態切換為核心態。
將使用者程序的核心空間的讀緩衝區與使用者空間的快取區進行記憶體地址對映。
CPU 利用 DMA 控制器將資料從主存或硬碟複製到核心空間的讀緩衝區。
上下文從核心態切換回使用者態,mmap 系統呼叫執行返回。
使用者程序透過 write() 函式向核心發起系統呼叫,上下文從使用者態切換為核心態。
CPU 將讀緩衝區中的資料複製到的網路緩衝區。
CPU 利用 DMA 控制器將資料從網路緩衝區(socket buffer)複製到網絡卡進行數據傳輸。
上下文從核心態切換回使用者態,write 系統呼叫執行返回。
mmap 的複製雖然減少了 1 次 CPU 複製,提升了效率,但也存在一些隱藏的問題。
當 mmap 一個檔案時,如果這個檔案被另一個程序所截獲,那麼 write 系統呼叫會因為訪問非法地址被 SIGBUS 訊號終止,SIGBUS 預設會殺死程序併產生一個 coredump,伺服器可能因此被終止。
3.2 sendfile
sendfile 系統呼叫在 Linux 核心版本 2.1 中被引入,目的是簡化透過網路在兩個通道之間進行的數據傳輸過程。
透過 sendfile 系統呼叫,資料可以直接在核心空間內部進行 I/O 傳輸,從而省去了資料在使用者空間和核心空間之間的來回複製。
sendfile 系統呼叫的引入,不僅減少了 CPU 複製的次數,還減少了上下文切換的次數,它的虛擬碼如下:
基於 sendfile 系統呼叫的零複製方式,整個複製過程會發生 2 次上下文切換,1 次 CPU 複製和 2 次 DMA 複製。
使用者程式讀寫資料的流程如下:
使用者程序透過 sendfile() 函式向核心發起系統呼叫,上下文從使用者態切換為核心態。
CPU 利用 DMA 控制器將資料從主存或硬碟複製到核心空間的讀緩衝區。
CPU 將讀緩衝區中的資料複製到的網路緩衝區。
CPU 利用 DMA 控制器將資料從網路緩衝區複製到網絡卡進行數據傳輸。
上下文從核心態切換回使用者態,sendfile 系統呼叫執行返回。
相比較於 mmap 記憶體對映的方式,sendfile 少了 2 次上下文切換,但是仍然有 1 次 CPU 複製操作。
3.3 sendfile + DMA gather copy
Linux 2.4 版本的核心對 sendfile 系統呼叫進行修改,為 DMA 複製引入了 gather 操作。
它將核心空間的讀緩衝區中對應的資料描述資訊(記憶體地址、地址偏移量)記錄到相應的網路緩衝區中,由 DMA 根據記憶體地址、地址偏移量將資料批次地從讀緩衝區複製到網絡卡裝置中,這樣就省去了核心空間中僅剩的 1 次 CPU 複製操作。
在硬體的支援下,sendfile 複製方式不再從核心緩衝區的資料複製到 socket 緩衝區,取而代之的僅僅是緩衝區檔案描述符和資料長度的複製,這樣 DMA 引擎直接利用 gather 操作將頁快取中資料打包傳送到網路中即可,本質就是和虛擬記憶體對映的思路類似。
基於 sendfile + DMA gather copy 系統呼叫的零複製方式,整個複製過程會發生 2 次上下文切換、0 次 CPU 複製以及 2 次 DMA 複製,使用者程式讀寫資料的流程如下:
使用者程序透過 sendfile() 函式向核心發起系統呼叫,上下文從使用者態切換為核心態。
CPU 利用 DMA 控制器將資料從主存或硬碟複製到核心空間的讀緩衝區。
CPU 把讀緩衝區的檔案描述符(file descriptor)和資料長度複製到網路緩衝區。
基於已複製的檔案描述符和資料長度,CPU 利用 DMA 控制器的 gather/scatter 操作直接批次地將資料從核心的讀緩衝區複製到網絡卡進行數據傳輸。
上下文從核心態切換回使用者態,sendfile 系統呼叫執行返回。
5 寫到最後
無論是傳統 I/O 複製方式還是引入零複製的方式,2 次 DMA 複製是都少不了的,因為兩次 DMA 都是依賴硬體完成的。
複製方式 | CPU複製 | DMA複製 | 系統呼叫 | 上下文切換 |
---|---|---|---|---|
傳統方式(read + write) | 2 | 2 | read / write | 4 |
記憶體對映(mmap + write) | 1 | 2 | mmap / write | 4 |
sendfile | 1 | 2 | sendfile | 2 |
sendfile + DMA gather copy | 0 | 2 | sendfile | 2 |
RocketMQ 選擇了 mmap + write 這種零複製方式,適用於業務級訊息這種小塊檔案的資料持久化和傳輸;
而 Kafka 採用的是 sendfile 這種零複製方式,適用於系統日誌訊息這種高吞吐量的大塊檔案的資料持久化和傳輸。
但是值得注意的一點是,Kafka 的索引檔案使用的是 mmap + write 方式,資料檔案使用的是 sendfile 方式。
作者:勇哥Java實戰
連結:https://juejin.cn/post/7435615450409992246