Linux 中的零拷貝技術 (二) 轉


 

傳統IO拷貝流程

 比如:讀取文件,再用socket發送出去

 先讀取、再發送,實際經過1~4四次copy。

  1、第一次:將磁盤文件,讀取到操作系統內核緩沖區;

  2、第二次:將內核緩沖區的數據,copy到application應用程序的buffer;

  3、第三步:將application應用程序buffer中的數據,copy到socket網絡發送緩沖區(屬於操作系統內核的緩沖區);

  4、第四次:將socket buffer的數據,copy到網卡,由網卡進行網絡傳輸。

 

                  

           圖 1. 傳統使用 read 和 write 系統調用的數據傳輸

 

  傳統方式,讀取磁盤文件並進行網絡發送,經過的四次數據copy是非常繁瑣的。實際IO讀寫,需要進行IO中斷,需要CPU響應中斷(帶來上下文切換),盡管后來引入DMA來接管CPU的中斷請求,但四次copy是存在“不必要的拷貝”的。

 

題外話:什么是DMA?

  其實DMA技術很容易理解,本質上,DMA技術就是我們在主板上放⼀塊獨立的芯片。在進行內存和I/O設備的數據傳輸的時候,我們不再通過CPU來控制數據傳輸,而直接通過 DMA控制器(DMA?Controller,簡稱DMAC)。這塊芯片,我們可以認為它其實就是一個協處理器(Co-Processor))

 

Linux 下實現零拷貝的幾種方式介紹

Linux 中的直接 I/O

  如果應用程序可以直接訪問網絡接口存儲,那么在應用程序訪問數據之前存儲總線就不需要被遍歷,數據傳輸所引起的開銷將會是最小的。應用程序或者運行在用戶模式下的庫函數可以直接訪問硬件設備的存儲,操作系統內核除了進行必要的虛擬存儲配置工作之外,不參與數據傳輸過程中的其它任何事情。直接 I/O 使得數據可以直接在應用程序和外圍設備之間進行傳輸,完全不需要操作系統內核頁緩存的支持。關於直接 I/O 技術的具體實現細節可以參看 developerWorks 上的另一篇文章”Linux 中直接 I/O 機制的介紹” ,本文不做過多描述。

             

               圖 2. 使用直接 I/O 的數據傳輸

 

針對數據傳輸不需要經過應用程序地址空間的零拷貝技術

利用 mmap()

在 Linux 中,減少拷貝次數的一種方法是調用 mmap() 來代替調用 read,比如:

tmp_buf = mmap(file, len);
write(socket, tmp_buf, len); 

  應用程序調用mmap(),磁盤上的數據會通過DMA被拷貝的內核緩沖區,接着操作系統會把這段內核緩沖區與應用程序共享,這樣就不需要把內核緩沖區的內容往用戶空間拷貝。應用程序再調用write(),操作系統直接將內核緩沖區的內容拷貝到socket緩沖區中,這一切都發生在內核態,最后,socket緩沖區再把數據發到網卡去。同樣的,看圖很簡單:
              

              圖 3. 利用 mmap() 代替 read()

 

  通過使用 mmap() 來代替 read(), 已經可以減半操作系統需要進行數據拷貝的次數。當大量數據需要傳輸的時候,這樣做就會有一個比較好的效率。但是,這種改進也是需要代價的,使用 mma()p 其實是存在潛在的問題的。當對文件進行了內存映射,然后調用 write() 系統調用,如果此時其他的進程截斷了這個文件,那么 write() 系統調用將會被總線錯誤信號 SIGBUS 中斷,因為此時正在執行的是一個錯誤的存儲訪問。這個信號將會導致進程被殺死,解決這個問題可以通過以下這兩種方法:

  1. 為 SIGBUS 安裝一個新的信號處理器,這樣,write() 系統調用在它被中斷之前就返回已經寫入的字節數目,errno 會被設置成 success。但是這種方法也有其缺點,它不能反映出產生這個問題的根源所在,因為 BIGBUS 信號只是顯示某進程發生了一些很嚴重的錯誤。
  2. 第二種方法是通過文件租借鎖來解決這個問題的,這種方法相對來說更好一些。我們可以通過內核對文件加讀或者寫的租借鎖,當另外一個進程嘗試對用戶正在進行傳輸的文件進行截斷的時候,內核會發送給用戶一個實時信號:RT_SIGNAL_LEASE 信號,這個信號會告訴用戶內核破壞了用戶加在那個文件上的寫或者讀租借鎖,那么 write() 系統調用則會被中斷,並且進程會被 SIGBUS 信號殺死,返回值則是中斷前寫的字節數,errno 也會被設置為 success。文件租借鎖需要在對文件進行內存映射之前設置。

  使用 mmap 是 POSIX 兼容的,但是使用 mmap 並不一定能獲得理想的數據傳輸性能。數據傳輸的過程中仍然需要一次 CPU 拷貝操作,而且映射操作也是一個開銷很大的虛擬存儲操作,這種操作需要通過更改頁表以及沖刷 TLB (使得 TLB 的內容無效)來維持存儲的一致性。但是,因為映射通常適用於較大范圍,所以對於相同長度的數據來說,映射所帶來的開銷遠遠低於 CPU 拷貝所帶來的開銷。

 

使用sendfile()

從2.1版內核開始,Linux引入了sendfile來簡化操作:

#include<sys/sendfile.h>ssize\_t sendfile(int out\_fd, int in\_fd, off\_t *offset, size_t count);

  系統調用sendfile()在代表輸入文件的描述符infd和代表輸出文件的描述符outfd之間傳送文件內容(字節)。描述符outfd必須指向一個套接字,而infd指向的文件必須是可以mmap的。這些局限限制了sendfile的使用,使sendfile只能將數據從文件傳遞到套接字上,反之則不行。

                    

                   圖 4. 利用 sendfile () 進行數據傳輸

 

  sendfile() 不僅減少了數據拷貝操作,它也減少了上下文切換。數據傳送始終只發生在kernel space。首先:sendfile() 系統調用利用 DMA 引擎將文件中的數據拷貝到操作系統內核緩沖區中,然后數據被拷貝到與 socket 相關的內核緩沖區中去。接下來,DMA 引擎將數據從內核 socket 緩沖區中拷貝到協議引擎中去。如果在用戶調用 sendfile () 系統調用進行數據傳輸的過程中有其他進程截斷了該文件,那么 sendfile () 系統調用會簡單地返回給用戶應用程序中斷前所傳輸的字節數,errno 會被設置為 success。如果在調用 sendfile() 之前操作系統對文件加上了租借鎖,那么 sendfile() 的操作和返回狀態將會和 mmap()/write () 一樣。

 

  sendfile() 系統調用不需要將數據拷貝或者映射到應用程序地址空間中去,所以 sendfile() 只是適用於應用程序地址空間不需要對所訪問數據進行處理的情況。相對於 mmap() 方法來說,因為 sendfile 傳輸的數據沒有越過用戶應用程序 / 操作系統內核的邊界線,所以 sendfile () 也極大地減少了存儲管理的開銷。但是,sendfile () 也有很多局限性,如下所列:

  • sendfile() 局限於基於文件服務的網絡應用程序,比如 web 服務器。據說,在 Linux 內核中實現 sendfile() 只是為了在其他平台上使用 sendfile() 的 Apache 程序。
  • 由於網絡傳輸具有異步性,很難在 sendfile () 系統調用的接收端進行配對的實現方式,所以數據傳輸的接收端一般沒有用到這種技術。
  • 基於性能的考慮來說,sendfile () 仍然需要有一次從文件到 socket 緩沖區的 CPU 拷貝操作,這就導致頁緩存有可能會被傳輸的數據所污染。

 

帶有 DMA 收集拷貝功能的 sendfile()

  上小節介紹的 sendfile() 技術在進行數據傳輸仍然還需要一次多余的數據拷貝操作,通過引入一點硬件上的幫助,這僅有的一次數據拷貝操作也可以避免。為了避免操作系統內核造成的數據副本,需要用到一個支持收集操作的網絡接口,這也就是說,待傳輸的數據可以分散在存儲的不同位置上,而不需要在連續存儲中存放。這樣一來,從文件中讀出的數據就根本不需要被拷貝到 socket 緩沖區中去,而只是需要將緩沖區描述符傳到網絡協議棧中去,之后其在緩沖區中建立起數據包的相關結構,然后通過 DMA 收集拷貝功能將所有的數據結合成一個網絡數據包。網卡的 DMA 引擎會在一次操作中從多個位置讀取包頭和數據。Linux 2.4 版本中的 socket 緩沖區就可以滿足這種條件,這也就是用於 Linux 中的眾所周知的零拷貝技術,這種方法不但減少了因為多次上下文切換所帶來開銷,同時也減少了處理器造成的數據副本的個數。對於用戶應用程序來說,代碼沒有任何改變。首先,sendfile() 系統調用利用 DMA 引擎將文件內容拷貝到內核緩沖區去;然后,將帶有文件位置和長度信息的緩沖區描述符添加到 socket 緩沖區中去,此過程不需要將數據從操作系統內核緩沖區拷貝到 socket 緩沖區中,DMA 引擎會將數據直接從內核緩沖區拷貝到協議引擎中去,這樣就避免了最后一次數據拷貝。

             

                  圖 5. 帶有 DMA 收集拷貝功能的 sendfile

 

  通過這種方法,CPU 在數據傳輸的過程中不但避免了數據拷貝操作,理論上,CPU 也永遠不會跟傳輸的數據有任何關聯,這對於 CPU 的性能來說起到了積極的作用:首先,高速緩沖存儲器沒有受到污染;其次,高速緩沖存儲器的一致性不需要維護,高速緩沖存儲器在 DMA 進行數據傳輸前或者傳輸后不需要被刷新。然而實際上,后者實現起來非常困難。源緩沖區有可能是頁緩存的一部分,這也就是說一般的讀操作可以訪問它,而且該訪問也可以是通過傳統方式進行的。只要存儲區域可以被 CPU 訪問到,那么高速緩沖存儲器的一致性就需要通過 DMA 傳輸之前沖刷新高速緩沖存儲器來維護。而且,這種數據收集拷貝功能的實現是需要硬件以及設備驅動程序支持的。

 

splice()

  splice() 是  Linux  中與 mmap() 和  sendfile() 類似的一種方法。它也可以用於用戶應用程序地址空間和操作系統地址空間之間的數據傳輸。splice() 適用於可以確定數據傳輸路徑的用戶應用程序,它不需要利用用戶地址空間的緩沖區進行顯式的數據傳輸操作。那么,當數據只是從一個地方傳送到另一個地方,過程中所傳輸的數據不需要經過用戶應用程序的處理的時候,spice() 就成為了一種比較好的選擇。splice() 可以在操作系統地址空間中整塊地移動數據,從而減少大多數數據拷貝操作。而且,splice() 進行數據傳輸可以通過異步的方式來進行,用戶應用程序可以先從系統調用返回,而操作系統內核進程會控制數據傳輸過程繼續進行下去。splice() 可以被看成是類似於基於流的管道的實現,管道可以使得兩個文件描述符相互連接,splice 的調用者則可以控制兩個設備(或者協議棧)在操作系統內核中的相互連接。

  splice() 系統調用和 sendfile() 非常類似,用戶應用程序必須擁有兩個已經打開的文件描述符,一個用於表示輸入設備,一個用於表示輸出設備。與 sendfile() 不同的是,splice() 允許任意兩個文件之間互相連接,而並不只是文件到 socket 進行數據傳輸。對於從一個文件描述符發送數據到 socket 這種特例來說,一直都是使用 sendfile() 這個系統調用,而 splice 一直以來就只是一種機制,它並不僅限於 sendfile() 的功能。也就是說,sendfile() 只是 splice() 的一個子集,在 Linux 2.6.23 中,sendfile() 這種機制的實現已經沒有了,但是這個 API 以及相應的功能還存在,只不過 API 以及相應的功能是利用了 splice() 這種機制來實現的。  

  在數據傳輸的過程中,splice() 機制交替地發送相關的文件描述符的讀寫操作,並且可以將讀緩沖區重新用於寫操作。它也利用了一種簡單的流控制,通過預先定義的水印( watermark )來阻塞寫請求。有實驗表明,利用這種方法將數據從一個磁盤傳輸到另一個磁盤會增加 30% 到 70% 的吞吐量,數據傳輸的過程中, CPU 的負載也會減少一半。

  Linux 2.6.17 內核引入了 splice() 系統調用,但是,這個概念在此之前 ] 其實已經存在了很長一段時間了。1988 年,Larry McVoy 提出了這個概念,它被看成是一種改進服務器端系統的 I/O 性能的一種技術,盡管在之后的若干年中經常被提及,但是 splice 系統調用從來沒有在主流的 Linux 操作系統內核中實現過,一直到 Linux 2.6.17 版本的出現。splice 系統調用需要用到四個參數,其中兩個是文件描述符,一個表示文件長度,還有一個用於控制如何進行數據拷貝。splice 系統調用可以同步實現,也可以使用異步方式來實現。在使用異步方式的時候,用戶應用程序會通過信號 SIGIO 來獲知數據傳輸已經終止。splice() 系統調用的接口如下所示:

long splice(int fdin, int fdout, size_t len, unsigned int flags);

  調用 splice() 系統調用會導致操作系統內核從數據源 fdin 移動最多 len 個字節的數據到 fdout 中去,這個數據的移動過程只是經過操作系統內核空間,需要最少的拷貝次數。使用 splice() 系統調用需要這兩個文件描述符中的一個必須是用來表示一個管道設備的。不難看出,這種設計具有局限性,Linux 的后續版本針對這一問題將會有所改進。參數 flags 用於表示拷貝操作的執行方法,當前的 flags 有如下這些取值:

  • SPLICE_F_NONBLOCK:splice 操作不會被阻塞。然而,如果文件描述符沒有被設置為不可被阻塞方式的 I/O ,那么調用 splice 有可能仍然被阻塞。
  • SPLICE_F_MORE:告知操作系統內核下一個 splice 系統調用將會有更多的數據傳來。
  • SPLICE_F_MOVE:如果輸出是文件,這個值則會使得操作系統內核嘗試從輸入管道緩沖區直接將數據讀入到輸出地址空間,這個數據傳輸過程沒有任何數據拷貝操作發生。

  Splice() 系統調用利用了 Linux 提出的管道緩沖區( pipe buffer )機制,這就是為什么這個系統調用的兩個文件描述符參數中至少有一個必須要指代管道設備的原因。為了支持 splice 這種機制,Linux 在用於設備和文件系統的 file_operations 結構中增加了下邊這兩個定義:

ssize_t (*splice_write)(struct inode *pipe, strucuct file *out,
                      size_t len, unsigned int flags);
ssize_t (*splice_read)(struct inode *in, strucuct file *pipe,
                      size_t len, unsigned int flags);

  這兩個新的操作可以根據 flags 的設定在 pipe 和 in 或者 out 之間移動 len 個字節。Linux 文件系統已經實現了具有上述功能並且可以使用的操作,而且還實現了一個 generic_splice_sendpage() 函數用於和 socket 之間的接合。

 

對應用程序地址空間和內核之間的數據傳輸進行優化的零拷貝技術

  前面提到的幾種零拷貝技術都是通過盡量避免用戶應用程序和操作系統內核緩沖區之間的數據拷貝來實現的,使用上面那些零拷貝技術的應用程序通常都要局限於某些特殊的情況:要么不能在操作系統內核中處理數據,要么不能在用戶地址空間中處理數據。而這一小節提出的零拷貝技術保留了傳統在用戶應用程序地址空間和操作系統內核地址空間之間傳遞數據的技術,但卻在傳輸上進行優化。我們知道,數據在系統軟件和硬件之間的傳遞可以通過 DMA 傳輸來提高效率,但是對於用戶應用程序和操作系統之間進行數據傳輸這種情況來說,並沒有類似的工具可以使用。本節介紹的技術就是針對這種情況提出來的。

 

利用寫時復制

  在某些情況下,Linux 操作系統內核中的頁緩存可能會被多個應用程序所共享,操作系統有可能會將用戶應用程序地址空間緩沖區中的頁面映射到操作系統內核地址空間中去。如果某個應用程序想要對這共享的數據調用  write() 系統調用,那么它就可能破壞內核緩沖區中的共享數據,傳統的 write() 系統調用並沒有提供任何顯示的加鎖操作,Linux 中引入了寫時復制這樣一種技術用來保護數據。

 

什么是寫時復制

  寫時復制是計算機編程中的一種優化策略,它的基本思想是這樣的:如果有多個應用程序需要同時訪問同一塊數據,那么可以為這些應用程序分配指向這塊數據的指針,在每一個應用程序看來,它們都擁有這塊數據的一份數據拷貝,當其中一個應用程序需要對自己的這份數據拷貝進行修改的時候,就需要將數據真正地拷貝到該應用程序的地址空間中去,也就是說,該應用程序擁有了一份真正的私有數據拷貝,這樣做是為了避免該應用程序對這塊數據做的更改被其他應用程序看到。這個過程對於應用程序來說是透明的,如果應用程序永遠不會對所訪問的這塊數據進行任何更改,那么就永遠不需要將數據拷貝到應用程序自己的地址空間中去。這也是寫時復制的最主要的優點。

  寫時復制的實現需要 MMU 的支持,MMU 需要知曉進程地址空間中哪些特殊的頁面是只讀的,當需要往這些頁面中寫數據的時候,MMU 就會發出一個異常給操作系統內核,操作系統內核就會分配新的物理存儲空間,即將被寫入數據的頁面需要與新的物理存儲位置相對應。

  寫時復制的最大好處就是可以節約內存。不過對於操作系統內核來說,寫時復制增加了其處理過程的復雜性。

 

數據傳輸的實現及其局限性

數據發送端

  對於數據傳輸的發送端來說,實現相對來說是比較簡單的,對與應用程序緩沖區相關的物理頁面進行加鎖,並將這些頁面映射到操作系統內核的地址空間,並標識為“ write only ”。當系統調用返回的時候,用戶應用程序和網絡堆棧就都可以讀取該緩沖區中的數據。在操作系統已經傳送完所有的數據之后,應用程序就可以對這些數據進行寫操作。如果應用程序嘗試在數據傳輸完成之前對數據進行寫操作,那么就會產生異常,這個時候操作系統就會將數據拷貝到應用程序自己的緩沖區中去,並且重置應用程序端的映射。數據傳輸完成之后,對加鎖的頁面進行解鎖操作,並重置 COW 標識。

數據接收端

  對於數據接收端來說,該技術的實現則需要處理復雜得多的情況。如果 read() 系統調用是在數據包到達之前發出的,並且應用程序是被阻塞的,那么 read() 系統調用就會告知操作系統接收到的數據包中的數據應該存放到什么地方去。在這種情況下,根本沒有必要進行頁面重映射,網絡接口卡可以提供足夠的支持讓數據直接存入用戶應用程序的緩沖區中去。如果數據接收是異步的,在 read() 系統調用發出之前,操作系統不知道該把數據寫到哪里,因為它不知道用戶應用程序緩沖區的位置,所以操作系統內核必須要先把數據存放到自己的緩沖區中去。

局限性

  寫時復制技術有可能會導致操作系統的處理開銷很大.所有相關的緩沖區都必須要進行頁對齊處理,並且使用的 MMU 頁面一定要是整數個的。對於發送端來說,這不會造成什么問題。但是對於接收端來說,它需要有能力處理更加復雜的情況。首先,數據包的尺寸大小要合適,大小需要恰到好處能夠覆蓋一整頁的數據,這就限制了那些 MTU 大小大於系統內存頁的網絡,比如 FDDI 和 ATM。其次,為了在沒有任何中斷的情況下將頁面重映射到數據包的流,數據包中的數據部分必須占用整數個頁面。對於異步接收數據的情況來說,為了將數據高效地移動到用戶地址空間中去,可以使用這樣一種方法:利用網絡接口卡的支持,傳來的數據包可以被分割成包頭和數據兩部分,數據被存放在一個單獨的緩沖區內,虛擬存儲系統然后就會將數據映射到用戶地址空間緩沖區去。使用這種方法需要滿足兩個先決條件,也就是上面提到過的:一是應用程序緩沖區必須是頁對齊的,並且在虛擬存儲上是連續的;二是傳來的數據有一頁大小的時候才可以對數據包進行分割。事實上,這兩個先決條件是很難滿足的。如果應用程序緩沖區不是頁對齊的,或者數據包的大小超過一個頁,那么數據就需要被拷貝。對於數據發送端來說,就算數據在傳輸的過程中對於應用程序來說是寫保護的,應用程序仍然需要避免使用這些忙緩沖區,這是因為寫時拷貝操作所帶來的開銷是很大的。如果沒有端到端這一級別的通知,那么應用程序很難會知道某緩沖區是否已經被釋放還是仍然在被占用。

  這種零拷貝技術比較適用於那種寫時復制事件發生比較少的情況,因為寫時復制事件所產生的開銷要遠遠高於一次 CPU 拷貝所產生的開銷。實際情況中,大多數應用程序通常都會多次重復使用相同的緩沖區,所以,一次使用完數據之后,不要從操作系統地址空間解除頁面的映射,這樣會提高效率。考慮到同樣的頁面可能會被再次訪問,所以保留頁面的映射可以節省管理開銷,但是,這種映射保留不會減少由於頁表往返移動和 TLB 沖刷所帶來的開銷,這是因為每次頁面由於寫時復制而進行加鎖或者解鎖的時候,頁面的只讀標志都要被更改。

 

緩沖區共享

  還有另外一種利用預先映射機制的共享緩沖區的方法也可以在應用程序地址空間和操作系統內核之間快速傳輸數據。采用緩沖區共享這種思想的架構最先在 Solaris 上實現,該架構使用了“ fbufs ”這個概念。這種方法需要修改 API。應用程序地址空間和操作系統內核地址空間之間的數據傳遞需要嚴格按照 fbufs 體系結構來實現,操作系統內核之間的通信也是嚴格按照 fbufs 體系結構來完成的。每一個應用程序都有一個緩沖區池,這個緩沖區池被同時映射到用戶地址空間和內核地址空間,也可以在必要的時候才創建它們。通過完成一次虛擬存儲操作來創建緩沖區,fbufs 可以有效地減少由存儲一致性維護所引起的大多數性能問題。該技術在 Linux 中還停留在實驗階段。

為什么要擴展 Linux I/O API

  傳統的 Linux 輸入輸出接口,比如讀和寫系統調用,都是基於拷貝的,也就是說,數據需要在操作系統內核和應用程序定義的緩沖區之間進行拷貝。對於讀系統調用來說,用戶應用程序呈現給操作系統內核一個預先分配好的緩沖區,內核必須把讀進來的數據放到這個緩沖區內。對於寫系統調用來說,只要系統調用返回,用戶應用程序就可以自由重新利用數據緩沖區。

  為了支持上面這種機制,Linux 需要能夠為每一個操作都進行建立和刪除虛擬存儲映射。這種頁面重映射的機制依賴於機器配置、cache 體系結構、TLB 未命中處理所帶來的開銷以及處理器是單處理器還是多處理器等多種因素。如果能夠避免處理 I/O 請求的時候虛擬存儲 / TLB 操作所產生的開銷,則會極大地提高 I/O 的性能。fbufs 就是這樣一種機制。使用 fbufs 體系結構就可以避免虛擬存儲操作。由數據顯示,fbufs 這種結構在 DECStation™ 5000/200 這個單處理器工作站上會取得比上面提到的頁面重映射方法好得多的性能。如果要使用 fbufs 這種體系結構,必須要擴展 Linux API,從而實現一種有效而且全面的零拷貝技術。

快速緩沖區( Fast Buffers )原理介紹

  I/O 數據存放在一些被稱作 fbufs 的緩沖區內,每一個這樣的緩沖區都包含一個或者多個連續的虛擬存儲頁。應用程序訪問 fbuf 是通過保護域來實現的,有如下這兩種方式:

  • 如果應用程序分配了 fbuf,那么應用程序就有訪問該 fbuf 的權限
  • 如果應用程序通過 IPC 接收到了 fbuf,那么應用程序對這個 fbuf 也有訪問的權限

  對於第一種情況來說,這個保護域被稱作是 fbuf 的“ originator ”;對於后一種情況來說,這個保護域被稱作是 fbuf 的“ receiver ”。

  傳統的 Linux I/O 接口支持數據在應用程序地址空間和操作系統內核之間交換,這種交換操作導致所有的數據都需要進行拷貝。如果采用 fbufs 這種方法,需要交換的是包含數據的緩沖區,這樣就消除了多余的拷貝操作。應用程序將 fbuf 傳遞給操作系統內核,這樣就能減少傳統的 write 系統調用所產生的數據拷貝開銷。同樣的,應用程序通過 fbuf 來接收數據,這樣也可以減少傳統 read 系統調用所產生的數據拷貝開銷。如下圖所示:

                             

                      圖 6. Linux I/O API

 

 I/O 子系統或者應用程序都可以通過 fbufs 管理器來分配 fbufs。一旦分配了 fbufs,這些 fbufs 就可以從程序傳遞到 I/O 子系統,或者從 I/O 子系統傳遞到程序。使用完后,這些 fbufs 會被釋放回 fbufs 緩沖區池。

 fbufs 在實現上有如下這些特性,如圖 9 所示:

  • fbuf 需要從 fbufs 緩沖區池里分配。每一個 fbuf 都存在一個所屬對象,要么是應用程序,要么是操作系統內核。fbuf 可以在應用程序和操作系統之間進行傳遞,fbuf 使用完之后需要被釋放回特定的 fbufs 緩沖區池,在 fbuf 傳遞的過程中它們需要攜帶關於 fbufs 緩沖區池的相關信息。
  • 每一個 fbufs 緩沖區池都會和一個應用程序相關聯,一個應用程序最多只能與一個 fbufs 緩沖區池相關聯。應用程序只有資格訪問它自己的緩沖區池。
  • fbufs 不需要虛擬地址重映射,這是因為對於每個應用程序來說,它們可以重新使用相同的緩沖區集合。這樣,虛擬存儲轉換的信息就可以被緩存起來,虛擬存儲子系統方面的開銷就可以消除。
  • I/O 子系統(設備驅動程序,文件系統等)可以分配 fbufs,並將到達的數據直接放到這些 fbuf 里邊。這樣,緩沖區之間的拷貝操作就可以避免。

 

                       

                   圖 7. fbufs 體系結構 

 

  前面提到,這種方法需要修改 API,如果要使用 fbufs 體系結構,應用程序和 Linux 操作系統內核驅動程序都需要使用新的 API,如果應用程序要發送數據,那么它就要從緩沖區池里獲取一個 fbuf,將數據填充進去,然后通過文件描述符將數據發送出去。接收到的 fbufs 可以被應用程序保留一段時間,之后,應用程序可以使用它繼續發送其他的數據,或者還給緩沖區池。但是,在某些情況下,需要對數據包內的數據進行重新組裝,那么通過 fbuf 接收到數據的應用程序就需要將數據拷貝到另外一個緩沖區內。再者,應用程序不能對當前正在被內核處理的數據進行修改,基於這一點,fbufs 體系結構引入了強制鎖的概念以保證其實現。對於應用程序來說,如果 fbufs 已經被發送給操作系統內核,那么應用程序就不會再處理這些 fbufs。

fbufs 存在的一些問題

  管理共享緩沖區池需要應用程序、網絡軟件、以及設備驅動程序之間的緊密合作。對於數據接收端來說,網絡硬件必須要能夠將到達的數據包利用 DMA 傳輸到由接收端分配的正確的存儲緩沖區池中去。而且,應用程序稍微不注意就會更改之前發到共享存儲中的數據的內容,從而導致數據被破壞,但是這種問題在應用程序端是很難調試的。同時,共享存儲這種模型很難與其他類型的存儲對象關聯使用,但是應用程序、網絡軟件以及設備驅動程序之間的緊密合作是需要其他存儲管理器的支持的。對於共享緩沖區這種技術來說,雖然這種技術看起來前景光明,但是這種技術不但需要對 API 進行更改,而且需要對驅動程序也進行更改,並且這種技術本身也存在一些未解決的問題,這就使得這種技術目前還只是出於試驗階段。在測試系統中,這種技術在性能上有很大的改進,不過這種新的架構的整體安裝目前看起來還是不可行的。這種預先分配共享緩沖區的機制有時也因為粒度問題需要將數據拷貝到另外一個緩沖區中去。

 

總結

  本系列文章介紹了 Linux 中的零拷貝技術,本文是其中的第二部分。本文對第一部分文章中提出的 Linux 操作系統上出現的幾種零拷貝技術進行了更詳細的介紹,主要描述了它們各自的優點,缺點以及適用場景。對於網絡數據傳輸來說,零拷貝技術的應用受到了很多體系結構方面因素的阻礙,包括虛擬存儲體系結構以及網絡協議體系結構等。所以,零拷貝技術仍然只是在某些很特殊的情況中才可以應用,比如文件服務或者使用某種特殊的協議進行高帶寬的通信等。但是,零拷貝技術在磁盤操作中的應用的可行性就高得多了,這很可能是因為磁盤操作具有同步的特點,以及數據傳輸單元是按照頁的粒度來進行的。

  針對 Linux 操作系統平台提出並實現了很多種零拷貝技術,但是並不是所有這些零拷貝技術都被廣泛應用於現實中的操作系統中的。比如,fbufs 體系結構,它在很多方面看起來都很吸引人,但是使用它需要更改 API 以及驅動程序,它還存在其他一些實現上的困難,這就使得 fbufs 還只是停留在實驗的階段。動態地址重映射技術只是需要對操作系統做少量修改,雖然不需要修改用戶軟件,但是當前的虛擬存儲體系結構並不能很好地支持頻繁的虛擬地址重映射操作。而且為了保證存儲的一致性,重映射之后還必須對 TLB 和一級緩存進行刷新。事實上,利用地址重映射實現的零拷貝技術適用的范圍是很小的,這是因為虛擬存儲操作所帶來的開銷往往要比 CPU 拷貝所產生的開銷還要大。此外,為了完全消除 CPU 訪問存儲,通常都需要額外的硬件來支持,而這種硬件的支持並不是很普及,同時也是非常昂貴的。

  本系列文章的目的是想幫助讀者理清這些出現在 Linux 操作系統中的零拷貝技術都是從何種角度來幫助改善數據傳輸過程中遇到的性能問題的。關於各種零拷貝技術的具體實現細節,本系列文章沒有做詳細描述。同時,零拷貝技術一直是在不斷地發展和完善當中的,本系列文章並沒有涵蓋 Linux 上出現的所有零拷貝技術。

 


免責聲明!

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



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