零拷貝

概念

  1. 零拷貝

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

  2. DMA

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

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

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

傳統I/O

在Java中,咱們能夠經過InputStream從數據源將數據讀取到緩衝區,而後能夠經過OutputStream來將數據保存到數據源。這種方式相對來講效率低下。這是由於什麼呢?咱們看一下傳統IO操做,在系統層面發生了什麼。函數

ss

加入咱們如今有一個需求:將某個圖片文件讀取出來,而後發送
咱們看一下這裏面的操做優化

  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直接寫到目標地址就能夠了,徹底沒有必要走一遍用戶空間。spa

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拷貝,是真的一次拷貝都不須要麼?答案是顯而易見的~~~.net

機智的小夥伴發現了,上述過程當中從kernel buffer中將數據copy到socket buffer是沒有必要的。都是在內核中操做,我直接從內核緩衝區拷貝到hardware不就能夠了嗎?嗯,小夥伴仍是頗有思考頭腦的嘛~對象

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

帶有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消費消息
剩下的使用案例,歡迎你們補充

本文由 開了肯 發佈!
相關文章
相關標籤/搜索