零拷貝


概念

  1. 零拷貝

    CPU不執行數據從一個存儲區域到另一個存儲區域的任務。所以同一個存儲區域之間的拷貝也屬於零拷貝。

  2. DMA

    DMA(Direct Memory Access,直接存儲器訪問)。將一批數據從源地址搬運到目的地址去而不經過CPU的干預。相關知識可以參考DMA之理解

  3. I/O內存映射(mmap)

    關聯 進程中的1個虛擬內存區域 & 1個磁盤上的對象,使得二者存在映射關系。這樣不再需要來回的進行數據的復制,即數據不需要在內核空間和用戶空間進行數據拷貝了。此處留下一個問題,Java中的volatile關鍵字是和內存應該有關系么?

傳統I/O

在Java中,我們可以通過InputStream從數據源將數據讀取到緩沖區,然后可以通過OutputStream來將數據保存到數據源。這種方式相對來說效率低下。這是因為什么呢?我們看一下傳統IO操作,在系統層面發生了什么。

file

加入我們現在有一個需求:將某個圖片文件讀取出來,然后發送
我們看一下這里面的操作

  1. JVM發出Read()系統請求
  2. OS進行上下文切換,切到內核模式(第一次上下文切換),並將數據從數據源讀取到內核緩沖區(第一次數據拷貝:hardware->kernel-buffer)
  3. OS內核將數據從內核緩沖區復制到用戶空間緩沖區(第二次拷貝:kernel buffer->user buffer)。到此read()函數返回。系統調用返回導致從內核空間切到用戶空間(第二次上下文切換)
  4. JVM處理完代碼后調用write()系統請求。
  5. OS進行上下文切換,切到內核模式(第三次上下文切換),並將數據才能給用戶空間緩沖區復制到內核緩沖區(第三次拷貝:user buffer->kernel buffer)
  6. write系統返回,從內核空間切換到用戶空間(第四次上下文切換)。然后系統將內核緩沖區的數據寫到協議引擎上(這里是網卡設備)(第四次拷貝:kernel buffer->hardware).。

整體來說,進行了4次上下文的切換和4次的數據拷貝。但是其實是有2次拷貝是沒有用的,如果我們直接從hardware讀取數據到kernel buffer之后,再從kernel buffer直接寫到目標地址就可以了,完全沒有必要走一遍用戶空間。

sendfile 實現零拷貝I/O

sendfile()方法是系統提供給我們的一種能夠實現我們剛才的需求的一種方案。我們看一下使用sendfile之后的數據流向和上下文切換。
file
我們總結一下具體的流程:

  1. JVM發出transferTo()系統請求
  2. OS進行上下文切換,切到內核模式(第一次上下文切換),通過DMA將數據從數據源讀取到內核緩沖區(第一次數據拷貝:hardware->kernel-buffer)
  3. 內核將數據從內核緩沖區拷貝到與socket相關內核緩沖區(第二次拷貝:user buffer->kernel buffer)
  4. sendfile系統返回,從內核空間切換到用戶空間(第二次上下文切換)。然后系統將內核緩沖區的數據寫到協議引擎上(這里是網卡設備)(第三次拷貝:kernel buffer->hardware).。
    通過sendfile實現的零拷貝I/O只使用了2次上下文的切換和3次數據的拷貝。

我們所謂的0拷貝,並不是說真的0次數據的拷貝,而是相對於操作系統層面,沒有用戶空間和內核空間之間的數據拷貝過程。這就有一個經典的面試題了:0拷貝,是真的一次拷貝都不需要么?答案是顯而易見的~~~

機智的小伙伴發現了,上述過程中從kernel buffer中將數據copy到socket buffer是沒有必要的。都是在內核中操作,我直接從內核緩沖區拷貝到hardware不就可以了嗎?嗯,小伙伴還是很有思考頭腦的嘛~

其實上述方式是在Linux 2.1內核中使用的方案,在后來可能研發人員也發現了改進方案,所以在Linux 2.4中改進了代碼的相關實現。

帶有DMA的sendfile實現的零拷貝I/O

帶有DMA的sendfile就是機智的小伙伴所要的答案。我們先看看他的時序圖

file

我們總結一下具體的流程:

  1. JVM發出sendfile系統請求
  2. OS進行上下文切換,切到內核模式(第一次上下文切換),通過DMA將數據從數據源讀取到內核緩沖區(第一次數據拷貝:hardware->kernel-buffer)
  3. 這里沒有將數據拷貝到socket緩沖區,而是將相應的描述符信息拷貝到socket緩沖區中。描述符中記錄了kernel buffer的內存地址以及偏移量。
  4. sendfile系統返回,從內核空間切換到用戶空間(第二次上下文切換)。DMA根據socket緩沖區中的描述符信息,直接將內核空間的數據拷貝到協議引擎上。
    通過sendfile實現的零拷貝I/O只使用了2次上下文的切換和3次數據的拷貝。

帶有DMA功能的sendfile實現IO的過程中,只使用了2次的上下文切換和2次的數據拷貝過程。相對於第二種方案,速度又有了提升。

基於mmap的優化

在相關概念里面,我們知道了,mmap 通過內存映射,將文件映射到內核緩沖區,同時,用戶空間可以共享內核空間的數據。這樣,我們在進行數據傳輸時,其實可以通過mmap技術,減少內核空間到用戶空間之間的數據拷貝。

file

我們看一下這里面的操作

  1. JVM發出mmap系統請求
  2. OS進行上下文切換,切到內核模式(第一次上下文切換),並將數據從數據源讀取到內核緩沖區(第一次數據拷貝:hardware->kernel-buffer)
  3. mmap系統調用返回,導致從內核空間切到用戶空間(第二次上下文切換)。接着將內核緩沖區映射到用戶空間緩沖區(這里並沒有進行數據的拷貝)。用戶空間和內核空間共享這個緩沖區,而不需要將數據從內核空間拷貝到用戶空間。因為用戶空間和內核空間共享了這個緩沖區數據,所以用戶空間就可以像在操作自己緩沖區中數據一般操作這個由內核空間共享的緩沖區數據。
  4. JVM處理完代碼后調用write()系統請求。
  5. OS進行上下文切換,切到內核模式(第三次上下文切換),並將數據從內核緩沖區復制到socket的內核緩沖區(第二次拷貝:kernel buffer->socket buffer)
  6. write系統返回,從內核空間切換到用戶空間(第四次上下文切換)。然后系統將內核緩沖區的數據寫到協議引擎上(這里是網卡設備)(第三次拷貝:kernel buffer->hardware)。

可以看到,相比較於傳統的IO,通過mmap優化,將拷貝次數從四次變為了3次。其中3次數據拷貝中包括了2次DMA拷貝和1次CPU拷貝,能夠提升一部分IO效率。

mmap VS sendFile

mmap和sendfile都屬於零拷貝的實現方式。在具體的選擇上,要根據實際的情況來進行考慮。

  1. mmp適合小數據量讀寫,sendFile適合大文件傳輸。
  2. mmp需要4次上下文切換,3次數據拷貝;sendFile需要3次上下文切換,3次(或者2次)數據拷貝。
  3. sendFile可以利用DMA方式減少CPU拷貝;mmap只能減少用戶層和內核層之間的拷貝,不能減少CPU拷貝。

在進行數據傳輸時,不同的開源應用采用了不同的實現方式:

sendFile使用者:Tomcat內部文件拷貝,Tomcat的心跳保活,kafka,pulsar 下載文件
mmap使用者:rocketMQ消費消息
剩下的使用案例,歡迎大家補充

本文由 開了肯 發布!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM