前言
大白話解釋,零拷貝就是沒有把數據從一個存儲區域拷貝到另一個存儲區域。但是沒有數據的復制,怎么可能實現數據的傳輸呢?其實我們在java NIO、netty、kafka遇到的零拷貝,並不是不復制數據,而是減少不必要的數據拷貝次數,從而提升代碼性能
- 零拷貝的好處
- 內核空間和用戶空間
- 緩沖區和虛擬內存
- 傳統的 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數據復制
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拷貝
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());
}
}