如果應用程式可以直接存取網路介面儲存,那麼在應用程式訪問資料之前儲存匯流排就不需要被遍曆,資料轉送所引起的開銷將會是最小的。應用程式或 者運行在使用者模式下的庫函數可以直接存取硬體裝置的儲存,作業系統核心除了進行必要的虛擬儲存配置工作之外,不參與資料轉送過程中的其它任何事情。直接 I/O 使得資料可以直接在應用程式和外圍裝置之間進行傳輸,完全不需要作業系統核心頁緩衝的支援。關於直接 I/O 技術的具體實現細節可以參看 developerWorks 上的另一篇文章”Linux 中直接 I/O 機制的介紹” ,本文不做過多描述。
1. 使用直接 I/O 的資料轉送
圖 1. 使用直接 I/O 的資料轉送
針對資料轉送不需要經過應用程式地址空間的零拷貝技術
利用mmap()
在 Linux 中,減少拷貝次數的一種方法是調用 mmap() 來代替調用 read,比如:
tmp_buf = mmap(file, len); write(socket, tmp_buf, len); |
首先,應用程式調用了 mmap() 之後,資料會先通過 DMA 拷貝到作業系統核心的緩衝區中去。接著,應用程式跟作業系統共用這個緩衝區,這樣,作業系統核心和應用程式儲存空間就不需要再進行任何的資料拷貝操作。應 用程式調用了 write() 之後,作業系統核心將資料從原來的核心緩衝區中拷貝到與 socket 相關的核心緩衝區中。接下來,資料從核心 socket 緩衝區拷貝到協議引擎中去,這是第三次資料拷貝操作。
利用mmap()代替read()
圖 2. 利用 mmap() 代替 read()
通過使用 mmap() 來代替 read(), 已經可以減半作業系統需要進行資料拷貝的次數。當大量資料需要傳輸的時候,這樣做就會有一個比較好的效率。但是,這種改進也是需要代價的,使用 mma()p 其實是存在潛在的問題的。當對檔案進行了記憶體映射,然後調用 write() 系統調用,如果此時其他的進程截斷了這個檔案,那麼 write() 系統調用將會被匯流排錯誤訊號 SIGBUS 中斷,因為此時正在執行的是一個錯誤的儲存訪問。這個訊號將會導致進程被殺死,解決這個問題可以通過以下這兩種方法:
- 為 SIGBUS 安裝一個新的訊號處理器,這樣,write() 系統調用在它被中斷之前就返回已經寫入的位元組數目,errno 會被設定成 success。但是這種方法也有其缺點,它不能反映出產生這個問題的根源所在,因為 BIGBUS 訊號只是顯示某進程發生了一些很嚴重的錯誤。
- 第二種方法是通過檔案租借鎖來解決這個問題的,這種方法相對來說更好一些。我們可以通過核心對檔案加讀或者寫的租借鎖,當另外一個進程嘗試對使用者進行中 傳輸的檔案進行截斷的時候,核心會發送給使用者一個即時訊號:RT_SIGNAL_LEASE 訊號,這個訊號會告訴使用者核心破壞了使用者加在那個檔案上的寫或者讀租借鎖,那麼 write() 系統調用則會被中斷,並且進程會被 SIGBUS 訊號殺死,傳回值則是中斷前寫的位元組數,errno 也會被設定為 success。檔案租借鎖需要在對檔案進行記憶體映射之前設定。
使用 mmap 是 POSIX 相容的,但是使用 mmap 並不一定能獲得理想的資料轉送效能。資料轉送的過程中仍然需要一次 CPU 拷貝操作,而且映射操作也是一個開銷很大的虛擬儲存操作,這種操作需要通過更改頁表以及沖刷 TLB (使得 TLB 的內容無效)來維持儲存的一致性。但是,因為映射通常適用於較大範圍,所以對於相同長度的資料來說,映射所帶來的開銷遠遠低於 CPU 拷貝所帶來的開銷。
sendfile()
為了簡化使用者介面,同時還要繼續保留 mmap()/write() 技術的優點:減少 CPU 的拷貝次數,Linux 在版本 2.1 中引入了 sendfile() 這個系統調用。
sendfile() 不僅減少了資料拷貝操作,它也減少了環境切換。首先:sendfile() 系統調用利用 DMA 引擎將檔案中的資料拷貝到作業系統核心緩衝區中,然後資料被拷貝到與 socket 相關的核心緩衝區中去。接下來,DMA 引擎將資料從核心 socket 緩衝區中拷貝到協議引擎中去。如果在使用者調用 sendfile () 系統調用進行資料轉送的過程中有其他進程截斷了該檔案,那麼 sendfile () 系統調用會簡單地返回給使用者應用程式中斷前所傳輸的位元組數,errno 會被設定為 success。如果在調用
sendfile() 之前作業系統對檔案加上了租借鎖,那麼 sendfile() 的操作和返回狀態將會和 mmap()/write () 一樣。
圖:利用sendfile()進行資料轉送
3. 利用 sendfile () 進行資料轉送
sendfile() 系統調用不需要將資料拷貝或者映射到應用程式地址空間中去,所以 sendfile() 只是適用於應用程式地址空間不需要對所訪問資料進行處理的情況。相對於 mmap() 方法來說,因為 sendfile 傳輸的資料沒有越過使用者應用程式 / 作業系統核心的邊界線,所以 sendfile () 也極大地減少了儲存管理的開銷。但是,sendfile () 也有很多局限性,如下所列:
- sendfile() 局限於基於檔案服務的網路應用程式,比如 網頁伺服器。據說,在 Linux 核心中實現 sendfile() 只是為了在其他平台上使用 sendfile() 的 Apache 程式
- 由於網路傳輸具有非同步性,很難在 sendfile () 系統調用的接收端進行配對的實現方式,所以資料轉送的接收端一般沒有用到這種技術。
- 基於效能的考慮來說,sendfile () 仍然需要有一次從檔案到 socket 緩衝區的 CPU 拷貝操作,這就導致頁緩衝有可能會被傳輸的資料所汙染。
上小節介紹的 sendfile() 技術在進行資料轉送仍然還需要一次多餘的資料拷貝操作,通過引入一點硬體上的協助,這僅有的一次資料拷貝操作也可以避免。為了避免作業系統核心造成的資料 副本,需要用到一個支援收集操作的網路介面,這也就是說,待傳輸的資料可以分散在儲存的不同位置上,而不需要在連續儲存中存放。這樣一來,從檔案中讀出的 資料就根本不需要被拷貝到 socket 緩衝區中去,而只是需要將緩衝區描述符傳到網路通訊協定棧中去,之後其在緩衝區中建立起資料包的相關結構,然後通過 DMA 收集拷貝功能將所有的資料結合成一個網路資料包。網卡的
DMA 引擎會在一次操作中從多個位置讀取包頭和資料。Linux 2.4 版本中的 socket 緩衝區就可以滿足這種條件,這也就是用於 Linux 中的眾所周知的零拷貝技術,這種方法不但減少了因為多次環境切換所帶來開銷,同時也減少了處理器造成的資料副本的個數。對於使用者應用程式來說,代碼沒有 任何改變。首先,sendfile() 系統調用利用 DMA 引擎將檔案內容拷貝到核心緩衝區去;然後,將帶有檔案位置和長度資訊的緩衝區描述符添加到 socket 緩衝區中去,此過程不需要將資料從作業系統核心緩衝區拷貝到
socket 緩衝區中,DMA 引擎會將資料直接從核心緩衝區拷貝到協議引擎中去,這樣就避免了最後一次資料拷貝。
圖:帶有DMA收集拷貝功能的sendfile()MA收集拷貝功能的sendfi
帶有 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 中引入了寫時複製這樣一種技術用來保護資料的
還有另外一種利用預先映射機制的共用緩衝區的方法也可以在應用程式地址空間和作業系統核心之間快速傳輸資料。採用緩衝區共用這種思想的架構最 先在 Solaris 上實現,該架構使用了“ fbufs ”這個概念。這種方法需要修改 API。應用程式地址空間和作業系統核心地址空間之間的資料傳遞需要嚴格按照 fbufs 體繫結構來實現,作業系統核心之間的通訊也是嚴格按照 fbufs 體繫結構來完成的。每一個應用程式都有一個緩衝區池,這個緩衝區池被同時映射到使用者地址空間和核心地址空間,也可以在必要的時候才建立它們。通過完成一次
虛擬儲存操作來建立緩衝區,fbufs 可以有效地減少由儲存一致性維護所引起的大多數效能問題。該技術在 Linux 中還停留在實驗階段。5Linux 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 裡邊。這樣,緩衝區之間的拷貝操作就可以避免。
圖 6. fbufs 體繫結構
前面提到,這種方法需要修改 API,如果要使用 fbufs 體繫結構,應用程式和 Linux 作業系統核心驅動程式都需要使用新的 API,如果應用程式要發送資料,那麼它就要從緩衝區池裡擷取一個 fbuf,將資料填充進去,然後通過檔案描述符將資料發送出去。接收到的 fbufs 可以被應用程式保留一段時間,之後,應用程式可以使用它繼續發送其他的資料,或者還給緩衝區池。但是,在某些情況下,需要對資料包內的資料進行重新組裝, 那麼通過 fbuf 接收到資料的應用程式就需要將資料拷貝到另外一個緩衝區內。再者,應用程式不能對當前正在被核心處理的資料進行修改,基於這一點,fbufs
體繫結構引入了強制鎖的概念以保證其實現。對於應用程式來說,如果 fbufs 已經被發送給作業系統核心,那麼應用程式就不會再處理這些 fbufs。
本系列文章介紹了 Linux 中的零拷貝技術,本文是其中的第二部分。本文對第一部分文章中提出的 Linux 作業系統上出現的幾種零拷貝技術進行了更詳細的介紹,主要描述了它們各自的優點,缺點以及適用情境。對於網路資料轉送來說,零拷貝技術的應用受到了很多體 繫結構方面因素的阻礙,包括虛擬儲存體繫結構以及網路通訊協定體繫結構等。所以,零拷貝技術仍然只是在某些很特殊的情況中才可以應用,比如檔案服務或者使用某 種特殊的協議進行高頻寬的通訊等。但是,零拷貝技術在磁碟操作中的應用的可行性就高得多了,這很可能是因為磁碟操作具有同步的特點,以及數據傳輸單元是按
照頁的粒度來進行的。
針對 Linux 作業系統平台提出並實現了很多種零拷貝技術,但是並不是所有這些零拷貝技術都被廣泛應用於現實中的作業系統中的。比如,fbufs 體繫結構,它在很多方面看起來都很吸引人,但是使用它需要更改 API 以及驅動程式,它還存在其他一些實現上的困難,這就使得 fbufs 還只是停留在實驗的階段。動態地址重新對應技術只是需要對作業系統做少量修改,雖然不需要修改使用者軟體,但是當前的虛擬儲存體繫結構並不能很好地支援頻繁的 虛擬位址重新對應操作。而且為了保證儲存的一致性,重新對應之後還必須對 TLB 和一級緩衝進行重新整理。事實上,利用地址重新對應實現的零拷貝技術適用的範圍是很小的,這是因為虛擬儲存操作所帶來的開銷往往要比
CPU 拷貝所產生的開銷還要大。此外,為了完全消除 CPU 訪問儲存,通常都需要額外的硬體來支援,而這種硬體的支援並不是很普及,同時也是非常昂貴的。
本系列文章的目的是想協助讀者理清這些出現在 Linux 作業系統中的零拷貝技術都是從何種角度來協助改善資料轉送過程中遇到的效能問題的。關於各種零拷貝技術的具體實現細節,本系列文章沒有做詳細描述。同時, 零拷貝技術一直是在不斷地發展和完善當中的,本系列文章並沒有涵蓋 Linux 上出現的所有零拷貝技術。