基礎知識-零拷貝技術

高級I/O函數

主要介紹一下Linux下的一些高級I/O函數,可以用於服務器的效率優化,這裡介紹的API都是“零拷貝技術”的具體實現,可以直接直接跳到下一部分的對零拷貝技術的淺析

readv函數和writev函數

readv將數據從文件描述符讀取到分散的內存塊中,即分散讀;writev函數將多塊分散的內存數據一併寫入文件描述符,即集中寫。

#include ssize_t readv(int fd, const struct iovec* vector, int count);ssize_t writev(int fd, const struct iovec* vector, int count);

其中fd時被操作的目標文件描述符,vector是iovec結構數組(一個iovec描述一塊內存區),count是vector數組的長度,即有多少塊內存數據需要從fd讀出or寫到fd。

使用舉例:

Web服務器中,當期解析完一個HTTP請求後,如果目標文檔存在且客戶具有讀取該文檔的權限,那麼服務器需要返回一個HTTP應答來傳輸文檔。HTTP應答包含1個狀態行、多個頭部字段、1個空行和文檔的內容。前3部分可能被Web服務器放置在一塊內存中,而文檔內容通常被讀入到另一塊單獨的內存中,通過writev函數就不需要把兩部分內容拼接到一起再發送,而是直接同時寫出。

sendfile函數

用於在兩個文件描述符之間直接傳遞數據(完全在內核中操作),從而避免了內核緩衝區和用戶緩衝區之間的數據拷貝,效率很高,也被稱為

零拷貝,即從實際的文件中將內容直接拷貝到socket。

#include ssize_t sendfile(int out_fd, int in_fd, off_t * offset, size_t count);

其中in_fd時待讀出內容的文件描述符,out_fd是待寫入內容的文件描述符,offset指定從讀入文件流的哪個位置開始讀(為空則使用默認的位置),count指定在兩個文件描述符之間傳輸的字節數。

需要注意,in_fd必須是一個類似於mmap函數的文件描述符,即必須指向真實的文件,而不能是socket和管道,而out_fd則必須是一個socket,因此sendfile幾乎是專門為在網絡上傳輸文件而設計的。

使用sendfile就不需要為目標文件分配任何用戶空間的緩存,也沒有執行讀取文件的操作,但是同樣實現了文件的發送,效率顯然高得多。

mmap函數和munmap函數

前者用於申請一段內存空間,可以作為進程間通信的共享內存,也可以將文件映射在其中。後者則為對應的釋放空間的操作

#include void * mmap(void * start, size_t length, int prot, int flags, int fd, off_t offset);int munmap(void * start, size_t length);

start參數允許用戶使用特定的地址作為這段內存的起始地址,如果被設置成NULL,則自動分配;

length指定內存段的長度;

prot用於設置內存段的訪問權限,由後面的幾個值按位或得出:

  • PROT_READ,內存段可讀

  • PROT_WRITE,內存段可寫

  • PROT_EXEC,內存段可執行

  • PROT_NONE,內存段不能被訪問

flags參數控制內存段內容被修改後程序的行為,由以下幾個值按位或得出:

  • MAP_SHARED

    在進程間共享這段內存,對該內存端的修改將反映到被映射的文件中

  • MAP_PRIVATE

    內存段為調用進程所私有。對其的修改不會反映到被映射的文件中。與上個選項互斥

  • MAP_ANONYMOUS

    這段內存不是從文件映射而來,內容全部初始化為0

  • MAP_FIXED

    內存段必須位於start參數指定的地址處,start必須是內存頁面大小(4096字節)的整數倍

  • MAP_HUGETLB

    按照“大內存頁面”來分配內存空間,“大內存頁面”可以通過/proc/meminfo文件查看

fd參數是被映射文件對應的文件描述符;

offset參數設置從文件的何處開始映射;

splice函數

用於在兩個文件描述符之間移動數據,也是零拷貝操作

#include ssize_t splice(int fd_in, loff_t * off_in, int fd_out, loff_t * off_out, size_t len, unsigned int flags);

fd_in是待輸入數據的文件描述符,但是如果是管道的話那麼off_in則必須為NULL(因為off_in表示從輸入數據流的何處開始讀取數據,此時為NULL,表示從當前偏移位置讀入)。fd_out和off_out的含義大致與前面相同;

len參數指定移動數據的長度;

flags參數控制數據如何移動,從下面的值按位或得到:

  • SPLICE_F_MOVE(存在BUG)

    如果合適的話,按整夜內存移動數據

  • SPLICE_F_NONBLOCK

    非阻塞的splice操作,實際效果還是會受文件描述符本身的阻塞狀態的影響

  • SPLICE_F_MORE

    給內核的一個提示,後續的splice調用將讀取更多數據

  • SPLICE_F_GIFT

    對splice沒有任何效果

使用splice函數時,fd_in和fd_out必須至少有一個是管道文件描述符。

舉例:在寫回射服務器的時候,通過splice函數將客戶端的內容讀入到pipefd[1]中,再使用splice函數從pipefd[0]中讀取內容到客戶端中,實現簡單而高效的回射服務,因為沒有使用revc和send操作,因此並未涉及用戶空間和內核空間之間的數據拷貝,效率很高

tee函數

用於在兩個管道文件描述符之間複製數據,也是零拷貝操作

其不消耗數據,源文件描述符上的數據仍然可以用於後續的讀操作。

#include ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);

參數含義與splice相同,但是fd_in和fd_out都是管道文件描述符


零拷貝原理

傳統操作系統的標準I/O是基於數據拷貝進行操作的,即I/O操作會導致數據在操作系統內核地址空間的緩衝區和應用程序地址空間定義的緩衝區之間進行傳輸,這樣的緩衝區之間的操作實際上減少了磁盤I/O操作,因為數據在高速緩衝存儲器中,就不需要再進行實際的物理磁盤I/O操作(注意這裡所指的是物理磁盤,表示的是直接進行內存上的數據拷貝,而不是寫入磁盤再從磁盤上讀),但是這種內存之間的拷貝動作會導致極大的CPU開銷。

零拷貝技術可以有效地改善數據傳輸的性能,當內核驅動程序處理I/O數據的時候可以減少甚至完全避免不必要CPU數據拷貝操作。

補充:Java中的零拷貝技術的方法是java.nio.channel.FileChannel的transferTo()以及transferFrom()方法,可以用這兩個方法將bytes直接從調用它的channel傳輸到另一個writable byte channel中,中間不會讓數據經過應用程序,提高數據轉移的效率。它不像Linux的系統調用,有著好幾個接口適用於不同的文件描述符情況,這兩個方法可以實現所有種類的文件描述符的傳輸,即socket與普通的文件描述符都可以。而還有的readv和writev的相關功能的零拷貝技術,在Java的nio包中沒有實現,而在Netty這個I/O框架庫中有實現,通過ChannelBuffer接口的眾多實現中的一個實現CompositeChannelBuffer類來使用此功能。

傳統服務器進行數據傳輸的流程

一般來說,傳輸數據的時候,用戶應用程序需要分配一塊大小合適的緩衝區用來存放需要傳輸的數據。應用程序從文件中讀出一塊數據,填充這個緩衝區,然後把這塊數據通過網絡發送到接收端去。這樣接口比較簡單,只需要read()和write()這兩個系統調用就可以完成這個數據傳輸操作,應用程序不會意識到這個過程中OS所作的數據拷貝操作。但是內核在某些情況下,數據拷貝工作會極大地降低數據傳輸的性能。

此外傳統的數據傳輸過程中,數據至少發生了四次拷貝操作,如下圖所示

基礎知識-零拷貝技術

如果使用DMA操作,那麼可以省去硬件設備與內核空間之間的拷貝的步驟,因為DMA操作可以解放與硬件通訊時的CPU消耗,即仍然需要訪問數據兩次,如下圖所示

基礎知識-零拷貝技術

什麼是零拷貝技術

簡單來說,零拷貝技術是一種避免CPU將數據從一塊存儲拷貝到另一塊存儲的技術,零拷貝技術可以消除數據在存儲器之間不必要的中間拷貝次數,從而有效提高數據傳輸效率。可以概括如下

  • 避免數據拷貝

    包括內核緩衝區之間、內核與用戶空間的拷貝,避開OS直接訪問硬件存儲,數據傳輸儘量讓DMA來完成

  • 將多個操作結合在一起

    對於多個內存區域的訪問,本來是需要調用多次的訪問操作才可以,而readv和writev可以一次訪問多個內存區域

零拷貝技術分類

一般包含以下幾種:

  • 直接I/O

    應用程序直接訪問硬件存儲

  • 避免數據在OS 內核地址空間緩衝區和應用程序地址空間緩衝區之間拷貝

  • 對數據在Linux的頁緩存和用戶進程的緩衝區之間的傳輸過程進行優化

    側重於靈活地處理數據在用戶進程的緩衝區和OS頁緩存之間的拷貝操作,主要利用CopyOnWrite技術

基礎知識-零拷貝技術

  • 如果要傳輸的數據量比較大,那麼直接I/O是可以提高性能,但是直接I/O的開銷很大,不能利用緩存I/O的優勢,讀操作會造成磁盤的同步讀,這個執行進程需要很長時間才能執行完成,寫操作會造成應用程序關閉緩慢。因此應用程序使用直接IO進行數據傳輸通常和異步IO結合使用。

    目前執行的操作是,執行open()(打開文件)時指定O_DIRECT標識符。一般用於OS不需要對數據進行處理的情況

前面的兩種主要為了避免應用程序地址空間和OS內核地址空間兩者之間的緩衝區拷貝操作,通常適用於某些特殊情況,例如:傳送的數據不需要經過OS內核的處理or不需要經過應用程序的處理。第三類的方法繼承了傳統的應用程序地址空間和OS內核地址空間之間數據傳輸的概念,即在傳輸效率上進行改善,而不是避免傳輸

具體方法

對於前面的第一種直接I/O沒有什麼好說的,第二種是比較常用的零拷貝技術,通常用的一些mmap()、sendfile()、splice()等都是這種零拷貝技術,下面的部分中具體介紹這些方法中如何實現“零拷貝”,對於第三種零拷貝技術並沒有進行學習,所以這裡只是簡單提了下其中用到的技術。

1. mmap()/write()

利用mmap()代替read()。mmap()通過DMA拷貝數據到OS內核的緩衝區中,然後應用程序與OS共享這個緩衝區,於是OS內核和應用程序內核空間沒有數據拷貝操作。如下圖,需要消耗CPU的只有頁緩存拷貝至socket緩衝區這一步操作,相比於原來所說傳統的I/O已經減少了3步的CPU消耗。具體的拷貝次數,如果加上由DMA操作的磁盤到頁緩存以及socket緩衝區到協議引擎中的過程,總共三次數據拷貝操作。

基礎知識-零拷貝技術

基礎知識-零拷貝技術

2.sendfile()

為了簡化用戶接口,保留原來mmap()、write()操作的優點,引入sendfile()系統調用。其不僅減少了數據拷貝操作,還減少了上下文切換,因為執行了sendfile後需要進入內核態將數拷貝到OS內核緩衝區,然後不返回用戶態(原本的mmap/write是需要返回的),直接將此數據拷貝到socket緩衝區中,從而使用DMA將數據發送到協議引擎,因此減少了上下文的切換,但是也因此只使用於應用程序地址空間不需要對所訪問數據進行處理的情況。

基礎知識-零拷貝技術

上圖這種情況並不是完美的sendfile(),因為可以看到其中存在了一次要消耗CPU的copy操作(DMA操作不消耗CPU),那麼就不符合零拷貝這個名稱了,那麼更好的sendfile()如何實現“零拷貝”操作呢?

如果需要不拷貝,那麼就是要解決頁緩存拷貝到socket緩衝區的那一步,這裡的實現選擇將帶有文件位置和長度信息的緩衝區描述符添加到socket緩衝區中,於是可以不需要進行那唯一的一次拷貝,DMA引擎會直接將數據從OS緩衝區拷貝到協議引擎中去。具體的實現原理如下引用,這裡的原理描述上很像是readv和writev的原理,應該就是這兩個系統調用的實現的根本。

為了避免操作系統內核造成的數據副本,需要用到一個支持收集操作的網絡接口,這也就是說,待傳輸的數據可以分散在存儲的不同位置上,而不需要在連續存儲中存放。這樣一來,從文件中讀出的數據就根本不需要被拷貝到 socket 緩衝區中去,而只是需要將緩衝區描述符傳到網絡協議棧中去,之後其在緩衝區中建立起數據包的相關結構,然後通過 DMA 收集拷貝功能將所有的數據結合成一個網絡數據包

基礎知識-零拷貝技術

總結一下上面的幾次優化過程,即如何從原本的4步拷貝操作變為“零拷貝”:1、通過DMA操作解決了硬件讀取到OS內核緩衝區、內核協議棧緩衝區到網絡接口卡的兩步拷貝操作;2、通過直接mmap()映射減少了內核緩衝區到用戶空間緩衝區的拷貝;3、最後在sendfile()中運用不讓用戶空間緩衝區操作數據以及readv、writev用到的支持收集操作的socket接口的技術,將最後的一次拷貝操作也避免掉,從而實現真正的零拷貝

3. splice()

與mmap()和sendfile()類似。適用於確定數據傳輸路徑的用戶應用程序,簡單來說,前面的sendfile()只能適用於傳輸給socket網絡接口(因為網絡接口實現了數據收集操作),而這裡的splice用於兩個文件描述符之間的傳遞數據,不過需要注意,splice()中的兩個文件描述符參數必須有一個為管道文件描述符。

不過需要注意的是splice本身代表了一種機制,即零拷貝的一種機制,而sendfile()實際上也是它的一個實現,不過上面的splice()這個系統調用引入中限制了兩個文件描述符必須有一個為管道文件描述,所以這個設計上存在了局限性

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

前面說過這種方法的優點在於不用免去OS內核空間對數據的操作or用戶空間對數據的操作,其保留了傳統在用戶應用程序地址空間和OS內核地址空間之間傳遞數據的技術,但是在傳輸上進行優化,這個優化主要就是CopyOnWrite機制,此外還有著共享緩存的機制來實現等等。


分享到:


相關文章: