前言
大白話解釋,零拷貝就是沒有把數據從一個存儲區域拷貝到另外一個存儲區域。可是沒有數據的複製,怎麼可能實現數據的傳輸呢?其實咱們在java NIO、netty、kafka遇到的零拷貝,並非不復制數據,而是減小沒必要要的數據拷貝次數,從而提高代碼性能java
-
零拷貝的好處 -
內核空間和用戶空間 -
緩衝區和虛擬內存 -
傳統的 I/O -
mmap+write 實現的零拷貝 -
sendfile 實現的零拷貝 -
帶有DMA收集拷貝功能的sendfile實現的零拷貝 -
java提供的零拷貝方式
「關注公衆號,一塊兒交流 :潛行前行」
零拷貝的好處
-
減小或避免沒必要要的CPU數據拷貝,從而釋放CPU去執行其餘任務 -
零拷貝機制能減小用戶空間和操做系統內核空間的上下文切換 -
減小內存的佔用
內核空間和用戶空間
-
內核空間:Linux自身使用的空間;主要提供進程調度、內存分配、鏈接硬件資源等功能 -
用戶空間:提供給各個程序進程的空間;用戶空間不具備訪問內核空間資源的權限,若是應用程序須要使用到內核空間的資源,則須要經過系統調用來完成:從用戶空間切換到內核空間,完成相關操做後再從內核空間切換回用戶空間
緩衝區和虛擬內存
-
直接內存訪問(Direct Memory Access)(DMA) -
直接內存訪問:DMA容許外設設備和內存存儲器之間直接進行IO數據傳輸,其過程不須要CPU的參與
-
緩衝區 是全部I/O的基礎,I/O 無非就是把數據移進或移出緩衝區 -
進程發起read請求,內核先檢查內核空間緩衝區是否存在進程所需數據,若是已經存在,則直接copy數據到進程的內存區。若是沒有,系統則向磁盤請求數據,經過DMA寫入內核的read緩衝衝區,接着再將內核緩衝區數據copy到進程的內存區 -
進程發起write請求,則是把進程的內存區數據copy到內核的write緩衝區,而後再經過DMA把內核緩衝區數據刷回磁盤或者網卡中 -
虛擬內存:現代操做系統都使用虛擬內存,有以下兩個好處 -
一個以上的虛擬地址能夠指向同一個物理內存地址 -
虛擬內存空間可大於實際可用的物理地址 -
利用第一點特性能夠把內核空間地址和用戶空間的虛擬地址映射到同一個物理地址,這樣DMA就能夠填充(讀寫)對內核和用戶空間進程同時可見的緩衝區了;大體以下
傳統的 I/O
#include <unistd>
ssize_t write(int filedes, void *buf, size_t nbytes);
ssize_t read(int filedes, void *buf, size_t nbytes);
-
如java在linux系統上,讀取一個磁盤文件,併發送到遠程端的服務
-
1)發出read系統調用,會致使用戶空間到內核空間的上下文切換,而後再經過DMA將文件中的數據從磁盤上讀取到內核空間緩衝區 -
2)接着將內核空間緩衝區的數據拷貝到用戶空間進程內存,而後read系統調用返回。而系統調用的返回又會致使一次內核空間到用戶空間的上下文切換 -
3)write系統調用,則再次致使用戶空間到內核空間的上下文切換,將用戶空間的進程裏的內存數據複製到內核空間的socket緩衝區(也是內核緩衝區,不過是給socket使用的),而後write系統調用返回,再次觸發上下文切換 -
4)至於socket緩衝區到網卡的數據傳輸則是獨立異步的過程,也就是說write系統調用的返回並不保證數據被傳輸到網卡
「一共有四次用戶空間與內核空間的上下文切換。四次數據copy,分別是兩次CPU數據複製,兩次DMA數據複製」linux
mmap+write實現的零拷貝
#include <sys/mman.h>
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset)
-
1)發出mmap系統調用,致使用戶空間到內核空間的上下文切換。而後經過DMA引擎將磁盤文件中的數據複製到內核空間緩衝區 -
2)mmap系統調用返回,致使內核空間到用戶空間的上下文切換 -
3)這裏不須要將數據從內核空間複製到用戶空間,由於用戶空間和內核空間共享了這個緩衝區 -
4)發出write系統調用,致使用戶空間到內核空間的上下文切換。將數據從內核空間緩衝區複製到內核空間socket緩衝區;write系統調用返回,致使內核空間到用戶空間的上下文切換 -
5)異步,DMA引擎將socket緩衝區中的數據copy到網卡
「經過mmap實現的零拷貝I/O進行了4次用戶空間與內核空間的上下文切換,以及3次數據拷貝;其中3次數據拷貝中包括了2次DMA拷貝和1次CPU拷貝」web
sendfile實現的零拷貝
#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
-
1)發出sendfile系統調用,致使用戶空間到內核空間的上下文切換,而後經過DMA引擎將磁盤文件中的內容複製到內核空間緩衝區中,接着再將數據從內核空間緩衝區複製到socket相關的緩衝區 -
2)sendfile系統調用返回,致使內核空間到用戶空間的上下文切換。DMA異步將內核空間socket緩衝區中的數據傳遞到網卡
「經過sendfile實現的零拷貝I/O使用了2次用戶空間與內核空間的上下文切換,以及3次數據的拷貝。其中3次數據拷貝中包括了2次DMA拷貝和1次CPU拷貝」面試
帶有DMA收集拷貝功能的sendfile實現的零拷貝
-
從Linux 2.4版本開始,操做系統提供scatter和gather的SG-DMA方式,直接從內核空間緩衝區中將數據讀取到網卡,無需將內核空間緩衝區的數據再複製一份到socket緩衝區
-
1)發出sendfile系統調用,致使用戶空間到內核空間的上下文切換。經過DMA引擎將磁盤文件中的內容複製到內核空間緩衝區 -
2)這裏沒把數據複製到socket緩衝區;取而代之的是,相應的描述符信息被複制到socket緩衝區。該描述符包含了兩種的信息:A)內核緩衝區的內存地址、B)內核緩衝區的偏移量 -
3)sendfile系統調用返回,致使內核空間到用戶空間的上下文切換。DMA根據socket緩衝區的描述符提供的地址和偏移量直接將內核緩衝區中的數據複製到網卡
「帶有DMA收集拷貝功能的sendfile實現的I/O使用了2次用戶空間與內核空間的上下文切換,以及2次數據的拷貝,並且這2次的數據拷貝都是非CPU拷貝。這樣一來咱們就實現了最理想的零拷貝I/O傳輸了,不須要任何一次的CPU拷貝,以及最少的上下文切換」微信
java提供的零拷貝方式
-
java NIO的零拷貝實現是基於mmap+write方式 -
FileChannel的map方法產生的MappedByteBuffer FileChannel提供了map()方法,該方法能夠在一個打開的文件和MappedByteBuffer之間創建一個虛擬內存映射,MappedByteBuffer繼承於ByteBuffer;該緩衝器的內存是一個文件的內存映射區域。map方法底層是經過mmap實現的,所以將文件內存從磁盤讀取到內核緩衝區後,用戶空間和內核空間共享該緩衝區。用法以下
public void main(String[] args){
try {
FileChannel readChannel = FileChannel.open(Paths.get("./cscw.txt"), StandardOpenOption.READ);
FileChannel writeChannel = FileChannel.open(Paths.get("./siting.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
MappedByteBuffer data = readChannel.map(FileChannel.MapMode.READ_ONLY, 0, 1024 * 1024 * 40);
//數據傳輸
writeChannel.write(data);
readChannel.close();
writeChannel.close();
}catch (Exception e){
System.out.println(e.getMessage());
}
}
-
FileChannel的transferTo、transferFrom 若是操做系統底層支持的話,transferTo、transferFrom也會使用相關的零拷貝技術來實現數據的傳輸。用法以下
public void main(String[] args) {
try {
FileChannel readChannel = FileChannel.open(Paths.get("./cscw.txt"), StandardOpenOption.READ);
FileChannel writeChannel = FileChannel.open(Paths.get("./siting.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
long len = readChannel.size();
long position = readChannel.position();
//數據傳輸
readChannel.transferTo(position, len, writeChannel);
//效果和transferTo 同樣的
//writeChannel.transferFrom(readChannel, position, len, );
readChannel.close();
writeChannel.close();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
歡迎指正文中錯誤
參考文章
-
淺談 Linux下的零拷貝機制 [1] -
面試被問到「零拷貝」!你真的理解嗎? [2] -
java NIO 的通道Channel的理解 [3] -
Channel基本使用——FileChannel類和內存映射的使用 [4]
Reference
淺談 Linux下的零拷貝機制: https://www.jianshu.com/p/e76e3580e356併發
[2]面試被問到「零拷貝」!你真的理解嗎?: https://my.oschina.net/u/3990817/blog/3045359app
[3]java NIO 的通道Channel的理解: https://blog.csdn.net/qq_27092581/article/details/78347198異步
[4]Channel基本使用——FileChannel類和內存映射的使用: https://blog.csdn.net/qq_45337431/article/details/99645809socket
本文分享自微信公衆號 - 潛行前行(qianxingcsc)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。編輯器