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