什么是零拷貝?
一、摘要
相信不少的網(wǎng)友,在很多的博客文章里面,已經(jīng)見(jiàn)到過(guò)零拷貝這個(gè)詞,會(huì)不禁的發(fā)出一些疑問(wèn),什么是零拷貝?
從字面上我們很容易理解出,零拷貝包含兩個(gè)意思:
- 拷貝:就是指數(shù)據(jù)從一個(gè)存儲(chǔ)區(qū)域轉(zhuǎn)移到另一個(gè)存儲(chǔ)區(qū)域。
- 零:它表示拷貝數(shù)據(jù)的次數(shù)為 0。
合起來(lái)理解,零拷貝就是不需要將數(shù)據(jù)從一個(gè)存儲(chǔ)區(qū)域復(fù)制到另一個(gè)存儲(chǔ)區(qū)域。
果真是這樣的嗎?
最早的零拷貝定義,來(lái)源于 Linux 系統(tǒng)的 sendfile 方法邏輯!
在 Linux 2.4 內(nèi)核中,sendfile 系統(tǒng)調(diào)用方法,可以將磁盤數(shù)據(jù)通過(guò) DMA 拷貝到內(nèi)核態(tài) Buffer 后,再通過(guò) DMA 拷貝到 NIC Buffer(socket buffer),無(wú)需 CPU 拷貝,這個(gè)過(guò)程被稱之為零拷貝。
從這段描述里面我們可以得知,站在操作系統(tǒng)的角度,零拷貝沒(méi)有說(shuō)不需要拷貝數(shù)據(jù),而是省掉了 CPU 拷貝環(huán)節(jié),減少了不必要的拷貝次數(shù),提升數(shù)據(jù)拷貝效率。
要想深度的了解這里面的原理,我們還得從 IO 拷貝機(jī)制說(shuō)起!
二、IO 拷貝機(jī)制介紹
2.1、傳統(tǒng)數(shù)據(jù)拷貝流程
以客戶端從服務(wù)器下載文件為例,熟悉服務(wù)端開發(fā)的同學(xué)可能知道,服務(wù)端需要做兩件事:
- 第一步:從磁盤中讀取文件內(nèi)容
- 第二步:將文件內(nèi)容通過(guò)網(wǎng)絡(luò)傳輸給客戶端
事實(shí)上看似簡(jiǎn)單的操作,里面的流程卻沒(méi)那么簡(jiǎn)單,例如應(yīng)用程序從磁盤中讀取文件內(nèi)容的操作,大體會(huì)經(jīng)過(guò)以下幾個(gè)流程:
- 第一步:用戶應(yīng)用程序調(diào)用 read 方法,向操作系統(tǒng)發(fā)起 IO 請(qǐng)求,CPU 上下文從用戶態(tài)轉(zhuǎn)為內(nèi)核態(tài),完成第一次 CPU 切換
- 第二步:操作系統(tǒng)通過(guò) DMA 控制器從磁盤中讀數(shù)據(jù),并把數(shù)據(jù)存儲(chǔ)到內(nèi)核緩沖區(qū)
- 第三步:CPU 把內(nèi)核緩沖區(qū)的數(shù)據(jù),拷貝到用戶緩沖區(qū),同時(shí)上下文從內(nèi)核態(tài)轉(zhuǎn)為用戶態(tài),完成第二次 CPU 切換
整個(gè)讀取數(shù)據(jù)的過(guò)程,完成了 1 次 DMA 拷貝,1 次 CPU 拷貝,2 次 CPU 切換;反之寫入數(shù)據(jù)的過(guò)程,也是一樣的。
整個(gè)拷貝過(guò)程,可以用如下流程圖來(lái)描述!
圖片
從上圖,我們可以得出如下結(jié)論,4 次拷貝次數(shù)、4 次上下文切換次數(shù)。
- 數(shù)據(jù)拷貝次數(shù):2 次 DMA 拷貝,2 次 CPU 拷貝
- CPU 切換次數(shù):4 次用戶態(tài)和內(nèi)核態(tài)的切換
而實(shí)際 IO 讀寫,有時(shí)候需要進(jìn)行 IO 中斷,同時(shí)也需要 CPU 響應(yīng)中斷,拷貝次數(shù)和切換次數(shù)比預(yù)期的還要多,以至于當(dāng)客戶端進(jìn)行資源文件下載的時(shí)候,傳輸速度總是不盡人意。
那有沒(méi)有好的辦法來(lái)提升資源拷貝的速度呢?
答案是肯定的,傳統(tǒng)的數(shù)據(jù)拷貝流程還有很大的優(yōu)化空間。
下面我們一起來(lái)看看幾種其它的拷貝方式。
2.2、mmap 內(nèi)存映射拷貝流程
mmap 內(nèi)存映射的拷貝,指的是將用戶應(yīng)用程序的緩沖區(qū)和操作系統(tǒng)的內(nèi)核緩沖區(qū)進(jìn)行映射處理,數(shù)據(jù)在內(nèi)核緩沖區(qū)和用戶緩沖區(qū)之間的 CPU 拷貝將其省略,進(jìn)而加快資源拷貝效率。
整個(gè)拷貝過(guò)程,可以用如下流程圖來(lái)描述!
圖片
mmap 內(nèi)存映射拷貝流程,從上圖可以得出如下結(jié)論:
- 數(shù)據(jù)拷貝次數(shù):2 次 DMA 拷貝,1 次 CPU 拷貝
- CPU 切換次數(shù):4 次用戶態(tài)和內(nèi)核態(tài)的切換
整個(gè)過(guò)程省掉了數(shù)據(jù)在內(nèi)核緩沖區(qū)和用戶緩沖區(qū)之間的 CPU 拷貝環(huán)節(jié),在實(shí)際的應(yīng)用中,對(duì)資源的拷貝能提升不少。
2.3、Linux 系統(tǒng) sendfile 拷貝流程
在 Linux 2.1 內(nèi)核版本中,引入了一個(gè)系統(tǒng)調(diào)用方法:sendfile。
當(dāng)調(diào)用 sendfile() 時(shí),DMA 將磁盤數(shù)據(jù)復(fù)制到內(nèi)核緩沖區(qū) kernel buffer;然后將內(nèi)核中的 kernel buffer 直接拷貝到 socket buffer;最后利用 DMA 將 socket buffer 通過(guò)網(wǎng)卡傳輸給客戶端。
整個(gè)拷貝過(guò)程,可以用如下流程圖來(lái)描述!
圖片
Linux 系統(tǒng) sendfile 拷貝流程,從上圖可以得出如下結(jié)論:
- 數(shù)據(jù)拷貝次數(shù):2 次 DMA 拷貝,1 次 CPU 拷貝
- CPU 切換次數(shù):2 次用戶態(tài)和內(nèi)核態(tài)的切換
相比 mmap 內(nèi)存映射方式,Linux 2.1 內(nèi)核版本中 sendfile 拷貝流程省掉了 2 次用戶態(tài)和內(nèi)核態(tài)的切換,同時(shí)內(nèi)核緩沖區(qū)和用戶緩沖區(qū)也無(wú)需建立內(nèi)存映射,對(duì)資源的拷貝能提升不少。
2.4、sendfile With DMA scatter/gather 拷貝流程
在 Linux 2.4 內(nèi)核版本中,對(duì) sendfile 系統(tǒng)方法做了優(yōu)化升級(jí),引入 SG-DMA 技術(shù),需要 DMA 控制器支持。
其實(shí)就是對(duì) DMA 拷貝加入了 scatter/gather 操作,它可以直接從內(nèi)核空間緩沖區(qū)中將數(shù)據(jù)讀取到網(wǎng)卡。使用這個(gè)特點(diǎn)來(lái)實(shí)現(xiàn)數(shù)據(jù)拷貝,可以多省去一次 CPU 拷貝。
整個(gè)拷貝過(guò)程,可以用如下流程圖來(lái)描述!
圖片
Linux 系統(tǒng) sendfile With DMA scatter/gather 拷貝流程,從上圖可以得出如下結(jié)論:
- 數(shù)據(jù)拷貝次數(shù):2 次 DMA 拷貝,0 次 CPU 拷貝
- CPU 切換次數(shù):2 次用戶態(tài)和內(nèi)核態(tài)的切換
可以發(fā)現(xiàn),sendfile With DMA scatter/gather 實(shí)現(xiàn)的拷貝,其中 2 次數(shù)據(jù)拷貝都是 DMA 拷貝,全程都沒(méi)有通過(guò) CPU 來(lái)拷貝數(shù)據(jù),所有的數(shù)據(jù)都是通過(guò) DMA 來(lái)進(jìn)行傳輸?shù)模@就是操作系統(tǒng)真正意義上的零拷貝(Zero-copy) 技術(shù),相比其他拷貝方式,傳輸效率最佳。
2.5、Linux 系統(tǒng) splice 零拷貝流程
在 Linux 2.6.17 內(nèi)核版本中,引入了 splice 系統(tǒng)調(diào)用方法,和 sendfile 方法不同的是,splice 不需要硬件支持。
它將數(shù)據(jù)從磁盤讀取到 OS 內(nèi)核緩沖區(qū)后,內(nèi)核緩沖區(qū)和 socket 緩沖區(qū)之間建立管道來(lái)傳輸數(shù)據(jù),避免了兩者之間的 CPU 拷貝操作。
整個(gè)拷貝過(guò)程,可以用如下流程圖來(lái)描述!
圖片
Linux 系統(tǒng) splice 拷貝流程,從上圖可以得出如下結(jié)論:
- 數(shù)據(jù)拷貝次數(shù):2 次 DMA 拷貝,0 次 CPU 拷貝
- CPU 切換次數(shù):2 次用戶態(tài)和內(nèi)核態(tài)的切換
Linux 系統(tǒng) splice 方法邏輯拷貝,也是操作系統(tǒng)真正意義上的零拷貝。
三、IO 拷貝機(jī)制對(duì)比
從上面的 IO 拷貝機(jī)制可以看出,無(wú)論是傳統(tǒng) IO 方式,還是引入零拷貝之后,2次 DMA copy 是都少不了的,唯一的區(qū)別就是省掉 CPU 參與環(huán)節(jié)的方式不同。
以 Linux 系統(tǒng)為例,拷貝機(jī)制對(duì)比結(jié)果如下!
圖片
需要注意的地方是,零拷貝所有的方式,都需要操作系統(tǒng)支持,具體采用哪種方式,由操作系統(tǒng)來(lái)決定。
四、相關(guān)概念說(shuō)明
上文中我們提到了幾個(gè)很少見(jiàn)的名詞,比如 DMA,用戶空間,內(nèi)核空間等。它們到底是什么意思呢?
4.1、DMA 介紹
DMA,英文全稱是 Direct Memory Access,即直接內(nèi)存訪問(wèn)。DMA 本質(zhì)上是一塊主板上獨(dú)立的芯片,允許外設(shè)設(shè)備和內(nèi)存存儲(chǔ)器之間直接進(jìn)行 IO 數(shù)據(jù)傳輸,其過(guò)程不需要 CPU 的參與。
4.2、內(nèi)核空間和用戶空間介紹
操作系統(tǒng)的核心是內(nèi)核,與普通的應(yīng)用程序不同,它可以訪問(wèn)受保護(hù)的內(nèi)存空間,也有訪問(wèn)底層硬件設(shè)備的權(quán)限。
為了避免用戶進(jìn)程直接操作內(nèi)核,保證內(nèi)核安全,操作系統(tǒng)將虛擬內(nèi)存劃分為兩部分,一部分是內(nèi)核空間(Kernel-space),一部分是用戶空間(User-space)。在 Linux 系統(tǒng)中,內(nèi)核模塊運(yùn)行在內(nèi)核空間,對(duì)應(yīng)的進(jìn)程處于內(nèi)核態(tài);而用戶程序運(yùn)行在用戶空間,對(duì)應(yīng)的進(jìn)程處于用戶態(tài)。
內(nèi)核空間總是駐留在內(nèi)存中,它是為操作系統(tǒng)的內(nèi)核保留的。應(yīng)用程序是不允許直接在該區(qū)域進(jìn)行讀寫或直接調(diào)用內(nèi)核代碼定義的函數(shù)。
當(dāng)啟動(dòng)某個(gè)應(yīng)用程序時(shí),操作系統(tǒng)會(huì)給應(yīng)用程序分配一個(gè)單獨(dú)的用戶空間,其實(shí)就是一個(gè)用戶獨(dú)享的虛擬內(nèi)存,每個(gè)普通的用戶進(jìn)程之間的用戶空間是完全隔離的、不共享的,當(dāng)用戶進(jìn)程結(jié)束的時(shí)候,用戶空間的虛擬內(nèi)存也會(huì)隨之釋放。
同時(shí)處于用戶態(tài)的進(jìn)程不能訪問(wèn)內(nèi)核空間中的數(shù)據(jù),也不能直接調(diào)用內(nèi)核函數(shù)的,如果要調(diào)用系統(tǒng)資源,就要將進(jìn)程切換到內(nèi)核態(tài),由內(nèi)核程序來(lái)進(jìn)行操作。
五、Java 零拷貝實(shí)現(xiàn)介紹
Linux 提供的零拷貝技術(shù),Java 并不是全部支持,目前只支持以下 2 種。
- mmap(內(nèi)存映射)
- sendfile
5.1、Java NIO 對(duì) mmap 的支持
Java NIO 有一個(gè)MappedByteBuffer的類,可以用來(lái)實(shí)現(xiàn)內(nèi)存映射。它的底層是調(diào)用了 Linux 內(nèi)核的mmap的 API。
實(shí)例代碼如下:
public static void main(String[] args) {
try {
FileChannel readChannel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.READ);
// 建立內(nèi)存文件映射
MappedByteBuffer data = readChannel.map(FileChannel.MapMode.READ_ONLY, 0, 1024 * 1024 * 40);
FileChannel writeChannel = FileChannel.open(Paths.get("b.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
// 拷貝數(shù)據(jù)
writeChannel.write(data);
// 關(guān)閉通道
readChannel.close();
writeChannel.close();
}catch (Exception e){
System.out.println(e.getMessage());
}
}
其中MappedByteBuffer的作用,就是將內(nèi)核緩沖區(qū)的內(nèi)存和用戶緩沖區(qū)的內(nèi)存做了一個(gè)地址映射,讀取小文件,效率并不高;但是讀取大文件,效率很高。
5.2、Java NIO 對(duì) sendfile 的支持
Java NIO 中的FileChannel.transferTo方法,底層調(diào)用的就是 Linux 內(nèi)核的sendfile系統(tǒng)調(diào)用方法。Kafka 這個(gè)開源項(xiàng)目就用到它,平時(shí)面試的時(shí)候,如果面試官問(wèn)起你為什么這么快,就可以提到sendfile零拷貝系統(tǒng)調(diào)用方法來(lái)回答。
實(shí)例代碼如下:
public static void main(String[] args) {
try {
// 原始文件
FileChannel srcChannel = FileChannel.open(Paths.get("a.txt"), StandardOpenOption.READ);
// 目標(biāo)文件
FileChannel destChannel = FileChannel.open(Paths.get("b.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
// 拷貝數(shù)據(jù)
srcChannel.transferTo(0, srcChannel.size(), destChannel);
// 關(guān)閉通道
srcChannel.close();
destChannel.close();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
Java NIO 提供的FileChannel.transferTo并不保證一定能使用零拷貝。實(shí)際上是否能使用零拷貝與操作系統(tǒng)相關(guān),如果操作系統(tǒng)提供sendfile這樣的零拷貝系統(tǒng)調(diào)用方法,那么會(huì)充分利用sendfile零拷貝的優(yōu)勢(shì),否則并不能實(shí)現(xiàn)零拷貝。
六、小結(jié)
本位主要圍繞零拷貝的邏輯,結(jié)合網(wǎng)友的知識(shí)分享,進(jìn)行一次知識(shí)整理和總結(jié)。
從上面的內(nèi)容總結(jié)可以看出,所謂的零拷貝,其目的并不是說(shuō)不需要拷貝數(shù)據(jù),而是通過(guò)一些手段省略 CPU 拷貝環(huán)節(jié),減少了不必要的拷貝次數(shù),提升數(shù)據(jù)拷貝效率。
以 Linux 操作系統(tǒng)為例,真正意義上大家比較認(rèn)可的零拷貝主要有 sendfile、splice 等方法,它們完全通過(guò) DMA 控制器來(lái)實(shí)現(xiàn)數(shù)據(jù)的拷貝,無(wú)需 CPU 來(lái)參與數(shù)據(jù)拷貝的過(guò)程,這個(gè)過(guò)程被稱為零拷貝。
但是比較糟心的是,一些機(jī)構(gòu)、團(tuán)隊(duì),在產(chǎn)品推廣中過(guò)度包裝零拷貝這個(gè)概念,名曰“采用零拷貝,性能有多高等等...”,這樣的宣傳似乎會(huì)讓很多人覺(jué)得很厲害。
作為一線技術(shù)者,應(yīng)該多多深入了解,識(shí)透其真相,弄清楚是偏向于優(yōu)化數(shù)據(jù)操作,還是真正切合場(chǎng)景、靈活運(yùn)用了操作系統(tǒng)意義上的零拷貝,大家可以多深入分析。