看一遍就理解:零拷貝詳解
前言
大家好,我是 程式設計師田螺 。
零拷貝是老生常談的問題啦,大廠非常喜歡問。比如Kafka為什麼快,RocketMQ為什麼快等,都涉及到零拷貝知識點。最近技術討論群幾個夥伴分享了 阿里、蝦皮的面試真題 ,也都涉及到零拷貝。因此本文將跟大家一起來學習零拷貝原理。
1. 什麼是零拷貝
2. 傳統的IO執行流程
3. 零拷貝相關的知識點回顧
4. 零拷貝實現的幾種方式
5. java提供的零拷貝方式
1.什麼是零拷貝
零拷貝字面上的意思包括兩個,“零”和“拷貝”:
-
“拷貝”:就是指資料從一個儲存區域轉移到另一個儲存區域。
-
“零” :表示次數為0,它表示拷貝資料的次數為0。
合起來,那 零拷貝 就是不需要將資料從一個儲存區域複製到另一個儲存區域咯。
零拷貝是指計算機執行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拷貝呢?我們一起來回顧下,零拷貝涉及的 作業系統知識點 哈。
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()); } } }
- 面試官:cglib為什麼不能代理private方法?
- 想看Dubbo原始碼?一定要先看一看這一篇!
- 死磕synchronized二:系統剖析延遲偏向篇一
- 架構與思維:高併發下解決主從延時的一些思路
- 道與術
- OopMap看不懂,怎麼調優哇
- Kafka 精妙的高效能設計(下篇)
- 一次tcp視窗被填滿問題的排查實踐
- 搶了個票,還以為發現了12306的系統BUG
- 微服務5:服務註冊與發現(實踐篇)
- 看一遍就理解:零拷貝詳解
- 分散式:分散式系統下的唯一序列
- 面試官:為什麼jdk動態代理只能代理介面實現類?
- 揭開記憶體屏障的神祕面紗
- 微服務4:服務註冊與發現
- 我就奇了怪了,STW到底是怎麼做到的
- 這樣使用 IDEA ,效率提升10倍!| IDEA 高效使用指南
- 從hotspot原始碼層面剖析Java的多型實現原理
- 垃圾回收全集之十二:GC 調優的實戰篇—Weak, Soft 及 Phantom 引用
- JVM的多型是如何實現的