什么是零拷貝
維基上是這么描述零拷貝的:零拷貝描述的是CPU不執(zhí)行拷貝數(shù)據(jù)從一個(gè)存儲區(qū)域到另一個(gè)存儲區(qū)域的任務(wù),這通常用于通過網(wǎng)絡(luò)傳輸一個(gè)文件時(shí)以減少CPU周期和內(nèi)存帶寬。
零拷貝給我們帶來的好處:
- 減少甚至完全避免不必要的CPU拷貝,從而讓CPU解脫出來去執(zhí)行其他的任務(wù)
- 減少內(nèi)存帶寬的占用
- 通常零拷貝技術(shù)還能夠減少用戶空間和操作系統(tǒng)內(nèi)核空間之間的上下文切換
Linux系統(tǒng)的“用戶空間”和“內(nèi)核空間”
從Linux系統(tǒng)上看,除了引導(dǎo)系統(tǒng)的BIN區(qū),整個(gè)內(nèi)存空間主要被分成兩個(gè)部分:內(nèi)核空間(Kernel space)、用戶空間(User space)?!坝脩艨臻g”和“內(nèi)核空間”的空間、操作權(quán)限以及作用都是不一樣的。內(nèi)核空間是Linux自身使用的內(nèi)存空間,主要提供給程序調(diào)度、內(nèi)存分配、連接硬件資源等程序邏輯使用;用戶空間則是提供給各個(gè)進(jìn)程的主要空間。用戶空間不具有訪問內(nèi)核空間資源的權(quán)限,因此如果應(yīng)用程序需要使用到內(nèi)核空間的資源,則需要通過系統(tǒng)調(diào)用來完成:從用戶空間切換到內(nèi)核空間,然后在完成相關(guān)操作后再從內(nèi)核空間切換回用戶空間。
Linux 中零拷貝技術(shù)的實(shí)現(xiàn)方向
① 直接 I/O:對于這種數(shù)據(jù)傳輸方式來說,應(yīng)用程序可以直接訪問硬件存儲,操作系統(tǒng)內(nèi)核只是輔助數(shù)據(jù)傳輸。這種方式依舊存在用戶空間和內(nèi)核空間的上下文切換,但是硬件上的數(shù)據(jù)不會拷貝一份到內(nèi)核空間,而是直接拷貝至了用戶空間,因此直接I/O不存在內(nèi)核空間緩沖區(qū)和用戶空間緩沖區(qū)之間的數(shù)據(jù)拷貝。
② 在數(shù)據(jù)傳輸過程中,避免數(shù)據(jù)在用戶空間緩沖區(qū)和系統(tǒng)內(nèi)核空間緩沖區(qū)之間的CPU拷貝,以及數(shù)據(jù)在系統(tǒng)內(nèi)核空間內(nèi)的CPU拷貝。本文主要討論的就是該方式下的零拷貝機(jī)制。
③ copy-on-write(寫時(shí)復(fù)制技術(shù)):在某些情況下,Linux操作系統(tǒng)的內(nèi)核空間緩沖區(qū)可能被多個(gè)應(yīng)用程序所共享,操作系統(tǒng)有可能會將用戶空間緩沖區(qū)地址映射到內(nèi)核空間緩存區(qū)中。當(dāng)應(yīng)用程序需要對共享的數(shù)據(jù)進(jìn)行修改的時(shí)候,才需要真正地拷貝數(shù)據(jù)到應(yīng)用程序的用戶空間緩沖區(qū)中,并且對自己用戶空間的緩沖區(qū)的數(shù)據(jù)進(jìn)行修改不會影響到其他共享數(shù)據(jù)的應(yīng)用程序。所以,如果應(yīng)用程序不需要對數(shù)據(jù)進(jìn)行任何修改的話,就不會存在數(shù)據(jù)從系統(tǒng)內(nèi)核空間緩沖區(qū)拷貝到用戶空間緩沖區(qū)的操作。
注意,對于各種零拷貝機(jī)制是否能夠?qū)崿F(xiàn)都是依賴于操作系統(tǒng)底層是否提供相應(yīng)的支持。
零拷貝機(jī)制的原理
下面我們通過一個(gè)Java非常常見的應(yīng)用場景:將系統(tǒng)中的文件發(fā)送到遠(yuǎn)端(該流程涉及:磁盤上文件 ——> 內(nèi)存(字節(jié)數(shù)組) ——> 傳輸給用戶/網(wǎng)絡(luò))來詳細(xì)展開傳統(tǒng)I/O操作和通過零拷貝來實(shí)現(xiàn)的I/O操作。
傳統(tǒng)I/O

① 發(fā)出read系統(tǒng)調(diào)用:導(dǎo)致用戶空間到內(nèi)核空間的上下文切換(第一次上下文切換)。通過DMA引擎將文件中的數(shù)據(jù)從磁盤上讀取到內(nèi)核空間緩沖區(qū)(第一次拷貝: hard drive ——> kernel buffer)。
② 將內(nèi)核空間緩沖區(qū)的數(shù)據(jù)拷貝到用戶空間緩沖區(qū)(第二次拷貝: kernel buffer ——> user buffer),然后read系統(tǒng)調(diào)用返回。而系統(tǒng)調(diào)用的返回又會導(dǎo)致一次內(nèi)核空間到用戶空間的上下文切換(第二次上下文切換)。
③ 發(fā)出write系統(tǒng)調(diào)用:導(dǎo)致用戶空間到內(nèi)核空間的上下文切換(第三次上下文切換)。將用戶空間緩沖區(qū)中的數(shù)據(jù)拷貝到內(nèi)核空間中與socket相關(guān)聯(lián)的緩沖區(qū)中(即,第②步中從內(nèi)核空間緩沖區(qū)拷貝而來的數(shù)據(jù)原封不動的再次拷貝到內(nèi)核空間的socket緩沖區(qū)中。)(第三次拷貝: user buffer ——> socket buffer)。
④ write系統(tǒng)調(diào)用返回,導(dǎo)致內(nèi)核空間到用戶空間的再次上下文切換(第四次上下文切換)。通過DMA引擎將內(nèi)核緩沖區(qū)中的數(shù)據(jù)傳遞到協(xié)議引擎(第四次拷貝: socket buffer ——> protocol engine),這次拷貝是一個(gè)獨(dú)立且異步的過程。
Q:你可能會問獨(dú)立和異步這是什么意思?難道是調(diào)用會在數(shù)據(jù)被傳輸前返回?
A:事實(shí)上調(diào)用的返回并不保證數(shù)據(jù)被傳輸;它甚至不保證傳輸?shù)拈_始。它只是意味著將我們要發(fā)送的數(shù)據(jù)放入到了一個(gè)待發(fā)送的隊(duì)列中,在我們之前可能有許多數(shù)據(jù)包在排隊(duì)。除非驅(qū)動器或硬件實(shí)現(xiàn)優(yōu)先級環(huán)或隊(duì)列,否則數(shù)據(jù)是以先進(jìn)先出的方式傳輸?shù)摹?/p>
總的來說,傳統(tǒng)的I/O操作進(jìn)行了4次用戶空間與內(nèi)核空間的上下文切換,以及4次數(shù)據(jù)拷貝。其中4次數(shù)據(jù)拷貝中包括了2次DMA拷貝和2次CPU拷貝。
Q: 傳統(tǒng)I/O模式為什么將數(shù)據(jù)從磁盤讀取到內(nèi)核空間緩沖區(qū),然后再將數(shù)據(jù)從內(nèi)核空間緩沖區(qū)拷貝到用戶空間緩沖區(qū)了?為什么不直接將數(shù)據(jù)從磁盤讀取到用戶空間緩沖區(qū)就好?
A: 傳統(tǒng)I/O模式之所以將數(shù)據(jù)從磁盤讀取到內(nèi)核空間緩沖區(qū)而不是直接讀取到用戶空間緩沖區(qū),是為了減少磁盤I/O操作以此來提高性能。因?yàn)镺S會根據(jù)局部性原理在一次read()系統(tǒng)調(diào)用的時(shí)候預(yù)讀取更多的文件數(shù)據(jù)到內(nèi)核空間緩沖區(qū)中,這樣當(dāng)下一次read()系統(tǒng)調(diào)用的時(shí)候發(fā)現(xiàn)要讀取的數(shù)據(jù)已經(jīng)存在于內(nèi)核空間緩沖區(qū)中的時(shí)候只要直接拷貝數(shù)據(jù)到用戶空間緩沖區(qū)中即可,無需再進(jìn)行一次低效的磁盤I/O操作(注意:磁盤I/O操作的速度比直接訪問內(nèi)存慢了好幾個(gè)數(shù)量級)。
Q: 既然系統(tǒng)內(nèi)核緩沖區(qū)能夠減少磁盤I/O操作,那么我們經(jīng)常使用的BufferedInputStream緩沖區(qū)又是用來干啥的?
A: BufferedInputStream的作用是會根據(jù)情況自動為我們預(yù)取更多的數(shù)據(jù)到它自己維護(hù)的一個(gè)內(nèi)部字節(jié)數(shù)據(jù)緩沖區(qū)中,這樣做能夠減少系統(tǒng)調(diào)用的次數(shù)以此來提供性能。
總的來說內(nèi)核空間緩沖區(qū)的一大用處是為了減少磁盤I/O操作,因?yàn)樗鼤拇疟P中預(yù)讀更多的數(shù)據(jù)到緩沖區(qū)中。而BufferedInputStream的用處是減少“系統(tǒng)調(diào)用”。
DMA
DMA(Direct Memory Access) ———— 直接內(nèi)存訪問 :DMA是允許外設(shè)組件將I/O數(shù)據(jù)直接傳送到主存儲器中并且傳輸不需要CPU的參與,以此將CPU解放出來去完成其他的事情。
而用戶空間與內(nèi)核空間之間的數(shù)據(jù)傳輸并沒有類似DMA這種可以不需要CPU參與的傳輸工具,因此用戶空間與內(nèi)核空間之間的數(shù)據(jù)傳輸是需要CPU全程參與的。所有也就有了通過零拷貝技術(shù)來減少和避免不必要的CPU數(shù)據(jù)拷貝過程。
通過sendfile實(shí)現(xiàn)的零拷貝I/O

① 發(fā)出sendfile系統(tǒng)調(diào)用,導(dǎo)致用戶空間到內(nèi)核空間的上下文切換(第一次上下文切換)。通過DMA引擎將磁盤文件中的內(nèi)容拷貝到內(nèi)核空間緩沖區(qū)中(第一次拷貝: hard drive ——> kernel buffer)。然后再將數(shù)據(jù)從內(nèi)核空間緩沖區(qū)拷貝到內(nèi)核中與socket相關(guān)的緩沖區(qū)中(第二次拷貝: kernel buffer ——> socket buffer)。
② sendfile系統(tǒng)調(diào)用返回,導(dǎo)致內(nèi)核空間到用戶空間的上下文切換(第二次上下文切換)。通過DMA引擎將內(nèi)核空間socket緩沖區(qū)中的數(shù)據(jù)傳遞到協(xié)議引擎(第三次拷貝: socket buffer ——> protocol engine)
總的來說,通過sendfile實(shí)現(xiàn)的零拷貝I/O只使用了2次用戶空間與內(nèi)核空間的上下文切換,以及3次數(shù)據(jù)的拷貝。其中3次數(shù)據(jù)拷貝中包括了2次DMA拷貝和1次CPU拷貝。
Q:但通過是這里還是存在著一次CPU拷貝操作,即,kernel buffer ——> socket buffer。是否有辦法將該拷貝操作也取消掉了?
A:有的。但這需要底層操作系統(tǒng)的支持。從Linux 2.4版本開始,操作系統(tǒng)底層提供了scatter/gather這種DMA的方式來從內(nèi)核空間緩沖區(qū)中將數(shù)據(jù)直接讀取到協(xié)議引擎中,而無需將內(nèi)核空間緩沖區(qū)中的數(shù)據(jù)再拷貝一份到內(nèi)核空間socket相關(guān)聯(lián)的緩沖區(qū)中。
帶有DMA收集拷貝功能的sendfile實(shí)現(xiàn)的I/O
從Linux 2.4版本開始,操作系統(tǒng)底層提供了帶有scatter/gather的DMA來從內(nèi)核空間緩沖區(qū)中將數(shù)據(jù)讀取到協(xié)議引擎中。這樣一來待傳輸?shù)臄?shù)據(jù)可以分散在存儲的不同位置上,而不需要在連續(xù)存儲中存放。那么從文件中讀出的數(shù)據(jù)就根本不需要被拷貝到socket緩沖區(qū)中去,只是需要將緩沖區(qū)描述符添加到socket緩沖區(qū)中去,DMA收集操作會根據(jù)緩沖區(qū)描述符中的信息將內(nèi)核空間中的數(shù)據(jù)直接拷貝到協(xié)議引擎中。

① 發(fā)出sendfile系統(tǒng)調(diào)用,導(dǎo)致用戶空間到內(nèi)核空間的上下文切換(第一次上下文切換)。通過DMA引擎將磁盤文件中的內(nèi)容拷貝到內(nèi)核空間緩沖區(qū)中(第一次拷貝: hard drive ——> kernel buffer)。
② 沒有數(shù)據(jù)拷貝到socket緩沖區(qū)。取而代之的是只有相應(yīng)的描述符信息會被拷貝到相應(yīng)的socket緩沖區(qū)當(dāng)中。該描述符包含了兩方面的信息:a)kernel buffer的內(nèi)存地址;b)kernel buffer的偏移量。
③ sendfile系統(tǒng)調(diào)用返回,導(dǎo)致內(nèi)核空間到用戶空間的上下文切換(第二次上下文切換)。DMA gather copy根據(jù)socket緩沖區(qū)中描述符提供的位置和偏移量信息直接將內(nèi)核空間緩沖區(qū)中的數(shù)據(jù)拷貝到協(xié)議引擎上(第二次拷貝: kernel buffer ——> protocol engine),這樣就避免了最后一次CPU數(shù)據(jù)拷貝。
總的來說,帶有DMA收集拷貝功能的sendfile實(shí)現(xiàn)的I/O只使用了2次用戶空間與內(nèi)核空間的上下文切換,以及2次數(shù)據(jù)的拷貝,而且這2次的數(shù)據(jù)拷貝都是非CPU拷貝。這樣一來我們就實(shí)現(xiàn)了最理想的零拷貝I/O傳輸了,不需要任何一次的CPU拷貝,以及最少的上下文切換。
關(guān)于sendfile:
#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
在linux2.6.33版本之前 sendfile指支持文件到套接字之間傳輸數(shù)據(jù),即in_fd相當(dāng)于一個(gè)支持mmap的文件,out_fd必須是一個(gè)socket。但從linux2.6.33版本開始,out_fd可以是任意類型文件描述符。所以從linux2.6.33版本開始sendfile可以支持“文件到文件”和“文件到套接字”之間的數(shù)據(jù)傳輸。
"傳統(tǒng)I/O” VS “sendfile零拷貝I/O”
- 傳統(tǒng)I/O通過兩條系統(tǒng)指令read、write來完成數(shù)據(jù)的讀取和傳輸操作,以至于產(chǎn)生了4次用戶空間與內(nèi)核空間的上下文切換的開銷;而sendfile只使用了一條指令就完成了數(shù)據(jù)的讀寫操作,所以只產(chǎn)生了2次用戶空間與內(nèi)核空間的上下文切換。
- 傳統(tǒng)I/O產(chǎn)生了2次無用的CPU拷貝,即內(nèi)核空間緩存中數(shù)據(jù)與用戶空間緩沖區(qū)間數(shù)據(jù)的拷貝;而sendfile最多只產(chǎn)出了一次CPU拷貝,即內(nèi)核空間內(nèi)之間的數(shù)據(jù)拷貝,甚至在底層操作體系支持的情況下,sendfile可以實(shí)現(xiàn)零CPU拷貝的I/O。
- 因傳統(tǒng)I/O用戶空間緩沖區(qū)中存有數(shù)據(jù),因此應(yīng)用程序能夠?qū)Υ藬?shù)據(jù)進(jìn)行修改等操作;而sendfile零拷貝消除了所有內(nèi)核空間緩沖區(qū)與用戶空間緩沖區(qū)之間的數(shù)據(jù)拷貝過程,因此sendfile零拷貝I/O的實(shí)現(xiàn)是完成在內(nèi)核空間中完成的,這對于應(yīng)用程序來說就無法對數(shù)據(jù)進(jìn)行操作了。
Q:對于上面的第三點(diǎn),如果我們需要對數(shù)據(jù)進(jìn)行操作該怎么辦了?
A:Linux提供了mmap零拷貝來實(shí)現(xiàn)我們的需求。
通過mmap實(shí)現(xiàn)的零拷貝I/O
mmap(內(nèi)存映射)是一個(gè)比sendfile昂貴但優(yōu)于傳統(tǒng)I/O的方法。

① 發(fā)出mmap系統(tǒng)調(diào)用,導(dǎo)致用戶空間到內(nèi)核空間的上下文切換(第一次上下文切換)。通過DMA引擎將磁盤文件中的內(nèi)容拷貝到內(nèi)核空間緩沖區(qū)中(第一次拷貝: hard drive ——> kernel buffer)。
② mmap系統(tǒng)調(diào)用返回,導(dǎo)致內(nèi)核空間到用戶空間的上下文切換(第二次上下文切換)。接著用戶空間和內(nèi)核空間共享這個(gè)緩沖區(qū),而不需要將數(shù)據(jù)從內(nèi)核空間拷貝到用戶空間。因?yàn)橛脩艨臻g和內(nèi)核空間共享了這個(gè)緩沖區(qū)數(shù)據(jù),所以用戶空間就可以像在操作自己緩沖區(qū)中數(shù)據(jù)一般操作這個(gè)由內(nèi)核空間共享的緩沖區(qū)數(shù)據(jù)。
③ 發(fā)出write系統(tǒng)調(diào)用,導(dǎo)致用戶空間到內(nèi)核空間的上下文切換(第三次上下文切換)。將數(shù)據(jù)從內(nèi)核空間緩沖區(qū)拷貝到內(nèi)核空間socket相關(guān)聯(lián)的緩沖區(qū)(第二次拷貝: kernel buffer ——> socket buffer)。
④ write系統(tǒng)調(diào)用返回,導(dǎo)致內(nèi)核空間到用戶空間的上下文切換(第四次上下文切換)。通過DMA引擎將內(nèi)核空間socket緩沖區(qū)中的數(shù)據(jù)傳遞到協(xié)議引擎(第三次拷貝: socket buffer ——> protocol engine)
總的來說,通過mmap實(shí)現(xiàn)的零拷貝I/O進(jìn)行了4次用戶空間與內(nèi)核空間的上下文切換,以及3次數(shù)據(jù)拷貝。其中3次數(shù)據(jù)拷貝中包括了2次DMA拷貝和1次CPU拷貝。
FileChannel與零拷貝
FileChannel中大量使用了我們上面所提及的零拷貝技術(shù)。
FileChannel的map方法會返回一個(gè)MappedByteBuffer。MappedByteBuffer是一個(gè)直接字節(jié)緩沖器,該緩沖器的內(nèi)存是一個(gè)文件的內(nèi)存映射區(qū)域。map方法底層是通過mmap實(shí)現(xiàn)的,因此將文件內(nèi)存從磁盤讀取到內(nèi)核緩沖區(qū)后,用戶空間和內(nèi)核空間共享該緩沖區(qū)。
MappedByteBuffer內(nèi)存映射文件是一種允許Java程序直接從內(nèi)存訪問的一種特殊的文件。我們可以將整個(gè)文件或者整個(gè)文件的一部分映射到內(nèi)存當(dāng)中,那么接下來是由操作系統(tǒng)來進(jìn)行相關(guān)的頁面請求并將內(nèi)存的修改寫入到文件當(dāng)中。我們的應(yīng)用程序只需要處理內(nèi)存的數(shù)據(jù),這樣可以實(shí)現(xiàn)非常迅速的I/O操作。
FileChannel map的三種模式
- 只讀模式
/**
* Mode for a read-only mapping.
*/
public static final MapMode READ_ONLY = new MapMode("READ_ONLY");
只讀模式來說,如果程序試圖進(jìn)行寫操作,則會拋出ReadOnlyBufferException異常
- 讀寫模式
/**
* Mode for a read/write mapping.
*/
public static final MapMode READ_WRITE = new MapMode("READ_WRITE");
讀寫模式表明,對結(jié)果對緩沖區(qū)所做的修改將最終廣播到文件。但這個(gè)修改可能會也可能不會被其他映射了相同文件程序可見。
- 專用模式
/**
* Mode for a private (copy-on-write) mapping.
*/
public static final MapMode PRIVATE = new MapMode("PRIVATE");
私有模式來說,對結(jié)果緩沖區(qū)的修改將不會被廣播到文件并且也不會對其他映射了相同文件的程序可見。取而代之的是,它將導(dǎo)致被修改部分緩沖區(qū)獨(dú)自拷貝一份到用戶空間。這便是OS的“copy on write”原則。
FileChannel的transferTo、transferFrom
如果操作系統(tǒng)底層支持的話transferTo、transferFrom也會使用相關(guān)的零拷貝技術(shù)來實(shí)現(xiàn)數(shù)據(jù)的傳輸。所以,這里是否使用零拷貝必須依賴于底層的系統(tǒng)實(shí)現(xiàn)。
后記
本文是通過視頻學(xué)習(xí)以及大量資料查詢后對零拷貝機(jī)制進(jìn)行的一個(gè)非常膚淺的知識梳理,至少個(gè)人是這么覺得。通過這次的學(xué)習(xí),對Linux操作系統(tǒng)又多了一丟丟的了解,也希望在之后的學(xué)習(xí)中能對Linux系統(tǒng)有更近一步的深入的理解。非常歡迎大家對文中的不足和錯(cuò)誤進(jìn)行指點(diǎn)~
參考:
It's all about buffers: zero-copy, mmap and Java NIO
Zero Copy I: User-Mode Perspective
Linux Programmer's Manual SENDFILE(2)
Linux 中的零拷貝技術(shù),第 1 部分
Linux 中的零拷貝技術(shù),第 2 部分
圣思園《精通并發(fā)與Netty》