linux中零拷貝詳細描述
文章屬於轉載文章
這篇文章需要用到頁快取和記憶體對映的知識,所以建議先看之前的幾篇文章。
引言
傳統的 Linux 作業系統的標準 I/O 介面是基於資料拷貝操作的,即 I/O 操作會導致資料在作業系統核心地址空間的緩衝區和應用程式地址空間定義的緩衝區之間進行傳輸。這樣做最大的好處是可以減少磁碟 I/O 的操作,因為如果所請求的資料已經存放在作業系統的高速緩衝儲存器中,那麼就不需要再進行實際的物理磁碟 I/O 操作。但是資料傳輸過程中的資料拷貝操作卻導致了極大的 CPU 開銷,限制了作業系統有效進行資料傳輸操作的能力。
零拷貝( zero-copy )這種技術可以有效地改善資料傳輸的效能,在核心驅動程式(比如網路堆疊或者磁碟儲存驅動程式)處理 I/O 資料的時候,零拷貝技術可以在某種程度上減少甚至完全避免不必要 CPU 資料拷貝操作。現代的 CPU 和儲存體系結構提供了很多特徵可以有效地實現零拷貝技術,但是因為儲存體系結構非常複雜,而且網路協議棧有時需要對資料進行必要的處理,所以零拷貝技術有可能會產生很多負面的影響,甚至會導致零拷貝技術自身的優點完全喪失。
為什麼需要零拷貝技術
如今,很多網路伺服器都是基於客戶端 - 伺服器這一模型的。在這種模型中,客戶端向伺服器端請求資料或者服務;伺服器端則需要響應客戶端發出的請求,併為客戶端提供它所需要的資料。隨著網路服務的逐漸普及,video 這類應用程式發展迅速。當今的計算機系統已經具備足夠的能力去處理 video 這類應用程式對客戶端所造成的重負荷,但是對於伺服器端來說,它應付由 video 這類應用程式引起的網路通訊量就顯得捉襟見肘了。而且,客戶端的數量增長迅速,那麼伺服器端就更容易成為效能瓶頸。而對於負荷很重的伺服器來說,作業系統通常都是引起效能瓶頸的罪魁禍首。舉個例子來說,當資料“寫”操作或者資料“傳送”操作的系統呼叫發出時,作業系統通常都會將資料從應用程式地址空間的緩衝區拷貝到作業系統核心的緩衝區中去。作業系統這樣做的好處是介面簡單,但是卻在很大程度上損失了系統性能,因為這種資料拷貝操作不單需要佔用 CPU 時間片,同時也需要佔用額外的記憶體頻寬。
一般來說,客戶端通過網路介面卡向伺服器端傳送請求,作業系統將這些客戶端的請求傳遞給伺服器端應用程式,伺服器端應用程式會處理這些請求,請求處理完成以後,作業系統還需要將處理得到的結果通過網路介面卡傳遞回去。
下邊這一小節會跟讀者簡單介紹一下傳統的伺服器是如何進行資料傳輸的,以及這種資料傳輸的處理過程存在哪些問題有可能會造成伺服器的效能損失。
Linux 中傳統伺服器進行資料傳輸的流程
Linux 中傳統的 I/O 操作是一種緩衝 I/O,I/O 過程中產生的資料傳輸通常需要在緩衝區中進行多次的拷貝操作。一般來說,在傳輸資料的時候,使用者應用程式需要分配一塊大小合適的緩衝區用來存放需要傳輸的資料。應用程式從檔案中讀取一塊資料,然後把這塊資料通過網路傳送到接收端去。使用者應用程式只是需要呼叫兩個系統呼叫 read() 和 write() 就可以完成這個資料傳輸操作,應用程式並不知曉在這個資料傳輸的過程中作業系統所做的資料拷貝操作。對於 Linux 作業系統來說,基於資料排序或者校驗等各方面因素的考慮,作業系統核心會在處理資料傳輸的過程中進行多次拷貝操作。在某些情況下,這些資料拷貝操作會極大地降低資料傳輸的效能。
當應用程式需要訪問某塊資料的時候,作業系統核心會先檢查這塊資料是不是因為前一次對相同檔案的訪問而已經被存放在作業系統核心地址空間的緩衝區內,如果在核心緩衝區中找不到這塊資料,Linux 作業系統核心會先將這塊資料從磁碟讀出來放到作業系統核心的緩衝區裡去。如果這個資料讀取操作是由 DMA 完成的,那麼在 DMA 進行資料讀取的這一過程中,CPU 只是需要進行緩衝區管理,以及建立和處理 DMA ,除此之外,CPU 不需要再做更多的事情,DMA 執行完資料讀取操作之後,會通知作業系統做進一步的處理。Linux 作業系統會根據 read() 系統呼叫指定的應用程式地址空間的地址,把這塊資料存放到請求這塊資料的應用程式的地址空間中去,在接下來的處理過程中,作業系統需要將資料再一次從使用者應用程式地址空間的緩衝區拷貝到與網路堆疊相關的核心緩衝區中去,這個過程也是需要佔用 CPU 的。資料拷貝操作結束以後,資料會被打包,然後傳送到網路介面卡上去。在資料傳輸的過程中,應用程式可以先返回進而執行其他的操作。之後,在呼叫 write() 系統呼叫的時候,使用者應用程式緩衝區中的資料內容可以被安全的丟棄或者更改,因為作業系統已經在核心緩衝區中保留了一份資料拷貝,當資料被成功傳送到硬體上之後,這份資料拷貝就可以被丟棄。
從上面的描述可以看出,在這種傳統的資料傳輸過程中,資料至少發生了四次拷貝操作,即便是使用了 DMA 來進行與硬體的通訊,CPU 仍然需要訪問資料兩次。在 read() 讀資料的過程中,資料並不是直接來自於硬碟,而是必須先經過作業系統的檔案系統層。在 write() 寫資料的過程中,為了和要傳輸的資料包的大小相吻合,資料必須要先被分割成塊,而且還要預先考慮包頭,並且要進行資料校驗和操作。
圖 1. 傳統使用 read 和 write 系統呼叫的資料傳輸
零拷貝(zero copy)技術概述
什麼是零拷貝?
簡單一點來說,零拷貝就是一種避免 CPU 將資料從一塊儲存拷貝到另外一塊儲存的技術。針對作業系統中的裝置驅動程式、檔案系統以及網路協議堆疊而出現的各種零拷貝技術極大地提升了特定應用程式的效能,並且使得這些應用程式可以更加有效地利用系統資源。這種效能的提升就是通過在資料拷貝進行的同時,允許 CPU 執行其他的任務來實現的。零拷貝技術可以減少資料拷貝和共享匯流排操作的次數,消除傳輸資料在儲存器之間不必要的中間拷貝次數,從而有效地提高資料傳輸效率。而且,零拷貝技術減少了使用者應用程式地址空間和作業系統核心地址空間之間因為上下文切換而帶來的開銷。進行大量的資料拷貝操作其實是一件簡單的任務,從作業系統的角度來說,如果 CPU 一直被佔用著去執行這項簡單的任務,那麼這將會是很浪費資源的;如果有其他比較簡單的系統部件可以代勞這件事情,從而使得 CPU 解脫出來可以做別的事情,那麼系統資源的利用則會更加有效。綜上所述,零拷貝技術的目標可以概括如下:
避免資料拷貝
- 避免作業系統核心緩衝區之間進行資料拷貝操作。
- 避免作業系統核心和使用者應用程式地址空間這兩者之間進行資料拷貝操作。
- 使用者應用程式可以避開作業系統直接訪問硬體儲存。
- 資料傳輸儘量讓 DMA 來做。
將多種操作結合在一起
- 避免不必要的系統呼叫和上下文切換。
- 需要拷貝的資料可以先被快取起來。
- 對資料進行處理儘量讓硬體來做。
前文提到過,對於高速網路來說,零拷貝技術是非常重要的。這是因為高速網路的網路連結能力與 CPU 的處理能力接近,甚至會超過 CPU 的處理能力。如果是這樣的話,那麼 CPU 就有可能需要花費幾乎所有的時間去拷貝要傳輸的資料,而沒有能力再去做別的事情,這就產生了效能瓶頸,限制了通訊速率,從而降低了網路連結的能力。一般來說,一個 CPU 時鐘週期可以處理一位的資料。舉例來說,一個 1 GHz 的處理器可以對 1Gbit/s 的網路連結進行傳統的資料拷貝操作,但是如果是 10 Gbit/s 的網路,那麼對於相同的處理器來說,零拷貝技術就變得非常重要了。對於超過 1 Gbit/s 的網路連結來說,零拷貝技術在超級計算機叢集以及大型的商業資料中心中都有所應用。然而,隨著資訊科技的發展,1 Gbit/s,10 Gbit/s 以及 100 Gbit/s 的網路會越來越普及,那麼零拷貝技術也會變得越來越普及,這是因為網路連結的處理能力比 CPU 的處理能力的增長要快得多。傳統的資料拷貝受限於傳統的作業系統或者通訊協議,這就限制了資料傳輸效能。零拷貝技術通過減少資料拷貝次數,簡化協議處理的層次,在應用程式和網路之間提供更快的資料傳輸方法,從而可以有效地降低通訊延遲,提高網路吞吐率。零拷貝技術是實現主機或者路由器等裝置高速網路介面的主要技術之一。
現代的 CPU 和儲存體系結構提供了很多相關的功能來減少或避免 I/O 操作過程中產生的不必要的 CPU 資料拷貝操作,但是,CPU 和儲存體系結構的這種優勢經常被過高估計。儲存體系結構的複雜性以及網路協議中必需的資料傳輸可能會產生問題,有時甚至會導致零拷貝這種技術的優點完全喪失。在下一章中,我們會介紹幾種 Linux 作業系統中出現的零拷貝技術,簡單描述一下它們的實現方法,並對它們的弱點進行分析。
零拷貝技術分類
零拷貝技術的發展很多樣化,現有的零拷貝技術種類也非常多,而當前並沒有一個適合於所有場景的零拷貝技術的出現。對於 Linux 來說,現存的零拷貝技術也比較多,這些零拷貝技術大部分存在於不同的 Linux 核心版本,有些舊的技術在不同的 Linux 核心版本間得到了很大的發展或者已經漸漸被新的技術所代替。本文針對這些零拷貝技術所適用的不同場景對它們進行了劃分。概括起來,Linux 中的零拷貝技術主要有下面這幾種:
- 直接 I/O:對於這種資料傳輸方式來說,應用程式可以直接訪問硬體儲存,作業系統核心只是輔助資料傳輸:這類零拷貝技術針對的是作業系統核心並不需要對資料進行直接處理的情況,資料可以在應用程式地址空間的緩衝區和磁碟之間直接進行傳輸,完全不需要 Linux 作業系統核心提供的頁快取的支援。
- 在資料傳輸的過程中,避免資料在作業系統核心地址空間的緩衝區和使用者應用程式地址空間的緩衝區之間進行拷貝。有的時候,應用程式在資料進行傳輸的過程中不需要對資料進行訪問,那麼,將資料從 Linux 的頁快取拷貝到使用者程序的緩衝區中就可以完全避免,傳輸的資料在頁快取中就可以得到處理。在某些特殊的情況下,這種零拷貝技術可以獲得較好的效能。Linux 中提供類似的系統呼叫主要有 mmap(),sendfile() 以及 splice()。
- 對資料在 Linux 的頁快取和使用者程序的緩衝區之間的傳輸過程進行優化。該零拷貝技術側重於靈活地處理資料在使用者程序的緩衝區和作業系統的頁快取之間的拷貝操作。這種方法延續了傳統的通訊方式,但是更加靈活。在 Linux 中,該方法主要利用了寫時複製技術。
前兩類方法的目的主要是為了避免應用程式地址空間和作業系統核心地址空間這兩者之間的緩衝區拷貝操作。這兩類零拷貝技術通常適用在某些特殊的情況下,比如要傳送的資料不需要經過作業系統核心的處理或者不需要經過應用程式的處理。第三類方法則繼承了傳統的應用程式地址空間和作業系統核心地址空間之間資料傳輸的概念,進而針對資料傳輸本身進行優化。我們知道,硬體和軟體之間的資料傳輸可以通過使用 DMA 來進行,DMA 進行資料傳輸的過程中幾乎不需要 CPU 參與,這樣就可以把 CPU 解放出來去做更多其他的事情,但是當資料需要在使用者地址空間的緩衝區和 Linux 作業系統核心的頁快取之間進行傳輸的時候,並沒有類似 DMA 這種工具可以使用,CPU 需要全程參與到這種資料拷貝操作中,所以這第三類方法的目的是可以有效地改善資料在使用者地址空間和作業系統核心地址空間之間傳遞的效率。
總結
本系列文章介紹了 Linux 中的零拷貝技術,本文是其中的第一部分,介紹了零拷貝技術的基本概念,Linux 為什麼需要零拷貝這種技術以及簡要概述了 Linux 中都存在哪些零拷貝技術這樣一些基本背景知識。我們將在本系列文章的第二部分內容中詳細介紹本文提到的 Linux 中的幾種零拷貝技術。
第一部分主要介紹了一些零拷貝技術的相關背景知識,簡要概述了 Linux 為什麼需要零拷貝技術以及 Linux 中都有哪幾種零拷貝技術。本文是本系列文章的第二部分,針對第一部分內容中提到的幾種零拷貝技術分別進行更詳細的介紹,並對這些零拷貝技術的優缺點進行分析。
Linux 中的直接 I/O
如果應用程式可以直接訪問網路介面儲存,那麼在應用程式訪問資料之前儲存匯流排就不需要被遍歷,資料傳輸所引起的開銷將會是最小的。應用程式或者執行在使用者模式下的庫函式可以直接訪問硬體裝置的儲存,作業系統核心除了進行必要的虛擬儲存配置工作之外,不參與資料傳輸過程中的其它任何事情。直接 I/O 使得資料可以直接在應用程式和外圍裝置之間進行傳輸,完全不需要作業系統核心頁快取的支援。關於直接 I/O 技術的具體實現細節可以參看 developerWorks 上的另一篇文章”Linux 中直接 I/O 機制的介紹” ,本文不做過多描述。
圖 1. 使用直接 I/O 的資料傳輸
針對資料傳輸不需要經過應用程式地址空間的零拷貝技術
利用 mmap()
在 Linux 中,減少拷貝次數的一種方法是呼叫 mmap() 來代替呼叫 read,比如:
tmp_buf = mmap(file, len); write(socket, tmp_buf, len);
首先,應用程式呼叫了 mmap() 之後,資料會先通過 DMA 拷貝到作業系統核心的緩衝區中去。接著,應用程式跟作業系統共享這個緩衝區,這樣,作業系統核心和應用程式儲存空間就不需要再進行任何的資料拷貝操作。應用程式呼叫了 write() 之後,作業系統核心將資料從原來的核心緩衝區中拷貝到與 socket 相關的核心緩衝區中。接下來,資料從核心 socket 緩衝區拷貝到協議引擎中去,這是第三次資料拷貝操作。
圖 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 () 一樣。
圖 3. 利用 sendfile () 進行資料傳輸
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 引擎會將資料直接從核心緩衝區拷貝到協議引擎中去,這樣就避免了最後一次資料拷貝。
圖 4. 帶有 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 系統呼叫所產生的資料拷貝開銷。如下圖所示:
圖 5. 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 裡邊。這樣,緩衝區之間的拷貝操作就可以避免。
圖 6. 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 上出現的所有零拷貝技術。