看一遍就理解:零拷貝詳解

源?/?? ? ? ??文/?
前言
1.什么是零拷貝
“拷貝”:就是指數(shù)據(jù)從一個存儲區(qū)域轉(zhuǎn)移到另一個存儲區(qū)域。 “零” :表示次數(shù)為0,它表示拷貝數(shù)據(jù)的次數(shù)為0。
零拷貝是指計算機執(zhí)行IO操作時,CPU不需要將數(shù)據(jù)從一個存儲區(qū)域復(fù)制到另一個存儲區(qū)域,從而可以減少上下文切換以及CPU的拷貝時間。它是一種 I/O操作優(yōu)化技術(shù)。
2. 傳統(tǒng) IO 的執(zhí)行流程
while((n?=?read(diskfd,?buf,?BUF_SIZE))?>?0)
????write(sockfd,?buf?,?n);
read:把數(shù)據(jù)從磁盤讀取到內(nèi)核緩沖區(qū),再拷貝到用戶緩沖區(qū)write:先把數(shù)據(jù)寫入到socket緩沖區(qū),最后寫入網(wǎng)卡設(shè)備。

用戶應(yīng)用進程調(diào)用read函數(shù),向操作系統(tǒng)發(fā)起IO調(diào)用,上下文從用戶態(tài)轉(zhuǎn)為內(nèi)核態(tài)(切換1) DMA控制器把數(shù)據(jù)從磁盤中,讀取到內(nèi)核緩沖區(qū)。 CPU把內(nèi)核緩沖區(qū)數(shù)據(jù),拷貝到用戶應(yīng)用緩沖區(qū),上下文從內(nèi)核態(tài)轉(zhuǎn)為用戶態(tài)(切換2),read函數(shù)返回 用戶應(yīng)用進程通過write函數(shù),發(fā)起IO調(diào)用,上下文從用戶態(tài)轉(zhuǎn)為內(nèi)核態(tài)(切換3) CPU將用戶緩沖區(qū)中的數(shù)據(jù),拷貝到socket緩沖區(qū) DMA控制器把數(shù)據(jù)從socket緩沖區(qū),拷貝到網(wǎng)卡設(shè)備,上下文從內(nèi)核態(tài)切換回用戶態(tài)(切換4),write函數(shù)返回
3. 零拷貝相關(guān)的知識點回顧
3.1 內(nèi)核空間和用戶空間
內(nèi)核空間:主要提供進程調(diào)度、內(nèi)存分配、連接硬件資源等功能 用戶空間:提供給各個程序進程的空間,它不具有訪問內(nèi)核空間資源的權(quán)限,如果應(yīng)用程序需要使用到內(nèi)核空間的資源,則需要通過系統(tǒng)調(diào)用來完成。進程從用戶空間切換到內(nèi)核空間,完成相關(guān)操作后,再從內(nèi)核空間切換回用戶空間。
3.2 什么是用戶態(tài)、內(nèi)核態(tài)
如果進程運行于內(nèi)核空間,被稱為進程的內(nèi)核態(tài) 如果進程運行于用戶空間,被稱為進程的用戶態(tài)。
3.3 什么是上下文切換
什么是CPU上下文?
CPU 寄存器,是CPU內(nèi)置的容量小、但速度極快的內(nèi)存。而程序計數(shù)器,則是用來存儲 CPU 正在執(zhí)行的指令位置、或者即將執(zhí)行的下一條指令位置。它們都是 CPU 在運行任何任務(wù)前,必須的依賴環(huán)境,因此叫做CPU上下文。
什么是CPU上下文切換?
它是指,先把前一個任務(wù)的CPU上下文(也就是CPU寄存器和程序計數(shù)器)保存起來,然后加載新任務(wù)的上下文到這些寄存器和程序計數(shù)器,最后再跳轉(zhuǎn)到程序計數(shù)器所指的新位置,運行新任務(wù)。
CPU 寄存器里原來用戶態(tài)的指令位置,需要先保存起來。接著,為了執(zhí)行內(nèi)核態(tài)代碼,CPU 寄存器需要更新為內(nèi)核態(tài)指令的新位置。最后才是跳轉(zhuǎn)到內(nèi)核態(tài)運行內(nèi)核任務(wù)。

3.4 虛擬內(nèi)存
虛擬內(nèi)存空間可以遠遠大于物理內(nèi)存空間 多個虛擬內(nèi)存可以指向同一個物理地址

3.5 DMA技術(shù)

用戶應(yīng)用進程調(diào)用read函數(shù),向操作系統(tǒng)發(fā)起IO調(diào)用,進入阻塞狀態(tài),等待數(shù)據(jù)返回。 CPU收到指令后,對DMA控制器發(fā)起指令調(diào)度。 DMA收到IO請求后,將請求發(fā)送給磁盤; 磁盤將數(shù)據(jù)放入磁盤控制緩沖區(qū),并通知DMA DMA將數(shù)據(jù)從磁盤控制器緩沖區(qū)拷貝到內(nèi)核緩沖區(qū)。 DMA向CPU發(fā)出數(shù)據(jù)讀完的信號,把工作交換給CPU,由CPU負責(zé)將數(shù)據(jù)從內(nèi)核緩沖區(qū)拷貝到用戶緩沖區(qū)。 用戶應(yīng)用進程由內(nèi)核態(tài)切換回用戶態(tài),解除阻塞狀態(tài)
主要就是效率,它幫忙CPU做事情,這時候,CPU就可以閑下來去做別的事情,提高了CPU的利用效率。大白話解釋就是,CPU老哥太忙太累啦,所以他找了個小弟(名叫DMA) ,替他完成一部分的拷貝工作,這樣CPU老哥就能著手去做其他事情。
4. 零拷貝實現(xiàn)的幾種方式
mmap+write sendfile 帶有DMA收集拷貝功能的sendfile
4.1 mmap+write實現(xiàn)的零拷貝
void?*mmap(void?*addr,?size_t?length,?int?prot,?int?flags,?int?fd,?off_t?offset);
addr:指定映射的虛擬內(nèi)存地址 length:映射的長度 prot:映射內(nèi)存的保護模式 flags:指定映射的類型 fd:進行映射的文件句柄 offset:文件偏移量
mmap+write實現(xiàn)的零拷貝流程如下:
用戶進程通過 mmap方法向操作系統(tǒng)內(nèi)核發(fā)起IO調(diào)用,上下文從用戶態(tài)切換為內(nèi)核態(tài)。CPU利用DMA控制器,把數(shù)據(jù)從硬盤中拷貝到內(nèi)核緩沖區(qū)。 上下文從內(nèi)核態(tài)切換回用戶態(tài),mmap方法返回。 用戶進程通過 write方法向操作系統(tǒng)內(nèi)核發(fā)起IO調(diào)用,上下文從用戶態(tài)切換為內(nèi)核態(tài)。CPU將內(nèi)核緩沖區(qū)的數(shù)據(jù)拷貝到的socket緩沖區(qū)。 CPU利用DMA控制器,把數(shù)據(jù)從socket緩沖區(qū)拷貝到網(wǎng)卡,上下文從內(nèi)核態(tài)切換回用戶態(tài),write調(diào)用返回。
mmap+write實現(xiàn)的零拷貝,I/O發(fā)生了4次用戶空間與內(nèi)核空間的上下文切換,以及3次數(shù)據(jù)拷貝。其中3次數(shù)據(jù)拷貝中,包括了2次DMA拷貝和1次CPU拷貝。mmap是將讀緩沖區(qū)的地址和用戶緩沖區(qū)的地址進行映射,內(nèi)核緩沖區(qū)和應(yīng)用緩沖區(qū)共享,所以節(jié)省了一次CPU拷貝‘’并且用戶進程內(nèi)存是虛擬的,只是映射到內(nèi)核的讀緩沖區(qū),可以節(jié)省一半的內(nèi)存空間。4.2 sendfile實現(xiàn)的零拷貝
sendfile是Linux2.1內(nèi)核版本后引入的一個系統(tǒng)調(diào)用函數(shù),API如下:ssize_t?sendfile(int?out_fd,?int?in_fd,?off_t?*offset,?size_t?count);
out_fd:為待寫入內(nèi)容的文件描述符,一個socket描述符。, in_fd:為待讀出內(nèi)容的文件描述符,必須是真實的文件,不能是socket和管道。 offset:指定從讀入文件的哪個位置開始讀,如果為NULL,表示文件的默認起始位置。 count:指定在fdout和fdin之間傳輸?shù)淖止?jié)數(shù)。

用戶進程發(fā)起sendfile系統(tǒng)調(diào)用,上下文(切換1)從用戶態(tài)轉(zhuǎn)向內(nèi)核態(tài) DMA控制器,把數(shù)據(jù)從硬盤中拷貝到內(nèi)核緩沖區(qū)。 CPU將讀緩沖區(qū)中數(shù)據(jù)拷貝到socket緩沖區(qū) DMA控制器,異步把數(shù)據(jù)從socket緩沖區(qū)拷貝到網(wǎng)卡, 上下文(切換2)從內(nèi)核態(tài)切換回用戶態(tài),sendfile調(diào)用返回。
sendfile實現(xiàn)的零拷貝,I/O發(fā)生了2次用戶空間與內(nèi)核空間的上下文切換,以及3次數(shù)據(jù)拷貝。其中3次數(shù)據(jù)拷貝中,包括了2次DMA拷貝和1次CPU拷貝。那能不能把CPU拷貝的次數(shù)減少到0次呢?有的,即帶有DMA收集拷貝功能的sendfile!4.3 sendfile+DMA scatter/gather實現(xiàn)的零拷貝
sendfile做了優(yōu)化升級,引入SG-DMA技術(shù),其實就是對DMA拷貝加入了scatter/gather操作,它可以直接從內(nèi)核空間緩沖區(qū)中將數(shù)據(jù)讀取到網(wǎng)卡。使用這個特點搞零拷貝,即還可以多省去一次CPU拷貝。
用戶進程發(fā)起sendfile系統(tǒng)調(diào)用,上下文(切換1)從用戶態(tài)轉(zhuǎn)向內(nèi)核態(tài) DMA控制器,把數(shù)據(jù)從硬盤中拷貝到內(nèi)核緩沖區(qū)。 CPU把內(nèi)核緩沖區(qū)中的文件描述符信息(包括內(nèi)核緩沖區(qū)的內(nèi)存地址和偏移量)發(fā)送到socket緩沖區(qū) DMA控制器根據(jù)文件描述符信息,直接把數(shù)據(jù)從內(nèi)核緩沖區(qū)拷貝到網(wǎng)卡 上下文(切換2)從內(nèi)核態(tài)切換回用戶態(tài),sendfile調(diào)用返回。
sendfile+DMA scatter/gather實現(xiàn)的零拷貝,I/O發(fā)生了2次用戶空間與內(nèi)核空間的上下文切換,以及2次數(shù)據(jù)拷貝。其中2次數(shù)據(jù)拷貝都是包DMA拷貝。這就是真正的?零拷貝(Zero-copy)?技術(shù),全程都沒有通過CPU來搬運數(shù)據(jù),所有的數(shù)據(jù)都是通過DMA來進行傳輸?shù)摹?/span>5. java提供的零拷貝方式
Java NIO對mmap的支持 Java NIO對sendfile的支持
5.1 Java NIO對mmap的支持
MappedByteBuffer的類,可以用來實現(xiàn)內(nèi)存映射。它的底層是調(diào)用了Linux內(nèi)核的mmap的API。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);
????????????//數(shù)據(jù)傳輸
????????????writeChannel.write(data);
????????????readChannel.close();
????????????writeChannel.close();
????????}catch?(Exception?e){
????????????System.out.println(e.getMessage());
????????}
????}
}
5.2 Java NIO對sendfile的支持
transferTo()/transferFrom(),底層就是sendfile() 系統(tǒng)調(diào)用函數(shù)。Kafka 這個開源項目就用到它,平時面試的時候,回答面試官為什么這么快,就可以提到零拷貝sendfile這個點。@Override
public?long?transferFrom(FileChannel?fileChannel,?long?position,?long?count)?throws?IOException?{
???return?fileChannel.transferTo(position,?count,?socketChannel);
}
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);
????????????//數(shù)據(jù)傳輸
????????????readChannel.transferTo(position,?len,?writeChannel);
????????????readChannel.close();
????????????writeChannel.close();
????????}?catch?(Exception?e)?{
????????????System.out.println(e.getMessage());
????????}
????}
}
END


頂級程序員:topcoding
做最好的程序員社區(qū):Java后端開發(fā)、Python、大數(shù)據(jù)、AI
一鍵三連「分享」、「點贊」和「在看」
評論
圖片
表情
