零拷貝
本文圖片和一些內容均來自后面的參考,非原創只是把文章中的一些關鍵內容整理一下,算作是一個學習筆記。
傳統的I/O操作
傳統的IO操作是用戶應用程序只是需要調用兩個系統調用 read() 和 write() 就可以完成這個數據傳輸操作,但是底層會發生很多步驟,這些步驟對上層都是隱藏的。我們來梳理一下。
當應用程序需要訪問某塊數據的時候:
- 應用程序發起系統調用
read()
讀取文件(一次上下文切換,或者說是模式切換模式切換[1],用戶態切換到內核態) - 操作系統內核會先檢查這塊數據是不是已經被存放在操作系統內核地址空間的緩沖區內,如果存在就直接返回。如果不在就執行下一步。
- 如果在內核緩沖區中找不到這塊數據(叫做缺頁,會觸發缺頁異常),Linux 操作系統內核會先將這塊數據從磁盤讀出來放到操作系統內核的緩沖區里去(一次DMA[2]拷貝,硬盤到頁緩存)
- 然后內核把這塊數據拷貝到應用程序的地址空間中去(一次CPU拷貝,內核空間到用戶空間)
read()
函數返回。(一次上下文切換,或者說是模式切換,內核態切換到用戶態)- 應用程序調用
write()
函數向socket緩沖區寫數據。(一次上下文切換,或者說是模式切換,用戶態切換到內核態) - 內核需要將數據再一次從用戶應用程序地址空間的緩沖區拷貝到與網絡堆棧相關的內核緩沖區(一次CPU拷貝,內核空間內)
- 執行DMA拷貝,把內核的socket緩沖區數據通過DMA方式發送給物理網卡,在執行期間用戶空間應用程序的
write()
函數返回。(一次上下文切換,或者說是模式切換,內核態切換到用戶態)
從上面過程來看,經過了4次上下文切換或者是模式切換,4次拷貝操作(2次DMA拷貝,2次CPU拷貝)。
為什么需要零拷貝
從上面過程來看,4次切換和4次拷貝,整個處理過程比較冗長,但這還不是問題,在網絡速度比較慢的時代(56K貓、10/100MB以太網)其實不需要這種技術,因為內部再快也會被網絡速率卡住,木桶效應。但是當網路速度大幅提升出現1Gb、10Gb甚至100Gb網速的時候這種零拷貝技術就迫切需要,因為網絡傳輸速度已經遠遠大於計算機內部的數據流轉速度。所以有必要提速,那么這時候人們就關注如何優化計算機內部數據流轉。
零拷貝解決了什么問題
零拷貝技術的實現有很多種,但歸根結底其目的是減少數據傳輸的中間環節,尤其是上述過程中的用戶空間和內核空間的數據拷貝。
減少CPU拷貝的方法
直接I/O
緩存 I/O 又被稱作標准 I/O,大多數文件系統的默認 I/O 操作都是緩存 I/O。在 Linux 的緩存 I/O 機制中,操作系統會將 I/O 的數據緩存在文件系統的頁緩存(page cache)。讀取數據的時候先在緩沖中查找如果命中就直接返回,沒有命中則去磁盤讀取。其實這種機制是一種為了提高速度減少IO操作的良性機制,因為畢竟磁盤屬於低速設備。
那么反過來在寫數據的時候應用程序也是先寫到頁緩存,至於是否會立即同步到磁盤這取決於采用的寫操作機制,到底是同步寫還是異步寫。同步寫機制應用程序會立刻得到響應,而異步寫則會稍晚些得到響應。當然還有另外一種機制就是延遲寫入機制,不過延遲寫入寫到磁盤上的時候不會通知應用程序。
在直接I/O機制中,數據均直接在用戶地址空間的緩沖區和磁盤之間直接進行傳輸,完全不需要頁緩存的支持。這類零拷貝技術針對的是操作系統內核並不需要對數據進行直接處理的情況。在某些場景下會使用到這種方式。
Kafka就利用這種緩存I/O機制,寫入緩存,讀取的時候也從緩存讀取,這樣吞吐量非常高,但是數據丟失風險就會比較高,因為大量數據在內存中,不過參數可以調整。
mmap
應用程序調用了mmap()
之后,發生2次上下文切換(調用和返回)。數據拷貝除了2次DMA沒有變化之外最主要的就是減少了一次內核到用戶空間的數據拷貝,而是直接從頁緩存拷貝到socke緩沖區,所以跟標准I/O比,就變成了2次上下文切換,2次DMA拷貝,1次CPU拷貝。這個優化就減少了中間環節。
但是對文件進行了內存映射,就是應用程序緩沖區和內核空間緩沖區都映射到同一地址范圍的物理內存,你也可以說操作系統共享這個緩沖區給應用程序,而且映射操作也是一個開銷很大的虛擬存儲操作,這種操作需要通過更改頁表以及沖刷 TLB (使得 TLB 的內容無效)來維持存儲的一致性。不過這種刷新TLB的開銷要比。
不過mmap有一個比較大的隱患就是,調用 write() 系統調用,如果此時其他的進程截斷了這個文件,那么 write() 系統調用將會被總線錯誤信號 SIGBUS 中斷,因為此時正在執行的是一個錯誤的存儲訪問。這個信號將會導致進程被殺死。
sendfile
從上圖可以看到應用程序調用sendfile()
系統調用這里就只發生2次上下文切換(調用和返回)。數據拷貝除了2次DMA沒有變化之外最主要的就是減少了一次內核到用戶空間的數據拷貝,而是直接從頁緩存拷貝到socke緩沖區,所以跟標准I/O比,就變成了2次上下文切換,2次DMA拷貝,1次CPU拷貝。這個優化就減少了中間環節,提高了內部傳輸效率也解放了CPU。不過這並不是零拷貝,因為還有1次CPU拷貝。
在高級語言中如何使用這種特性就需要去查看該語言的庫函數,看看那些庫函數底層調用的是
sendfile()
系統調用。
帶DMA的sendfile
這種方式就是為了解決sendfile中的那1次CPU拷貝,也就是內核緩沖區到socket緩沖區的拷貝。不拷貝的話該如何發送數據呢?就是將內核緩沖區中待發送數據的描述符發送到網絡協議棧中,然后在socket緩沖區中建立數據包的結構,最后通過DMA的收集功能將所有的數據結合成一個網絡數據包。網卡的 DMA 引擎會在一次操作中從多個位置讀取包頭和數據。Linux 2.4 版本中的 socket 緩沖區就可以滿足這種條件,這也就是用於 Linux 中的眾所周知的零拷貝技術。
- 首先,sendfile() 系統調用利用 DMA 引擎將文件內容拷貝到內核緩沖區去;
- 然后,將帶有文件位置和長度信息的緩沖區描述符添加到 socket 緩沖區中去,此過程不需要將數據從操作系統內核緩沖區拷貝到 socket 緩沖區中;
- 最后,DMA 引擎會將數據直接從內核緩沖區拷貝到協議引擎中去,這樣就避免了最后一次數據拷貝。
sendfile的局限性
首先,sendfile只適用於數據發送端;其次要發送的數據中間不能被修改而是原樣發送的。