1. 程式人生 > 其它 >零拷貝技術

零拷貝技術

注意事項:除Direct I/O,與磁碟相關的檔案的讀寫都有使用到Page cache技術。

Netty、Kafka和Mysql等開源元件都用到了零拷貝這個核心技術。

1、資料的四次拷貝和四次上下文切換

很用應用程式在面臨客戶端請求時,可以等價為進行如下的系統呼叫:

1 File.read(file, buf, len);
2 Socket.send(socket, buf, len);

例如,訊息中介軟體Kafka就是這個應用場景,從磁碟中讀取一批訊息後原封不動地寫入網絡卡(NIC,Network interface controler)進行傳送。

在沒有任何優化技術使用的背景下,作業系統為此將進行4次資料拷貝和四次上下文切換,如下圖所示:

如果沒有優化,讀取磁碟資料,在通過網絡卡輸送的場景效能比較差:

1.1 4次拷貝

  • 物理裝置<->記憶體:
    • CPU負責將資料從磁碟搬運到核心空間的Page Cache中。
    • CPU負責將資料從核心空間的Socket緩衝區搬運到網路中。
  • 記憶體內部拷貝:
    • CPU負責將資料從核心空間的Page Cache搬運到使用者空間緩衝區。
    • CPU負責將資料從使用者空間的緩衝區搬運到核心空間的Socket緩衝區。

1.2 4次上下文切換

  1. read系統呼叫時:使用者態切換到核心態;
  2. read系統呼叫完畢時:核心態切換回使用者態;
  3. write系統呼叫時:使用者態切換內切換到核心態;
  4. write系統呼叫完畢時:核心態切換回使用者態;

這時遇到的問題:

  1. CPU全程負責記憶體內部的資料拷貝還可以接受,因為記憶體的資料拷貝效率還行(不過還是比CPU慢很多),但是如果要CPU全程負責記憶體與磁碟、記憶體與網絡卡的資料拷貝,這將難以接受,因為磁碟、網絡卡的I/O速度遠小於記憶體。
  2. 4次拷貝太多了,4次上下文切換太過頻繁。

2、DMA參與下的四次資料拷貝

DMA技術就是在主機板上放一塊獨立的晶片。在進行記憶體和I/O裝置的資料傳輸的時候,我們不再通過CPU來控制資料傳輸,而直接通過DMA控制器(DMA Controller,簡稱DMAC)。這塊晶片,我們可以認為它其實就是一個協處理器(Co-Processor)。

 DMAC的價值在下列情況下尤其明顯:

  • 傳輸的資料特別大、速度特別快。
  • 傳輸的資料特別小,速度特別慢。

比如,用千兆網絡卡或者硬碟傳輸大量資料時,如果都用CPU來搬運的話,肯定忙不過來,所以可以選擇DMAC。而當傳輸很慢的時候,DMAC可以等資料到齊了,再發送訊號,給到CPU去處理,而不是讓CPU在哪裡忙等。

注意:這裡的“協”字,DMA是在“協助”CPU,完成對應對資料傳輸工作。在DMAC控制資料傳輸的過程中,DMAC還是被CPU控制,只是資料的拷貝行為不再由CPU來完成。

原本計算機所有元件之間的資料拷貝(流動)必須經過CPU。以磁碟讀寫為例,如下圖所示:

現在,DMAC代替了CPU負責記憶體磁碟、記憶體與網絡卡之間的資料搬運,CPU作為DMAC的控制者,如下圖所示:

但是DMAC有其侷限性,DMAC僅僅能用於裝置間資料交換時進行資料拷貝,但是裝置內部之間的資料拷貝還需要CPU來親力親為。例如,CPU需要負責核心空間與使用者空間之間的資料拷貝(記憶體內部的拷貝),如下圖所示:

 

上圖中的read buffer也是page cache,socket buffer也就是socket緩衝區。

3、零拷貝技術

3.1 什麼是零拷貝技術

零拷貝技術是一種思想,指的是計算機執行操作時,CPU不需要先將資料從某處記憶體複製到另一個特定的區域。

可見,零拷貝的特點就是CPU不全程負責記憶體中的資料寫入其他元件,CPU僅僅起到管理的作用。但注意,零拷貝不是不進行拷貝,而是CPU不再全程負責資料拷貝的搬運工作。如果資料本身不在記憶體中,那麼必須先通過某種方式拷貝到記憶體中(這個過程CPU可以僅僅負責管理,DMAC來負責具體資料拷貝),因為資料只有在記憶體中,才能被轉移,才能被CPU直接讀取計算。

零拷貝技術的具體實現方式有很多,例如:

  • sendfile
  • mmap
  • 直接Direct I/O
  • splice

不同的零拷貝技術適用不同的應用場景,下面依次進行sendfile、mmap、Direct I/O的分析。

這裡先做一個前瞻性的技術總結:

  • DMA技術:DMA負責記憶體與其他元件之間的資料拷貝,CPU僅需負責管理,而無需負責全程的資料拷貝。
  • 使用page cache的zero copy:
    • sendfile:一次代替read/write系統呼叫,通過DMA技術以及傳遞檔案描述符,實現zero copy
    • mmap:僅代替read系統呼叫,將核心空間地址對映為使用者空間地址,write操作直接作用於核心空間。通過DMA技術以及地址對映技術,使用者空間與核心空間無須資料拷貝,實現了zero copy
  • 不使用page cache的Direct I/O:讀寫操作直接在磁碟上進行,不使用page cache機制,通常結合使用者空間的使用者快取使用。通過DMA技術直接與磁碟/網絡卡進行資料互動,實現zero copy。

3.2 sendfile

sendfile的應用場景是:使用者從磁碟讀起一些檔案資料後,不需要經過任何計算與處理就通過網路傳輸出去。此場景的典型應用就是訊息佇列。

在傳統I/O下,正如第一節所示,上述應用場景的一次資料傳輸需要四次CPU全權負責的拷貝與上下文切換。

sendfile主要使用到了兩個技術:

  • DMA技術
  • 傳遞檔案描述符代替資料拷貝

下面依次講解這兩個技術的作用:

  • 利用DMA技術

sendfile依賴DM啊技術,將四次CPU全程負責的拷貝與四次上下文切換減少到兩次,如下圖所示:

 

利用DMA技術減少2次CPU全程參與到拷貝

DMA負責磁碟到核心空間中Page Cache(Read Buffer)的資料拷貝以及從核心空間中socket buffer到網絡卡到資料拷貝。

 

  • 傳遞檔案描述符代替資料拷貝 

傳遞檔案描述可以代替資料拷貝,這是由於兩個原因:

  1. page cache以及socket buffer都在核心空間中;
  2. 資料在傳輸中沒有被更新;

利用傳遞檔案描述符代替核心中的資料拷貝

注意事項:只有網絡卡支援SG-DMA(The Scatter-Gather Direct Memory Access)技術才可以通過傳遞檔案描述符的方式避免核心空間的一次CPU拷貝。這意味著此優化取決於Linux系統的物理網絡卡是否支援(Linux在核心2.4版本里引入了DMA的scatter/gather-分散/收集功能,只要確保Linux版本高於2.4即可)

  • 一次系統呼叫代替兩次系統呼叫

由於sendfile僅僅對應一次系統呼叫,而傳統檔案操作則需要使用read以及write兩個系統呼叫。正因為如此,sendfile能夠將使用者態與核心態之間的上下文切換從4次降到2次。

sendfile系統呼叫僅僅需要兩次上下文切換

另一方面,我們需要注意sendfile系統呼叫的侷限性。如果應用程式需要對從磁碟讀取的資料進行寫操作,例如解密或加密,那麼sendfile系統呼叫就無法使用。這是因為使用者執行緒根本就不能夠通過sendfile系統呼叫到傳輸到資料。

3.3 mmap

  • mmap基礎概念

mmap即memory map,也就是記憶體對映。

mmap是一種記憶體對映到方法,即將一個檔案或者其他物件對映到程序的地址空間,實現檔案的磁碟地址和程序虛擬地址空間中一段虛擬地址的一一對映關係。實現這樣的對映關係後,程序就可以採用指標的方式讀寫這段記憶體,而系統會自動回血髒頁面到對應的檔案磁碟上,即完成了對檔案的操作而不必再呼叫read、write等系統呼叫函式。相反,核心空間對這段區域對修改也直接反映到使用者空間,從而實現不同程序間的檔案共享。如下圖所示:

mmap具有如下特點:

  1. mmap嚮應用程式提供記憶體訪問介面的記憶體地址是連續的,但是對應的磁碟檔案的block可以不是地址連續的;
  2. mmap提供的記憶體空間是虛擬空間(虛擬記憶體),而不是物理空間(實體記憶體),因此完全可以分配遠遠大於實體記憶體大小的虛擬空間(例如16G記憶體主機分配1000G的mmap記憶體空間);
  3. mmap負責對映檔案邏輯上一段連續的資料(物理上可以不連續儲存)對映為連續記憶體,而這裡的檔案可以是磁碟檔案、驅動假造出的檔案(例如DMA技術)以及裝置;
  4. mmap由作業系統負責管理,對同一個檔案地址的對映將被所有執行緒共享,作業系統確保執行緒安全以及執行緒可見性;

mmap的設計很有啟發性。基於磁碟的讀寫單位是block(一般大小為4KB),而基於記憶體的讀寫單位是地址(雖然記憶體的管理和分配單位是4KB)。換言之,CPU進行一次磁碟讀寫操作涉及的數量至少是4KB,但是進行一次記憶體操作涉及的資料是基於地址的,也就是通常的64bit(64位作業系統)。mmap下程序可以採用指標的方式進行讀寫操作,著是值得注意的。

  • mmap的I/O模型

mmap也是一種零拷貝技術,其I/O模型如下圖所示:

mmap技術有如下特點:

  1. 利用DMA技術來取代CPU在記憶體與其他元件之間的資料拷貝,例如從磁碟到記憶體,從記憶體到網絡卡;
  2. 使用者空間的mmap file使用虛擬記憶體,實際上不佔實體記憶體,只有在核心空間的kernel buffer cache才佔據實際的實體記憶體;
  3. mmap函式需要配合write()系統呼叫進行操作,這與sendfile()函式有所不同,後者一次性替代了read()/wirte();因此mmap也至少需要4次上下文切換;
  4. mmap僅僅能夠避免核心空間到使用者空間的全程CPU負責的資料拷貝,但是核心空間內部還是需要全程CPU負責的資料拷貝;

利用mmap()替換read(),配合write()呼叫的整個流程如下:

  1. 使用者程序呼叫mmap(),從使用者態陷入核心態,將核心緩衝區對映到使用者緩衝區;
  2. DMA控制器將資料從硬碟拷貝到核心緩衝區(可見其使用了page cache機制);
  3. mmap()返回,上下文從核心態切回用戶態;
  4. 使用者程序呼叫write(),嘗試把檔案資料寫到核心的套接字緩衝區,再次陷入核心態;
  5. CPU將核心緩衝區的資料拷貝到套接字緩衝區;
  6. DMA控制器將資料從套接字緩衝區拷貝到網絡卡傳送Ringbuffer中,完成資料傳輸。
  7. write()返回,上下文從核心態切回用戶態。
  • mmap的優勢

1)簡化使用者程序程式設計 

在使用者空間看來,通過mmap機制以後,磁碟上的檔案彷彿直接就在記憶體中,把訪問磁碟檔案簡化為按地址訪問記憶體。這樣一來,應用程式自然不需要使用檔案系統write(寫入)、讀取(read)、fsync(同步)等系統呼叫,因為現在只要面向記憶體等虛擬空間進行開發。

但是,這並不意外這我們不再需要進行這些系統呼叫,而是說這些系統呼叫由作業系統在mmap機制等內部封裝好了。

基於缺頁異常等懶載入

出於節約實體記憶體以及mmap方法快速返回的目的,mmap對映採用懶載入機制。具體來說,通過mmap申請1000G記憶體可能僅僅佔用了100MB的虛擬記憶體空間,甚至沒有分配實際的實體記憶體空間。當你訪問香港記憶體地址時,才會進行真正的write、read等系統呼叫。CPU會通過陷入缺頁異常等方式來將磁碟上的資料載入到實體記憶體中,此時才會發生真正的實體記憶體分配。

資料一致性由OS確保

當發生資料修改時,記憶體出現髒頁,與磁碟檔案出現不一致。mmap機制下由作業系統自動完成記憶體資料落盤(髒頁回刷),使用者程序通常並不需要手動管理資料落盤。

2)讀寫效率提高:避免核心空間到使用者空間的資料拷貝

簡而言之,mmap被認為快的原因是因為建立了頁到使用者程序的虛擬空間的對映,以讀取檔案為例,避免了頁從核心空間拷貝到使用者空間。

3)避免只讀操作時到swap操作

虛擬記憶體帶來了種種好處,但是一個最大的問題在於所有 程序的虛擬記憶體大小總和可能大於實體記憶體大小,因此當作業系統實體記憶體不夠用時,就會把一部分記憶體swap到磁碟上。

在mmap下,如果虛擬空間沒有發生讀寫操作,那麼由於通過mmap操作得到的記憶體資料完全可以通過再次呼叫mmap操作對映檔案得到。但是,通過其他方式分配的記憶體,在沒有發生寫操作的情況下,作業系統並不知道如何簡單地從現有檔案中(除非其重新執行一遍應用程式,但是代價很大)恢復記憶體資料,因此必須將記憶體swap到磁碟上。

4)節約記憶體

由於使用者空間與核心空間實際上共用一份資料,因此在大檔案場景下實際實體記憶體佔用上有優勢。

  • mmap不是銀彈

mmap不是銀彈,這意味著mmap也有其缺陷,在相關場景下的效能存在缺陷:

  1. 由於mmap使用時必須事先指定好記憶體對映的大小,因此mmap不適合變長檔案;
  2. 如果更新檔案的操作比較多,mmap避免兩態拷貝的優勢就被攤還,最終還是落在了大量的髒頁回xie及由此引發的隨機I/O上,所以在隨機寫很多的情況下,mmap方式在效率上不一定比帶緩衝區的一般寫快。
  3. 讀/寫小檔案(例如16K以下的檔案),mmap與通過read系統呼叫相比有著更高的開銷與延遲;同時mmap的刷盤由系統全權控制,但是在小資料量大情況下由應用本身手動控制更好。
  4. mmap受限於作業系統記憶體大小:例如在32bits作業系統上,虛擬記憶體總大小也就2GB,但由於mmap必須要在記憶體中找到一塊連續的地址塊,此時你就無法對4GB大小的檔案完全進行mmap,在這種情況下你必須分配多塊分別進行mmap,但是此時記憶體地址已經不再連續,使用mmap的意義大打折扣,而且引入了額外的複雜性。
  • mmap的適用場景

mmap的適用場景實際上非常受限,在如下場合下可以選擇使用mmap機制:

  1. 多個執行緒以只讀的方式同時訪問一個檔案,這是因為mmap機制下多執行緒共享了同一個實體記憶體空間,因此節約了記憶體。案例:多程序可能依賴同一個動態連線庫,利用mmap可以實現記憶體僅僅價值一份動態連線庫,多個程序共享此動態連線庫。
  2. mmap非常適合用於程序間通訊,這是因為對同一對應的mmap分配的實體記憶體天然多執行緒共享,並可以依賴於作業系統同步原語;
  3. mmap雖然比sendfile等機制多了一次CPU全程參與的記憶體拷貝,但是使用者空間語核心空間並不需要資料拷貝,因此在正確使用情況下並不比sendfile效率差。

3.4 Direct I/O

 Direct I/O即直接I/O。其名字中的“直接”二字用於區分使用page cache機制的快取I/O。

  • 快取檔案I/O:使用者空間要讀寫一個檔案並不直接與磁碟互動,而是中間夾層快取,即page cache;
  • 直接檔案I/O:使用者空間讀取的檔案直接與磁碟互動,中間沒有page catch層;

“直接”在這裡還有另一層語義:其他所有技術中,資料至少需要在核心空間儲存一份,但是在Direct I/O技術中,資料直接儲存在使用者空間中,繞過了核心。

Direct I/O模式如下圖所示:

此時,使用者空間直接通過DMA的方式與磁碟以及網絡卡進行資料拷貝。

事實上,即使Direct I/O還是可能需要使用作業系統的fsync系統呼叫的。因為雖然檔案的資料本身沒有使用任何快取,但是檔案的元資料仍然需要快取,包括VFS中的inode cache和dentry cache等。

在部分作業系統中,在Direct I/O模式下進行write系統呼叫能夠確保檔案資料落盤,但是檔案元資料不一定落盤。如果在此類作業系統上,那麼還需要執行一次fsync系統呼叫卻不檔案元資料頁落盤。否則,可能會導致檔案異常,元資料缺失等情況。MySQL等O_DIRECT與O_DIRECT_NO_FSYNC配置是一個具體的案例。

1)優點

  1. Linux中的直接I/O技術省略掉快取I/O技術中作業系統核心緩衝區的使用,資料直接在應用程式地址空間和磁碟之間進行傳輸,從而使得自快取應用程式可以省略掉複雜的系統級別的快取結構,而執行程式自己定義的資料讀寫管理,從而降低系統級別的管理對應用程式訪問資料的影響;
  2. 與其他零拷貝技術一樣,避免了核心空間到使用者空間的資料拷貝,如果要傳輸的資料量很大,使用直接I/O的方式進行資料傳輸,而不需要作業系統核心地址空間資料拷貝操作的參與,這將會大大提高效能。

2)缺點

  1. 由於裝置之間的資料傳輸是通過DMA完成的,因此使用者空間的資料緩衝區記憶體必須進行page pinning(頁鎖定),這是為了防止其物理頁框地址被交換到磁碟或者被移動到新的地址而導致DMA去拷貝資料的時候再指定的地址找不到記憶體頁從而引發缺頁錯誤,而頁鎖定的開銷並不必CPU拷貝小,所以為了避免頻繁的頁鎖定系統呼叫,應用程式必須分配和註冊一個持久的記憶體池,用於資料緩衝;
  2. 如果訪問的資料不在應用程式快取中,那麼每次資料都會直接從磁碟進行載入,這種直接載入會非常緩慢。
  3. 在應用層引入直接I/O需要應用自己管理,這帶來了額外的系統複雜性;

誰會使用Direct I/O?

自快取應用程式(self-caching applications)可以選擇使用Direct I/O。

自快取應用程式

對於某些應用程式來說,它會有它自己的資料快取機制,比如,它會將資料快取在應用程式地址空間,這類應用程式完全不需要使用作業系統核心中的高速緩衝儲存器,這類應用程式就被稱作是自快取應用程式(self-caching applications)。

例如,應用內部維護一個快取空間,當有讀取操作時,首先讀取應用層的快取資料,如果沒有,那麼就通過Direct I/O直接通過磁碟I/O來讀取資料。快取仍然在應用,只不過應用覺得自己實現一個快取比作業系統的快取更高效。

資料庫管理系統就是這類應用程式的一個代表。自快取應用程式傾向於使用資料的邏輯表示式,而非物理表示式;當系統記憶體較低的時候,自快取應用程式會讓這種資料的邏輯快取被換出,而並非時磁碟上實際的資料被換出。自快取應用程式對要操作的資料的語義瞭如執掌,所以它可以採用更加高效的快取替換演算法。自快取應用程式有可能會在多臺主機之間共享一塊記憶體,那麼自快取應用程式就需要提供一種能夠有效地將使用者地址空間的快取資料置為無效的機制,從而確保應用程式地址空間快取資料的一致性。

page cache 是Linux為所有應用程式提供的快取機制,但是資料庫應用太特殊了,page cache影響了資料對特性的追求。

另一方面,目前Linux上的非同步IO庫,其依賴於檔案使用O_DIRECT模式開啟,它們通常一起配合使用。

如何使用Direct I/O?

使用者應用需要實現使用者空間內的快取區,讀/寫操作應當儘量通過此快取區提供。如果有效能上的考慮,那麼儘量避免頻繁地基於 Direct I/O 進行讀/寫操作。

4、經典案例

4.1 Kafka

Kafka 作為一個訊息佇列,涉及到磁碟 I/O 主要有兩個操作:

  • Provider 向 Kakfa 傳送訊息,Kakfa 負責將訊息以日誌的方式持久化落盤;
  • Consumer 向 Kakfa 進行拉取訊息,Kafka 負責從磁碟中讀取一批日誌訊息,然後再通過網絡卡傳送;

Kakfa 服務端接收 Provider 的訊息並持久化的場景下使用 mmap 機制,能夠基於順序磁碟 I/O 提供高效的持久化能力,使用的 Java 類為 java.nio.MappedByteBuffer。
Kakfa 服務端向 Consumer 傳送訊息的場景下使用 sendfile 機制,這種機制主要兩個好處:

  • sendfile 避免了核心空間到使用者空間的 CPU 全程負責的資料移動;
  • sendfile 基於 Page Cache 實現,因此如果有多個 Consumer 在同時消費一個主題的訊息,那麼由於訊息一直在 page cache 中進行了快取,因此只需一次磁碟 I/O,就可以服務於多個 Consumer;

使用 mmap 來對接收到的資料進行持久化,使用 sendfile 從持久化介質中讀取資料然後對外發送是一對常用的組合。但是注意,你無法利用 sendfile 來持久化資料,利用 mmap 來實現 CPU 全程不參與資料搬運的資料拷貝。

4.2 MySQL

MySQL 的具體實現比 Kakfa 複雜很多,這是因為支援 SQL 查詢的資料庫本身比訊息佇列對複雜很多。可以參考MySQL 的零拷貝技術。

5、總結

DMA 技術使得記憶體與其他元件,例如磁碟、網絡卡進行資料拷貝時,CPU 僅僅需要發出控制訊號,而拷貝資料的過程則由 DMAC 負責完成。

Linux 的零拷貝技術有多種實現策略,但根據策略可以分為如下幾種型別:

  • 減少甚至避免使用者空間和核心空間之間的資料拷貝:在一些場景下,使用者程序在資料傳輸過程中並不需要對資料進行訪問和處理,那麼資料在 Linux 的 Page Cache 和使用者程序的緩衝區之間的傳輸就完全可以避免,讓資料拷貝完全在核心裡進行,甚至可以通過更巧妙的方式避免在核心裡的資料拷貝。這一類實現一般是是通過增加新的系統呼叫來完成的,比如 Linux 中的 mmap(),sendfile() 以及 splice() 等。
  • 繞過核心的直接 I/O:允許在使用者態程序繞過核心直接和硬體進行資料傳輸,核心在傳輸過程中只負責一些管理和輔助的工作。這種方式其實和第一種有點類似,也是試圖避免使用者空間和核心空間之間的資料傳輸,只是第一種方式是把資料傳輸過程放在核心態完成,而這種方式則是直接繞過核心和硬體通訊,效果類似但原理完全不同。
  • 核心緩衝區和使用者緩衝區之間的傳輸優化:這種方式側重於在使用者程序的緩衝區和作業系統的頁快取之間的 CPU 拷貝的優化。這種方法延續了以往那種傳統的通訊方式,但更靈活。