看一遍就能理解的:零複製技術
1.什麼是零複製
零複製字面上的意思包括兩個,“零”和“複製”:
“複製”:就是指資料從一個儲存區域轉移到另一個儲存區域。
“零” :表示次數為0,它表示複製資料的次數為0。
2022年嵌入式開發想進網際網路大廠,你技術過硬嗎?
從事十年嵌入式轉核心開發(23K到45K),給兄弟們的一些建議
騰訊首發Linux核心原始碼《嵌入式開發進階筆記》差距差的不止一點點哦
合起來,那
零複製
就是不需要將資料從一個儲存區域複製到另一個儲存區域咯。
零複製
是指計算機執行IO操作時,CPU不需要將資料從一個儲存區域複製到另一個儲存區域,從而可以減少上下文切換以及CPU的複製時間。它是一種
I/O
操作最佳化技術。
2. 傳統 IO 的執行流程
做服務端開發的小夥伴,檔案下載功能應該實現過不少了吧。如果你實現的是一個
web程式
,前端請求過來,服務端的任務就是:將服務端主機磁碟中的檔案從已連線的socket發出去。關鍵實現程式碼如下:
while((n = read(diskfd, buf, BUF_SIZE)) > 0)
write(sockfd, buf , n);
傳統的IO流程,包括read和write的過程。
read
:把資料從磁碟讀取到核心緩衝區,再複製到使用者緩衝區
write
:先把資料寫入到socket緩衝區,最後寫入網絡卡裝置。
流程圖如下:
使用者應用程序呼叫read函式,向作業系統發起IO呼叫,
上下文從使用者態轉為核心態(切換1)
DMA控制器把資料從磁碟中,讀取到核心緩衝區。
CPU把核心緩衝區資料,複製到使用者應用緩衝區,
上下文從核心態轉為使用者態(切換2)
,read函式返回
使用者應用程序透過write函式,發起IO呼叫,
上下文從使用者態轉為核心態(切換3)
CPU將使用者緩衝區中的資料,複製到socket緩衝區
DMA控制器把資料從socket緩衝區,複製到網絡卡裝置,
上下文從核心態切換回使用者態(切換4)
,write函式返回
從流程圖可以看出,
傳統IO的讀寫流程
,包括了4次上下文切換(4次使用者態和核心態的切換),4次資料複製(
兩次CPU複製以及兩次的DMA複製
),什麼是DMA複製呢?我們一起來回顧下,零複製涉及的
作業系統知識點
哈。
【文章福利
】小編推薦自己的Linux核心技術交流群:【
865977150
】整理了一些個人覺得比較好的學習書籍、影片資料共享在群檔案裡面,有需要的可以自行新增哦!!!前100名進群領取,額外贈送一份價值
699的核心資料包
(含影片教程、電子書、實戰專案及程式碼)
學習直通車:
Linux核心原始碼/記憶體調優/檔案系統/程序管理/裝置驅動/網路協議棧-學習影片教程-騰訊課堂ke。qq。com/course/4032547?flowToken=1040236ke。qq。com/course/4032547?flowToken=1040236
核心資料直通車:
Linux核心原始碼技術學習路線+影片教程程式碼資料docs。qq。com/doc/DTkZRWXRFcWx1bWVxdocs。qq。com/doc/DTkZRWXRFcWx1bWVx
3. 零複製相關的知識點回顧
3.1 核心空間和使用者空間
我們電腦上跑著的應用程式,其實是需要經過
作業系統
,才能做一些特殊操作,如磁碟檔案讀寫、記憶體的讀寫等等。因為這些都是比較危險的操作,
不可以由應用程式亂來
,只能交給底層作業系統來。
因此,作業系統為每個程序都分配了記憶體空間,一部分是使用者空間,一部分是核心空間。
核心空間是作業系統核心訪問的區域,是受保護的記憶體空間,而使用者空間是使用者應用程式訪問的記憶體區域。
以32位作業系統為例,它會為每一個程序都分配了
4G
(2的32次方)的記憶體空間。
核心空間:主要提供程序排程、記憶體分配、連線硬體資源等功能
使用者空間:提供給各個程式程序的空間,它不具有訪問核心空間資源的許可權,如果應用程式需要使用到核心空間的資源,則需要透過系統呼叫來完成。程序從使用者空間切換到核心空間,完成相關操作後,再從核心空間切換回使用者空間。
3.2 什麼是使用者態、核心態
如果程序運行於核心空間,被稱為程序的核心態
如果程序運行於使用者空間,被稱為程序的使用者態。
3.3 什麼是上下文切換
什麼是CPU上下文?
CPU 暫存器,是CPU內建的容量小、但速度極快的記憶體。而程式計數器,則是用來儲存 CPU 正在執行的指令位置、或者即將執行的下一條指令位置。它們都是 CPU 在執行任何任務前,必須的依賴環境,因此叫做CPU上下文。
什麼是
CPU上下文切換
?
它是指,先把前一個任務的CPU上下文(也就是CPU暫存器和程式計數器)儲存起來,然後載入新任務的上下文到這些暫存器和程式計數器,最後再跳轉到程式計數器所指的新位置,執行新任務。
一般我們說的
上下文切換
,就是指核心(作業系統的核心)在CPU上對程序或者執行緒進行切換。程序從使用者態到核心態的轉變,需要透過
系統呼叫
來完成。系統呼叫的過程,會發生
CPU上下文的切換
。
CPU 暫存器裡原來使用者態的指令位置,需要先儲存起來。接著,為了執行核心態程式碼,CPU 暫存器需要更新為核心態指令的新位置。最後才是跳轉到核心態執行核心任務。
3.4 虛擬記憶體
現代作業系統使用虛擬記憶體,即虛擬地址取代物理地址,使用虛擬記憶體可以有2個好處:
虛擬記憶體空間可以遠遠大於物理記憶體空間
多個虛擬記憶體可以指向同一個物理地址
正是
多個虛擬記憶體可以指向同一個物理地址
,可以把核心空間和使用者空間的虛擬地址對映到同一個物理地址,這樣的話,就可以減少IO的資料複製次數啦,示意圖如下
3.5 DMA技術
DMA,英文全稱是
Direct Memory Access
,即直接記憶體訪問。
DMA
本質上是一塊主機板上獨立的晶片,允許外設裝置和記憶體儲存器之間直接進行IO資料傳輸,其過程
不需要CPU的參與
。
我們一起來看下IO流程,DMA幫忙做了什麼事情。
使用者應用程序呼叫read函式,向作業系統發起IO呼叫,進入阻塞狀態,等待資料返回。
CPU收到指令後,對DMA控制器發起指令排程。
DMA收到IO請求後,將請求傳送給磁碟;
磁碟將資料放入磁碟控制緩衝區,並通知DMA
DMA將資料從磁碟控制器緩衝區複製到核心緩衝區。
DMA向CPU發出資料讀完的訊號,把工作交換給CPU,由CPU負責將資料從核心緩衝區複製到使用者緩衝區。
使用者應用程序由核心態切換回使用者態,解除阻塞狀態
可以發現,DMA做的事情很清晰啦,它主要就是
幫忙CPU轉發一下IO請求,以及複製資料
。為什麼需要它的?
主要就是效率,它幫忙CPU做事情,這時候,CPU就可以閒下來去做別的事情,提高了CPU的利用效率。大白話解釋就是,CPU老哥太忙太累啦,所以他找了個小弟(名叫DMA) ,替他完成一部分的複製工作,這樣CPU老哥就能著手去做其他事情。
4. 零複製實現的幾種方式
零複製並不是沒有複製資料,而是減少使用者態/核心態的切換次數以及CPU複製的次數。零複製實現有多種方式,分別是
mmap+write
sendfile
帶有DMA收集複製功能的sendfile
4.1 mmap+write實現的零複製
mmap 的函式原型如下:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
addr:指定對映的虛擬記憶體地址
length:對映的長度
prot:對映記憶體的保護模式
flags:指定對映的型別
fd:進行對映的檔案控制代碼
offset:檔案偏移量
前面一小節,零複製相關的知識點回顧,我們介紹了
虛擬記憶體
,可以把核心空間和使用者空間的虛擬地址對映到同一個物理地址,從而減少資料複製次數!mmap就是用了虛擬記憶體這個特點,它將核心中的讀緩衝區與使用者空間的緩衝區進行對映,所有的IO都在核心中完成。
mmap+write
實現的零複製流程如下:
使用者程序透過
mmap方法
向作業系統核心發起IO呼叫,
上下文從使用者態切換為核心態
。
CPU利用DMA控制器,把資料從硬碟中複製到核心緩衝區。
上下文從核心態切換回使用者態
,mmap方法返回。
使用者程序透過
write
方法向作業系統核心發起IO呼叫,
上下文從使用者態切換為核心態
。
CPU將核心緩衝區的資料複製到的socket緩衝區。
CPU利用DMA控制器,把資料從socket緩衝區複製到網絡卡,
上下文從核心態切換回使用者態
,write呼叫返回。
可以發現,
mmap+write
實現的零複製,I/O發生了
4
次使用者空間與核心空間的上下文切換,以及3次資料複製。其中3次資料複製中,包括了
2次DMA複製和1次CPU複製
。
mmap
是將讀緩衝區的地址和使用者緩衝區的地址進行對映,核心緩衝區和應用緩衝區共享,所以節省了一次CPU複製‘’並且使用者程序記憶體是
虛擬的
,只是
對映
到核心的讀緩衝區,可以節省一半的記憶體空間。
4.2 sendfile實現的零複製
sendfile
是Linux2。1核心版本後引入的一個系統呼叫函式,API如下:
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
out_fd:為待寫入內容的檔案描述符,一個socket描述符。,
in_fd:為待讀出內容的檔案描述符,必須是真實的檔案,不能是socket和管道。
offset:指定從讀入檔案的哪個位置開始讀,如果為NULL,表示檔案的預設起始位置。
count:指定在fdout和fdin之間傳輸的位元組數。
sendfile表示在兩個檔案描述符之間傳輸資料,它是在
作業系統核心
中操作的,
避免了資料從核心緩衝區和使用者緩衝區之間的複製操作
,因此可以使用它來實現零複製。
sendfile實現的零複製流程如下:
使用者程序發起sendfile系統呼叫,
上下文(切換1)從使用者態轉向核心態
DMA控制器,把資料從硬碟中複製到核心緩衝區。
CPU將讀緩衝區中資料複製到socket緩衝區
DMA控制器,非同步把資料從socket緩衝區複製到網絡卡,
上下文(切換2)從核心態切換回使用者態
,sendfile呼叫返回。
可以發現,
sendfile
實現的零複製,I/O發生了
2
次使用者空間與核心空間的上下文切換,以及3次資料複製。其中3次資料複製中,包括了
2次DMA複製和1次CPU複製
。那能不能把CPU複製的次數減少到0次呢?有的,即
帶有DMA收集複製功能的sendfile
!
4.3 sendfile+DMA scatter/gather實現的零複製
linux 2。4版本之後,對
sendfile
做了最佳化升級,引入SG-DMA技術,其實就是對DMA複製加入了
scatter/gather
操作,它可以直接從核心空間緩衝區中將資料讀取到網絡卡。使用這個特點搞零複製,即還可以多省去
一次CPU複製
。
sendfile+DMA scatter/gather實現的零複製流程如下:
使用者程序發起sendfile系統呼叫,
上下文(切換1)從使用者態轉向核心態
DMA控制器,把資料從硬碟中複製到核心緩衝區。
CPU把核心緩衝區中的
檔案描述符資訊
(包括核心緩衝區的記憶體地址和偏移量)傳送到socket緩衝區
DMA控制器根據檔案描述符資訊,直接把資料從核心緩衝區複製到網絡卡
上下文(切換2)從核心態切換回使用者態
,sendfile呼叫返回。
可以發現,
sendfile+DMA scatter/gather
實現的零複製,I/O發生了
2
次使用者空間與核心空間的上下文切換,以及2次資料複製。其中2次資料複製都是包
DMA複製
。這就是真正的
零複製(Zero-copy)
技術,全程都沒有透過CPU來搬運資料,所有的資料都是透過DMA來進行傳輸的。
5. java提供的零複製方式
Java NIO對mmap的支援
Java NIO對sendfile的支援
5.1 Java NIO對mmap的支援
Java NIO有一個
MappedByteBuffer
的類,可以用來實現記憶體對映。它的底層是呼叫了Linux核心的
mmap
的API。
mmap的小demo
如下:
public class MmapTest {
public static void main(String[] args) {
try {
FileChannel readChannel = FileChannel。open(Paths。get(“。/jay。txt”), StandardOpenOption。READ);
MappedByteBuffer data = readChannel。map(FileChannel。MapMode。READ_ONLY, 0, 1024 * 1024 * 40);
FileChannel writeChannel = FileChannel。open(Paths。get(“。/siting。txt”), StandardOpenOption。WRITE, StandardOpenOption。CREATE);
//資料傳輸
writeChannel。write(data);
readChannel。close();
writeChannel。close();
}catch (Exception e){
System。out。println(e。getMessage());
}
}
}
5.2 Java NIO對sendfile的支援
FileChannel的
transferTo()/transferFrom()
,底層就是sendfile() 系統呼叫函式。Kafka 這個開源專案就用到它,平時面試的時候,回答面試官為什麼這麼快,就可以提到零複製
sendfile
這個點。
@Override
public long transferFrom(FileChannel fileChannel, long position, long count) throws IOException {
return fileChannel。transferTo(position, count, socketChannel);
}
sendfile的小demo
如下:
public class SendFileTest {
public static void main(String[] args) {
try {
FileChannel readChannel = FileChannel。open(Paths。get(“。/jay。txt”), StandardOpenOption。READ);
long len = readChannel。size();
long position = readChannel。position();
FileChannel writeChannel = FileChannel。open(Paths。get(“。/siting。txt”), StandardOpenOption。WRITE, StandardOpenOption。CREATE);
//資料傳輸
readChannel。transferTo(position, len, writeChannel);
readChannel。close();
writeChannel。close();
} catch (Exception e) {
System。out。println(e。getMessage());
}
}
}