sendfile:Linux中的"零拷貝"__Linux

來源:互聯網
上載者:User

如今幾乎每個人都聽說過Linux中所謂的"零拷貝"特性,然而我經常碰到沒有充分理解這個問題的人們。因此,我決定寫一些文章略微深入的講述這個問題,希望能將這個有用的特性解釋清楚。在本文中,將從使用者空間應用程式的角度來闡述這個問題,因此有意忽略了複雜的核心實現。

什麼是”零拷貝”

為了更好的理解問題的解決法,我們首先需要理解問題本身。首先我們以一個網路服務守護進程為例,考慮它在將儲存在檔案中的資訊通過網路傳送給客戶這樣的簡單過程中,所涉及的操作。下面是其中的部分簡單代阿:

read(file, tmp_buf, len);
write(socket, tmp_buf, len);

看起來不能更簡單了。你也許認為執行這兩個系統調用並未產生多少開銷。實際上,這簡直錯的一塌糊塗。在執行這兩個系統調用的過程中,目標資料至少被複製了4次,同時發生了同樣多次數的使用者/核心空間的切換(實際上該過程遠比此處描述的要複雜,但是我希望以簡單的方式描述之,以更好的理解本文的主題)。


為了更好的理解這兩句代碼所涉及的操作,請看圖1。圖的上半部展示了環境切換,而下半部展示了複製操作。
圖1. Copying in Two Sample System Calls
步驟一:系統調用read導致了從使用者空間到核心空間的環境切換。DMA模組從磁碟中讀取檔案內容,並將其儲存在核心空間的緩衝區內,完成了第1次複製。

步驟二:資料從核心空間緩衝區複製到使用者空間緩衝區,之後系統調用read返回,這導致了從核心空間向使用者空間的環境切換。此時,需要的資料已存放在指定的使用者空間緩衝區內(參數tmp_buf),程式可以繼續下面的操作。

步驟三:系統調用write導致從使用者空間到核心空間的環境切換。資料從使用者空間緩衝區被再次複製到核心空間緩衝區,完成了第3次複製。不過,這次資料存放在核心空間中與使用的socket相關的特定緩衝區中,而不是步驟一中的緩衝區。

步驟四:系統調用返回,導致了第4次環境切換。第4次複製在DMA模組將資料從核心空間緩衝區傳遞至協議引擎的時候發生,這與我們的代碼的執行是獨立且非同步發生的。你可能會疑惑:“為何要說是獨立、非同步。難道不是在write系統調用返回前資料已經被傳送了。write系統調用的返回,並不意味著傳輸成功——它甚至無法保證傳輸的開始。調用的返回,只是表明乙太網路驅動程式在其傳輸隊列中有空位,並已經接受我們的資料用於傳輸。可能有眾多的資料排在我們的資料之前。除非驅動程式或硬體採用優先順序隊列的方法,各組資料是依照FIFO的次序被傳輸的(圖1中叉狀的DMA copy表明這最後一次複製可以被延後)。

正如你所看到的,上面的過程中存在很多的資料冗餘。某些冗餘可以被消除,以減少開銷、提高效能。作為一名驅動程式開發人員,我的工作圍繞著擁有先進特性的硬體展開。某些硬體支援完全繞開記憶體,將資料直接傳送給其他裝置的特性。這一特性消除了系統記憶體中的資料副本,因此是一種很好的選擇,但並不是所有的硬體都支援。此外,來自於硬碟的資料必須重新打包(地址連續)才能用於網路傳輸,這也引入了某些複雜性。為了減少開銷,我們可以從消除核心緩衝區與使用者緩衝區之間的複製入手。

消除複製的一種方法是將read系統調用,改為mmap系統調用,例如:

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

為了更好的理解這其中設計的操作,請看圖2。環境切換部分與圖1保持一致。

圖2. Calling mmap
步驟一:mmap系統調用導致檔案的內容通過DMA模組被複製到核心緩衝區中,該緩衝區之後與使用者進程共用,這樣就核心緩衝區與使用者緩衝區之間的複製就不會發生。

步驟二:write系統調用導致核心將資料從核心緩衝區複製到與socket相關聯的核心緩衝區中。

步驟三:DMA模組將資料由socket的緩衝區傳遞給協議引擎時,第3次複製發生。

通過調用mmap而不是read,我們已經將核心需要執行的複製操作減半。當有大量資料要進行傳輸是,這將有相當良好的效果。然而,效能的改進需要付出代價的;是用mmap與write這種組合方法,存在著一些隱藏的陷阱。例如,考慮一下在記憶體中對檔案進行映射後調用write,與此同時另外一個進程將同一檔案截斷的情形。此時write系統調用會被進程接收到的SIGBUS訊號中斷,因為當前進程訪問了非法記憶體位址。對SIGBUS訊號的預設處理是殺死當前進程並產生dump core檔案——而這對於網路伺服器程式而言不是最期望的操作。

有兩種方式可用於解決該問題:

第一種方式是為SIGBUS訊號設定訊號處理常式,並在處理常式中簡單的執行return語句。在這樣處理方式下,write系統調用返回被訊號中斷前已寫的位元組數,並將errno全域變數設定為成功。必須指出,這並不是個好的解決方式——治標不治本。由於收到SIGBUS訊號意味著進程發生了嚴重錯誤,我不鼓勵採取這種解決方式。

第二種方式應用了檔案租借(在Microsoft Windows系統中被稱為“機會鎖”)。這才是解勸前面問題的正確方式。通過對檔案描述符執行租借,可以同核心就某個特定檔案達成租約。從核心可以獲得讀/寫租約。當另外一個進程試圖將你正在傳輸的檔案截斷時,核心會向你的進程發送即時訊號——RT_SIGNAL_LEASE。該訊號通知你的進程,核心即將終止在該檔案上你曾獲得的租約。這樣,在write調用訪問非法記憶體位址、並被隨後接收到的SIGBUS訊號殺死之前,write系統調用就被RT_SIGNAL_LEASE訊號中斷了。write的傳回值是在被中斷前已寫的位元組數,全域變數errno設定為成功。下面是一段展示如何從核心獲得租約的範例程式碼。

if(fcntl(fd, F_SETSIG, RT_SIGNAL_LEASE) == -1) {
perror("kernel lease set signal");
return -1;
}

/* l_type can be F_RDLCK F_WRLCK */
if(fcntl(fd, F_SETLEASE, l_type)){
perror("kernel lease set type");
return -1;
}

Sendfile

sendfile系統調用在核心版本2.1中被引入,目的是簡化通過網路在兩個本地檔案之間進行的資料轉送過程。sendfile系統調用的引入,不僅減少了資料複製,還減少了環境切換的次數。使用方法如下:

sendfile(socket, file, len);

為了更好的理解所涉及的操作,請看圖3

圖3. Replacing Read and Write with Sendfile
步驟一:sendfile系統調用導致檔案內容通過DMA模組被複製到某個核心緩衝區,之後再被複製到與socket相關聯的緩衝區內。

步驟二:當DMA模組將位於socket相關聯緩衝區中的資料傳遞給協議引擎時,執行第3次複製。

你可能會在想,我們在調用sendfile發送資料的期間,如果另外一個進程將檔案截斷的話,會發生什麼事情。如果進程沒有為SIGBUS註冊任何訊號處理函數的話,sendfile系統調用返回被訊號中斷前已發送的位元組數,並將全域變數errno置為成功。

然而,如果在調用sendfile之前,從核心獲得了檔案租約,那麼類似的,在sendfile調用返回前會收到RT_SIGNAL_LEASE。

到此為止,我們已經能夠避免核心進行多次複製,然而我們還存在一分多餘的副本。這份副本也可以消除嗎。當然,在硬體提供的一些協助下是可以的。為了消除核心產生的素有資料冗餘,需要網路介面卡支援彙總操作特性。該特性意味著待發送的資料不要求存放在地址連續的記憶體空間中;相反,可以是分散在各個記憶體位置。在核心版本2.4中,socket緩衝區描述符結構發生了改動,以適應彙總操作的要求——這就是Linux中所謂的"零拷貝“。這種方式不僅減少了多個環境切換,而且消除了資料冗餘。從使用者層應用程式的角度來開,沒有發生任何改動,所有代碼仍然是類似下面的形式:

sendfile(socket, file, len);

為了更好的理解所涉及的操作,請看圖4

Figure 4. Hardware that supports gather can assemble data from multiple memory locations, eliminating another copy.
步驟一:sendfile系統調用導致檔案內容通過DMA模組被複製到核心緩衝區中。

步驟二:資料並未被複製到socket關聯的緩衝區內。取而代之的是,只有記錄資料位元置和長度的描述符被加入到socket緩衝區中。DMA模組將資料直接從核心緩衝區傳遞給協議引擎,從而消除了遺留的最後一次複製。

由於資料實際上仍然由磁碟複製到記憶體,再由記憶體複製到發送裝置,有人可能會聲稱這並不是真正的"零拷貝"。然而,從作業系統的角度來看,這就是"零拷貝",因為核心空間內不存在冗餘資料。應用"零拷貝"特性,出了避免複製之外,還能獲得其他效能優勢,例如更少的環境切換,更少的CPU cache汙染以及沒有CPU必要計算校正和。

現在我們明白了什麼是"零拷貝",讓我們將理論付諸實踐,編寫一些代碼。你可以從www.xalien.org/articles/source/sfl-src.tgz處下載完整的源碼。執行"tar -zxvf sfl-src.tgz"將源碼解壓。運行make命令,編譯源碼,並建立隨機資料檔案data.bin

從標頭檔開始介紹代碼:

/* sfl.c sendfile example program
Dragan Stancevic <
header name function / variable
-------------------------------------------------*/
#include <stdio.h> /* printf, perror */
#include <fcntl.h> /* open */
#include <unistd.h> /* close */
#include <errno.h> /* errno */
#include <string.h> /* memset */
#include <sys/socket.h> /* socket */
#include <netinet/in.h> /* sockaddr_in */
#include <sys/sendfile.h> /* sendfile */
#include <arpa/inet.h> /* inet_addr */
#define BUFF_SIZE (10*1024) /* size of the tmp buffer */

除了基本socket操作所需要的 <sys/socket.h> 和<netinet/in.h>標頭檔外,我們還需要包含sendfile系統調用的原型定義,這可以在<sys/sendfile.h>標頭檔中找到。

伺服器標誌:

/* are we sending or receiving */
if(argv[1][0] == 's') is_server++;
/* open descriptors */
sd = socket(PF_INET, SOCK_STREAM, 0);
if(is_server) fd = open("data.bin", O_RDONLY);

該程式既能以服務端/發送方,也能以用戶端/接收方的身份運行。我們需要檢查命令列參數中的一項,然後相應的設定is_server標誌。程式中大開了一個地址族為PF_INET的流通訊端;作為服務端運行時需要向客戶發送資料,因此要開啟某個資料檔案。由於程式中是用sendfile系統調用來發送資料,因此不需要讀取檔案內容並儲存在程式的緩衝區內。

接下來是伺服器位址:

/* clear the memory */
memset(&sa, 0, sizeof(struct sockaddr_in));
/* initialize structure */
sa.sin_family = PF_INET;
sa.sin_port = htons(1033);
sa.sin_addr.s_addr = inet_addr(argv[2]);


將服務端地址結構清零後設定協議族、連接埠和IP地址。服務端的IP地址作為命令列參數傳遞給程式。連接埠號碼寫入程式碼為1033,選擇該連接埠是因為它在要求root許可權的連接埠範圍之上。

下面是服務端的分支代碼:

if(is_server){
int client; /* new client socket */
printf("Server binding to [%s]\n", argv[2]);
if(bind(sd, (struct sockaddr *)&sa,sizeof(sa)) < 0){
perror("bind");
exit(errno);
}

作為服務端,需要為socket描述符分配一個地址,這是通過系統調用bind完成的,它將伺服器位址(sa)分配給socket描述符(sd).

if(listen(sd,1) < 0){
perror("listen");
exit(errno);
}

由於使用流通訊端,必須對核心聲明接受外來串連請求的意願,並設定串連隊列的尺寸。此處將隊列長度設為1,但是通常會將該值設的高一些,用於接受已建立的串連。在老版本的核心中,該隊列被用於阻止SYN flood攻擊。由於listen系統調用之改為設定已建立串連的數量,該特性已被listen調用遺棄。核心參數tcp_max_syn_backlog承擔了保護系統不受SYN flood攻擊的功能。

if((client = accept(sd, NULL, NULL)) < 0){
perror("accept");
exit(errno);
}

accept系統調用從待處理的已串連隊列中選取第一個串連請求,為之建立一個新的socket。accept調用的傳回值是建立立串連的描述符;新的socket可以用於read、write和poll/select系統調用。

if((cnt = sendfile(client,fd,&off, BUFF_SIZE)) < 0){
perror("sendfile");
exit(errno);
}

printf("Server sent %d bytes.\n", cnt);
close(client);

在客戶socket描述符上已經建立好串連,因此可以開始將資料轉送至遠端系統——這時通過調用sendfile系統調用來完成。該調用在Linux中的原型為如下形式:

extern ssize_t
sendfile (int __out_fd, int __in_fd, off_t *offset, size_t __count) __THROW;

前兩個參數為檔案描述符,第三個參數表示sendfile開始傳輸資料的位移量。第四個參數是打算傳輸的位元組數。為了sendfile可以使用"零拷貝“特性,網卡需要支援彙總操作,此外還應具備校正和計算能力。如果你的NIC不具備這些特性,仍可以是用sendfile來發送資料,區別是核心在傳輸前會將所有緩衝區的內容合并。

移植性問題

sendfile系統調用的問題之一,總體上來看,是缺少標準化的實現,這與open系統調用類些。sendfile在Linux、Solaris或HP-UX中的實現有很大的不同。這給希望在網路傳輸代碼中利用"零拷貝"的開發人員帶來了問題。

這些實現差異中的一點在於Linux提供的sendfile,是定義為用於兩個檔案描述符之間和檔案到socket之間的傳輸介面。另一方面,HP-UX和Solaris中,sendfile只能用於檔案到socket的傳輸。

第二點差異,是Linux沒有實現向量化傳輸。Solaris和HP-UX 中的sendfile系統調用包含額外的參數,用於消除為待傳輸資料添加頭部的開銷。

展望

Linux中“零拷貝”的實現還遠未結束,並很可能在不久的未來發生變化。更多的功能將會被添加,例如,現在的sendfile不支援向量化傳輸,而諸如Samba和Apache這樣的伺服器不得不是用TCP_COKR標誌來執行多個sendfile調用。該標誌告知系統還有資料要在下一個sendfile調用中到達。TCP_CORK和TCP_NODELAY不相容,後者在我們希望為資料添加頭部時使用。這也正是一個完美的例子,用於說明支援向量化的sendfile將在那些情況下,消除目前實現所強制產生的多個sendfile調用和延遲。

當前sendfile一個相當令人不愉快的限制是它無法使用者傳輸大於2GB的檔案。如此尺寸大小的檔案,在今天並非十分罕見,不得不複製資料是十分令人失望的。由於這種情況下sendfile和mmap都是停用,在未來核心版本中提供sendfile64,將會提供很大的協助。

結論

儘管有一些缺點,"零拷貝"sendfile是一個很有用的特性。我希望讀者認為本文提供了足夠的資訊以開始在程式中使用sendfile。如果你對這個主題有更深層次的興趣,敬請期待我的第二篇文章——"Zero Copy II: Kernel Perspective",在其中將更深一步的講述"零拷貝"的核心內部實現。


http://t.matong.me/2011/03/29/zero-copy-linux.html

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.