Java Nio,Netty,Kafka 中經常提到“零拷貝”到底是什么?
零拷貝技術 Zero-Copy 是指計算機執(zhí)行操作時,可以直接從源(如文件或網(wǎng)絡套接字)將數(shù)據(jù)傳輸?shù)侥繕司彌_區(qū), 而不需要 CPU 先將數(shù)據(jù)從某處內存復制到另一個特定區(qū)域,從而減少上下文切換以及 CPU 的拷貝時間。
1 I/O 中斷原理
在 DMA 技術出現(xiàn)之前,應用程序與磁盤之間的 I/O 操作都是通過 CPU 的中斷完成的。
圖片
- 用戶進程向 CPU 發(fā)起 read 系統(tǒng)調用讀取數(shù)據(jù),由用戶態(tài)切換為內核態(tài),然后一直阻塞等待數(shù)據(jù)的返回。
- CPU 在接收到指令以后對磁盤發(fā)起 I/O 請求,將磁盤數(shù)據(jù)先放入磁盤控制器緩沖區(qū)。
- 數(shù)據(jù)準備完成以后,磁盤向 CPU 發(fā)起 I/O 中斷。
- CPU 收到 I/O 中斷以后將磁盤緩沖區(qū)中的數(shù)據(jù)拷貝到內核緩沖區(qū),然后再從內核緩沖區(qū)拷貝到用戶緩沖區(qū)。
- 用戶進程由內核態(tài)切換回用戶態(tài),解除阻塞狀態(tài),然后等待 CPU 的下一個執(zhí)行時間鐘。
可以看到,整個數(shù)據(jù)的傳輸過程,都要需要 CPU 親自參與搬運數(shù)據(jù)的過程,而且這個過程,CPU 是不能做其他事情的。
假如通過千兆網(wǎng)卡或者磁盤傳輸大量數(shù)據(jù)時,使用 CPU 來拷貝的話,對 CPU 資源是種極大的消耗。
2 DMA 方式
DMA 的全稱是直接內存訪問(Direct Memory Access),是一種硬件設備繞開 CPU 獨立直接訪問內存的機制。
目前支持 DMA 的硬件包括:網(wǎng)卡、聲卡、顯卡、磁盤控制器等。
圖片
基于 DMA 訪問方式,系統(tǒng)主內存于硬盤或網(wǎng)卡之間的數(shù)據(jù)傳輸可以繞開 CPU 的全程調度,數(shù)據(jù)搬運的工作交給 DMA 控制器,在傳輸過程中,CPU 可以繼續(xù)處理其他的工作,提升系統(tǒng)的資源利用率。
圖片
- 用戶進程向 CPU 發(fā)起 read 系統(tǒng)調用讀取數(shù)據(jù),用戶態(tài)切換為內核態(tài),然后一直阻塞等待數(shù)據(jù)的返回。
- CPU 在接收到指令以后對 DMA 磁盤控制器發(fā)起調度指令。
- DMA 磁盤控制器對磁盤發(fā)起 I/O 請求,將磁盤數(shù)據(jù)先拷貝到磁盤控制器緩沖區(qū),CPU 全程不參與此過程。
- 數(shù)據(jù)讀取完成后,DMA 磁盤控制器會接受到磁盤的通知,將數(shù)據(jù)從磁盤控制器緩沖區(qū)拷貝到內核緩沖區(qū)。
- DMA 磁盤控制器向 CPU 發(fā)出數(shù)據(jù)讀完的信號,由 CPU 負責將數(shù)據(jù)從內核緩沖區(qū)拷貝到用戶緩沖區(qū)。
- 用戶進程由內核態(tài)切換回用戶態(tài),解除阻塞狀態(tài),然后等待 CPU 的下一個執(zhí)行時間鐘。
3 傳統(tǒng) I/O 方式
為了理解零拷貝技術的思路,首先了解一下傳統(tǒng) I/O 方式存在的問題。
Linux 系統(tǒng)中,傳統(tǒng)的訪問方式是通過 write() 和 read() 兩個系統(tǒng)調用實現(xiàn)的,通過 read() 函數(shù)讀取文件到到緩存區(qū)中,然后通過 write() 方法把緩存中的數(shù)據(jù)輸出到網(wǎng)絡端口 。
圖片
下圖分別對應傳統(tǒng) I/O 操作的數(shù)據(jù)讀寫流程,整個過程涉及 4 次上下文切換、 2 次 CPU 拷貝、2 次 DMA 拷貝總共 4 次拷貝。
圖片
- 4 次上下文切換
因為發(fā)生了兩次系統(tǒng)調用,一次是 read()
,一次是 write()
,用戶程序向內核發(fā)起系統(tǒng)調用時,CPU 將用戶進程從用戶態(tài)切換到內核態(tài);當系統(tǒng)調用返回時,CPU 將用戶進程從內核態(tài)切換回用戶態(tài)。
- 4 次 數(shù)據(jù)拷貝
- 第一次 DMA 拷貝,將磁盤上的數(shù)據(jù)拷貝到操作系統(tǒng)內核的緩沖區(qū) ;
- 第二次 CPU 拷貝,將內核緩沖區(qū)的數(shù)據(jù)拷貝到用戶的緩沖區(qū)里;
- 第三次 CPU 拷貝,將用戶緩沖區(qū)的數(shù)據(jù),拷貝到內核 Socket 緩沖區(qū);
- 第四次 DMA 拷貝,將內核 Socket 緩沖區(qū)的數(shù)據(jù)拷貝到網(wǎng)卡的緩沖區(qū)。
綜上,想要提升文件傳輸?shù)男阅?,因此我們需要減少「上下文切換」和「數(shù)據(jù)拷貝」的次數(shù)。
3 零拷貝方式
零拷貝技術實現(xiàn)的方式通常有 2 種:mmap + write 、sendfile、sendfile + DMA scatter-gather 。
3.1 mmap + write
mmap 是 Linux 提供的一種內存映射文件的機制,它實現(xiàn)了將內核中讀緩沖區(qū)地址與用戶空間緩沖區(qū)地址進行映射,從而實現(xiàn)內核緩沖區(qū)與用戶緩沖區(qū)的共享。
圖片
基于 mmap + write 系統(tǒng)調用的零拷貝方式,整個拷貝過程會發(fā)生 4 次上下文切換,1 次 CPU 拷貝和 2 次 DMA 拷貝。
圖片
用戶程序讀寫數(shù)據(jù)的流程如下:
- 用戶進程通過 mmap() 函數(shù)向內核發(fā)起系統(tǒng)調用,上下文從用戶態(tài)切換為內核態(tài)。
- 將用戶進程的內核空間的讀緩沖區(qū)與用戶空間的緩存區(qū)進行內存地址映射。
- CPU 利用 DMA 控制器將數(shù)據(jù)從主存或硬盤拷貝到內核空間的讀緩沖區(qū)。
- 上下文從內核態(tài)切換回用戶態(tài),mmap 系統(tǒng)調用執(zhí)行返回。
- 用戶進程通過 write() 函數(shù)向內核發(fā)起系統(tǒng)調用,上下文從用戶態(tài)切換為內核態(tài)。
- CPU 將讀緩沖區(qū)中的數(shù)據(jù)拷貝到的網(wǎng)絡緩沖區(qū)。
- CPU 利用 DMA 控制器將數(shù)據(jù)從網(wǎng)絡緩沖區(qū)(socket buffer)拷貝到網(wǎng)卡進行數(shù)據(jù)傳輸。
- 上下文從內核態(tài)切換回用戶態(tài),write 系統(tǒng)調用執(zhí)行返回。
mmap 的拷貝雖然減少了 1 次 CPU 拷貝,提升了效率,但也存在一些隱藏的問題。
當 mmap 一個文件時,如果這個文件被另一個進程所截獲,那么 write 系統(tǒng)調用會因為訪問非法地址被 SIGBUS 信號終止,SIGBUS 默認會殺死進程并產生一個 coredump,服務器可能因此被終止。
3.2 sendfile
sendfile 系統(tǒng)調用在 Linux 內核版本 2.1 中被引入,目的是簡化通過網(wǎng)絡在兩個通道之間進行的數(shù)據(jù)傳輸過程。
通過 sendfile 系統(tǒng)調用,數(shù)據(jù)可以直接在內核空間內部進行 I/O 傳輸,從而省去了數(shù)據(jù)在用戶空間和內核空間之間的來回拷貝。
sendfile 系統(tǒng)調用的引入,不僅減少了 CPU 拷貝的次數(shù),還減少了上下文切換的次數(shù),它的偽代碼如下:
圖片
基于 sendfile 系統(tǒng)調用的零拷貝方式,整個拷貝過程會發(fā)生 2 次上下文切換,1 次 CPU 拷貝和 2 次 DMA 拷貝。
圖片
用戶程序讀寫數(shù)據(jù)的流程如下:
- 用戶進程通過 sendfile() 函數(shù)向內核發(fā)起系統(tǒng)調用,上下文從用戶態(tài)切換為內核態(tài)。
- CPU 利用 DMA 控制器將數(shù)據(jù)從主存或硬盤拷貝到內核空間的讀緩沖區(qū)。
- CPU 將讀緩沖區(qū)中的數(shù)據(jù)拷貝到的網(wǎng)絡緩沖區(qū)。
- CPU 利用 DMA 控制器將數(shù)據(jù)從網(wǎng)絡緩沖區(qū)拷貝到網(wǎng)卡進行數(shù)據(jù)傳輸。
- 上下文從內核態(tài)切換回用戶態(tài),sendfile 系統(tǒng)調用執(zhí)行返回。
相比較于 mmap 內存映射的方式,sendfile 少了 2 次上下文切換,但是仍然有 1 次 CPU 拷貝操作。
3.3 sendfile + DMA gather copy
Linux 2.4 版本的內核對 sendfile 系統(tǒng)調用進行修改,為 DMA 拷貝引入了 gather 操作。
它將內核空間的讀緩沖區(qū)中對應的數(shù)據(jù)描述信息(內存地址、地址偏移量)記錄到相應的網(wǎng)絡緩沖區(qū)中,由 DMA 根據(jù)內存地址、地址偏移量將數(shù)據(jù)批量地從讀緩沖區(qū)拷貝到網(wǎng)卡設備中,這樣就省去了內核空間中僅剩的 1 次 CPU 拷貝操作。
圖片
在硬件的支持下,sendfile 拷貝方式不再從內核緩沖區(qū)的數(shù)據(jù)拷貝到 socket 緩沖區(qū),取而代之的僅僅是緩沖區(qū)文件描述符和數(shù)據(jù)長度的拷貝,這樣 DMA 引擎直接利用 gather 操作將頁緩存中數(shù)據(jù)打包發(fā)送到網(wǎng)絡中即可,本質就是和虛擬內存映射的思路類似。
圖片
基于 sendfile + DMA gather copy 系統(tǒng)調用的零拷貝方式,整個拷貝過程會發(fā)生 2 次上下文切換、0 次 CPU 拷貝以及 2 次 DMA 拷貝,用戶程序讀寫數(shù)據(jù)的流程如下:
- 用戶進程通過 sendfile() 函數(shù)向內核發(fā)起系統(tǒng)調用,上下文從用戶態(tài)切換為內核態(tài)。
- CPU 利用 DMA 控制器將數(shù)據(jù)從主存或硬盤拷貝到內核空間的讀緩沖區(qū)。
- CPU 把讀緩沖區(qū)的文件描述符(file descriptor)和數(shù)據(jù)長度拷貝到網(wǎng)絡緩沖區(qū)。
- 基于已拷貝的文件描述符和數(shù)據(jù)長度,CPU 利用 DMA 控制器的 gather/scatter 操作直接批量地將數(shù)據(jù)從內核的讀緩沖區(qū)拷貝到網(wǎng)卡進行數(shù)據(jù)傳輸。
- 上下文從內核態(tài)切換回用戶態(tài),sendfile 系統(tǒng)調用執(zhí)行返回。
5 寫到最后
無論是傳統(tǒng) I/O 拷貝方式還是引入零拷貝的方式,2 次 DMA 拷貝是都少不了的,因為兩次 DMA 都是依賴硬件完成的。
拷貝方式 | CPU拷貝 | DMA拷貝 | 系統(tǒng)調用 | 上下文切換 |
傳統(tǒng)方式(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 這種零拷貝方式,適用于業(yè)務級消息這種小塊文件的數(shù)據(jù)持久化和傳輸;
而 Kafka 采用的是 sendfile 這種零拷貝方式,適用于系統(tǒng)日志消息這種高吞吐量的大塊文件的數(shù)據(jù)持久化和傳輸。
但是值得注意的一點是,Kafka 的索引文件使用的是 mmap + write 方式,數(shù)據(jù)文件使用的是 sendfile 方式。