看一遍就理解:零拷貝詳解
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實現的零拷貝
- 用戶進程發起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());
}
}
}
參考
[1]框架篇:小白也能秒懂的Linux零拷貝原理: https://juejin.cn/post/6887469050515947528
[2]深入剖析Linux IO原理和幾種零拷貝機制的實現: https://juejin.cn/post/6844903949359644680#heading-11