sendfile详解
生活随笔
收集整理的這篇文章主要介紹了
sendfile详解
小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.
Zero Copy I: User-Mode Perspective
作者:Dragan Stancevic,2003-01-01
原文地址:http://www.linuxjournal.com/article/6345
譯者:Love. Katherine,2007-03-25
譯文地址:http://blog.csdn.net/lovekatherine/archive/2007/03/25/1540291.aspx
轉(zhuǎn)載時務(wù)必以超鏈接形式標明文章原始出處及作者、譯者信息。
(解釋Linux中的"零拷貝"特性,為什么有用以及何處需要改進之處)
如今幾乎每個人都聽說過Linux中所謂的"零拷貝"特性,然而我經(jīng)常碰到?jīng)]有充分理解這個問題的人們。因此,我決定寫一些文章略微深入的講述這個問題,希望能將這個有用的特性解釋清楚。在本文中,將從用戶空間應(yīng)用程序的角度來闡述這個問題,因此有意忽略了復(fù)雜的內(nèi)核實現(xiàn)。
什么是”零拷貝”
為了更好的理解問題的解決法,我們首先需要理解問題本身。首先我們以一個網(wǎng)絡(luò)服務(wù)守護進程為例,考慮它在將存儲在文件中的信息通過網(wǎng)絡(luò)傳送給客戶這樣的簡單過程中,所涉及的操作。下面是其中的部分簡單代阿:
read(file, tmp_buf, len);
write(socket, tmp_buf, len);
看起來不能更簡單了。你也許認為執(zhí)行這兩個系統(tǒng)調(diào)用并未產(chǎn)生多少開銷。實際上,這簡直錯的一塌糊涂。在執(zhí)行這兩個系統(tǒng)調(diào)用的過程中,目標數(shù)據(jù)至少被復(fù)制了4次,同時發(fā)生了同樣多次數(shù)的用戶/內(nèi)核空間的切換(實際上該過程遠比此處描述的要復(fù)雜,但是我希望以簡單的方式描述之,以更好的理解本文的主題)。
為了更好的理解這兩句代碼所涉及的操作,請看圖1。圖的上半部展示了上下文切換,而下半部展示了復(fù)制操作。
圖1. Copying in Two Sample System Calls
步驟一:系統(tǒng)調(diào)用read導(dǎo)致了從用戶空間到內(nèi)核空間的上下文切換。DMA模塊從磁盤中讀取文件內(nèi)容,并將其存儲在內(nèi)核空間的緩沖區(qū)內(nèi),完成了第1次復(fù)制。
步驟二:數(shù)據(jù)從內(nèi)核空間緩沖區(qū)復(fù)制到用戶空間緩沖區(qū),之后系統(tǒng)調(diào)用read返回,這導(dǎo)致了從內(nèi)核空間向用戶空間的上下文切換。此時,需要的數(shù)據(jù)已存放在指定的用戶空間緩沖區(qū)內(nèi)(參數(shù)tmp_buf),程序可以繼續(xù)下面的操作。
步驟三:系統(tǒng)調(diào)用write導(dǎo)致從用戶空間到內(nèi)核空間的上下文切換。數(shù)據(jù)從用戶空間緩沖區(qū)被再次復(fù)制到內(nèi)核空間緩沖區(qū),完成了第3次復(fù)制。不過,這次數(shù)據(jù)存放在內(nèi)核空間中與使用的socket相關(guān)的特定緩沖區(qū)中,而不是步驟一中的緩沖區(qū)。
步驟四:系統(tǒng)調(diào)用返回,導(dǎo)致了第4次上下文切換。第4次復(fù)制在DMA模塊將數(shù)據(jù)從內(nèi)核空間緩沖區(qū)傳遞至協(xié)議引擎的時候發(fā)生,這與我們的代碼的執(zhí)行是獨立且異步發(fā)生的。你可能會疑惑:“為何要說是獨立、異步?難道不是在write系統(tǒng)調(diào)用返回前數(shù)據(jù)已經(jīng)被傳送了?write系統(tǒng)調(diào)用的返回,并不意味著傳輸成功——它甚至無法保證傳輸?shù)拈_始。調(diào)用的返回,只是表明以太網(wǎng)驅(qū)動程序在其傳輸隊列中有空位,并已經(jīng)接受我們的數(shù)據(jù)用于傳輸??赡苡斜姸嗟臄?shù)據(jù)排在我們的數(shù)據(jù)之前。除非驅(qū)動程序或硬件采用優(yōu)先級隊列的方法,各組數(shù)據(jù)是依照FIFO的次序被傳輸?shù)?圖1中叉狀的DMA copy表明這最后一次復(fù)制可以被延后)。
正如你所看到的,上面的過程中存在很多的數(shù)據(jù)冗余。某些冗余可以被消除,以減少開銷、提高性能。作為一名驅(qū)動程序開發(fā)人員,我的工作圍繞著擁有先進特性的硬件展開。某些硬件支持完全繞開內(nèi)存,將數(shù)據(jù)直接傳送給其他設(shè)備的特性。這一特性消除了系統(tǒng)內(nèi)存中的數(shù)據(jù)副本,因此是一種很好的選擇,但并不是所有的硬件都支持。此外,來自于硬盤的數(shù)據(jù)必須重新打包(地址連續(xù))才能用于網(wǎng)絡(luò)傳輸,這也引入了某些復(fù)雜性。為了減少開銷,我們可以從消除內(nèi)核緩沖區(qū)與用戶緩沖區(qū)之間的復(fù)制入手。
消除復(fù)制的一種方法是將read系統(tǒng)調(diào)用,改為mmap系統(tǒng)調(diào)用,例如:
tmp_buf = mmap(file, len);
write(socket, tmp_buf, len);
為了更好的理解這其中設(shè)計的操作,請看圖2。上下文切換部分與圖1保持一致。
圖2. Calling mmap
步驟一:mmap系統(tǒng)調(diào)用導(dǎo)致文件的內(nèi)容通過DMA模塊被復(fù)制到內(nèi)核緩沖區(qū)中,該緩沖區(qū)之后與用戶進程共享,這樣就內(nèi)核緩沖區(qū)與用戶緩沖區(qū)之間的復(fù)制就不會發(fā)生。
步驟二:write系統(tǒng)調(diào)用導(dǎo)致內(nèi)核將數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到與socket相關(guān)聯(lián)的內(nèi)核緩沖區(qū)中。
步驟三:DMA模塊將數(shù)據(jù)由socket的緩沖區(qū)傳遞給協(xié)議引擎時,第3次復(fù)制發(fā)生。
通過調(diào)用mmap而不是read,我們已經(jīng)將內(nèi)核需要執(zhí)行的復(fù)制操作減半。當有大量數(shù)據(jù)要進行傳輸是,這將有相當良好的效果。然而,性能的改進需要付出代價的;是用mmap與write這種組合方法,存在著一些隱藏的陷阱。例如,考慮一下在內(nèi)存中對文件進行映射后調(diào)用write,與此同時另外一個進程將同一文件截斷的情形。此時write系統(tǒng)調(diào)用會被進程接收到的SIGBUS信號中斷,因為當前進程訪問了非法內(nèi)存地址。對SIGBUS信號的默認處理是殺死當前進程并生成dump core文件——而這對于網(wǎng)絡(luò)服務(wù)器程序而言不是最期望的操作。
有兩種方式可用于解決該問題:
第一種方式是為SIGBUS信號設(shè)置信號處理程序,并在處理程序中簡單的執(zhí)行return語句。在這樣處理方式下,write系統(tǒng)調(diào)用返回被信號中斷前已寫的字節(jié)數(shù),并將errno全局變量設(shè)置為成功。必須指出,這并不是個好的解決方式——治標不治本。由于收到SIGBUS信號意味著進程發(fā)生了嚴重錯誤,我不鼓勵采取這種解決方式。
第二種方式應(yīng)用了文件租借(在Microsoft Windows系統(tǒng)中被稱為“機會鎖”)。這才是解勸前面問題的正確方式。通過對文件描述符執(zhí)行租借,可以同內(nèi)核就某個特定文件達成租約。從內(nèi)核可以獲得讀/寫租約。當另外一個進程試圖將你正在傳輸?shù)奈募財鄷r,內(nèi)核會向你的進程發(fā)送實時信號——RT_SIGNAL_LEASE。該信號通知你的進程,內(nèi)核即將終止在該文件上你曾獲得的租約。這樣,在write調(diào)用訪問非法內(nèi)存地址、并被隨后接收到的SIGBUS信號殺死之前,write系統(tǒng)調(diào)用就被RT_SIGNAL_LEASE信號中斷了。write的返回值是在被中斷前已寫的字節(jié)數(shù),全局變量errno設(shè)置為成功。下面是一段展示如何從內(nèi)核獲得租約的示例代碼。
if(fcntl(fd, F_SETSIG, RT_SIGNAL_LEASE) == -1) {
perror("kernel lease set signal");
return -1;
}
if(fcntl(fd, F_SETLEASE, l_type)){
perror("kernel lease set type");
return -1;
}
在對文件進行映射前,應(yīng)該先獲得租約,并在結(jié)束write操作后結(jié)束租約。這是通過在fcntl調(diào)用中指定租約類型為F_UNLCK了來實現(xiàn)的。
Sendfile
sendfile系統(tǒng)調(diào)用在內(nèi)核版本2.1中被引入,目的是簡化通過網(wǎng)絡(luò)在兩個本地文件之間進行的數(shù)據(jù)傳輸過程。sendfile系統(tǒng)調(diào)用的引入,不僅減少了數(shù)據(jù)復(fù)制,還減少了上下文切換的次數(shù)。使用方法如下:
sendfile(socket, file, len);
為了更好的理解所涉及的操作,請看圖3
圖3. Replacing Read and Write with Sendfile
步驟一:sendfile系統(tǒng)調(diào)用導(dǎo)致文件內(nèi)容通過DMA模塊被復(fù)制到某個內(nèi)核緩沖區(qū),之后再被復(fù)制到與socket相關(guān)聯(lián)的緩沖區(qū)內(nèi)。
步驟二:當DMA模塊將位于socket相關(guān)聯(lián)緩沖區(qū)中的數(shù)據(jù)傳遞給協(xié)議引擎時,執(zhí)行第3次復(fù)制。
你可能會在想,我們在調(diào)用sendfile發(fā)送數(shù)據(jù)的期間,如果另外一個進程將文件截斷的話,會發(fā)生什么事情?如果進程沒有為SIGBUS注冊任何信號處理函數(shù)的話,sendfile系統(tǒng)調(diào)用返回被信號中斷前已發(fā)送的字節(jié)數(shù),并將全局變量errno置為成功。
然而,如果在調(diào)用sendfile之前,從內(nèi)核獲得了文件租約,那么類似的,在sendfile調(diào)用返回前會收到RT_SIGNAL_LEASE。
到此為止,我們已經(jīng)能夠避免內(nèi)核進行多次復(fù)制,然而我們還存在一分多余的副本。這份副本也可以消除嗎?當然,在硬件提供的一些幫助下是可以的。為了消除內(nèi)核產(chǎn)生的素有數(shù)據(jù)冗余,需要網(wǎng)絡(luò)適配器支持聚合操作特性。該特性意味著待發(fā)送的數(shù)據(jù)不要求存放在地址連續(xù)的內(nèi)存空間中;相反,可以是分散在各個內(nèi)存位置。在內(nèi)核版本2.4中,socket緩沖區(qū)描述符結(jié)構(gòu)發(fā)生了改動,以適應(yīng)聚合操作的要求——這就是Linux中所謂的"零拷貝“。這種方式不僅減少了多個上下文切換,而且消除了數(shù)據(jù)冗余。從用戶層應(yīng)用程序的角度來開,沒有發(fā)生任何改動,所有代碼仍然是類似下面的形式:
sendfile(socket, file, len);
為了更好的理解所涉及的操作,請看圖4
Figure 4. Hardware that supports gather can assemble data from multiple memory locations, eliminating another copy.
步驟一:sendfile系統(tǒng)調(diào)用導(dǎo)致文件內(nèi)容通過DMA模塊被復(fù)制到內(nèi)核緩沖區(qū)中。
步驟二:數(shù)據(jù)并未被復(fù)制到socket關(guān)聯(lián)的緩沖區(qū)內(nèi)。取而代之的是,只有記錄數(shù)據(jù)位置和長度的描述符被加入到socket緩沖區(qū)中。DMA模塊將數(shù)據(jù)直接從內(nèi)核緩沖區(qū)傳遞給協(xié)議引擎,從而消除了遺留的最后一次復(fù)制。
由于數(shù)據(jù)實際上仍然由磁盤復(fù)制到內(nèi)存,再由內(nèi)存復(fù)制到發(fā)送設(shè)備,有人可能會聲稱這并不是真正的"零拷貝"。然而,從操作系統(tǒng)的角度來看,這就是"零拷貝",因為內(nèi)核空間內(nèi)不存在冗余數(shù)據(jù)。應(yīng)用"零拷貝"特性,出了避免復(fù)制之外,還能獲得其他性能優(yōu)勢,例如更少的上下文切換,更少的CPU cache污染以及沒有CPU必要計算校驗和。
現(xiàn)在我們明白了什么是"零拷貝",讓我們將理論付諸實踐,編寫一些代碼。你可以從www.xalien.org/articles/source/sfl-src.tgz處下載完整的源碼。執(zhí)行"tar -zxvf sfl-src.tgz"將源碼解壓。運行make命令,編譯源碼,并創(chuàng)建隨機數(shù)據(jù)文件data.bin
從頭文件開始介紹代碼:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/sendfile.h>
#include <arpa/inet.h>
#define BUFF_SIZE (10*1024)
除了基本socket操作所需要的 <sys/socket.h> 和<netinet/in.h>頭文件外,我們還需要包含sendfile系統(tǒng)調(diào)用的原型定義,這可以在<sys/sendfile.h>頭文件中找到。
服務(wù)器標志:
if(argv[1][0] == 's') is_server++;
sd = socket(PF_INET, SOCK_STREAM, 0);
if(is_server) fd = open("data.bin", O_RDONLY);
該程序既能以服務(wù)端/發(fā)送方,也能以客戶端/接收方的身份運行。我們需要檢查命令行參數(shù)中的一項,然后相應(yīng)的設(shè)置is_server標志。程序中大開了一個地址族為PF_INET的流套接字;作為服務(wù)端運行時需要向客戶發(fā)送數(shù)據(jù),因此要打開某個數(shù)據(jù)文件。由于程序中是用sendfile系統(tǒng)調(diào)用來發(fā)送數(shù)據(jù),因此不需要讀取文件內(nèi)容并存儲在程序的緩沖區(qū)內(nèi)。
接下來是服務(wù)器地址:
memset(&sa, 0, sizeof(struct sockaddr_in));
sa.sin_family = PF_INET;
sa.sin_port = htons(1033);
sa.sin_addr.s_addr = inet_addr(argv[2]);
將服務(wù)端地址結(jié)構(gòu)清零后設(shè)置協(xié)議族、端口和IP地址。服務(wù)端的IP地址作為命令行參數(shù)傳遞給程序。端口號硬編碼為1033,選擇該端口是因為它在要求root權(quán)限的端口范圍之上。
下面是服務(wù)端的分支代碼:
if(is_server){
int client;
printf("Server binding to [%s]\n", argv[2]);
if(bind(sd, (struct sockaddr *)&sa,sizeof(sa)) < 0){
perror("bind");
exit(errno);
}
作為服務(wù)端,需要為socket描述符分配一個地址,這是通過系統(tǒng)調(diào)用bind完成的,它將服務(wù)器地址(sa)分配給socket描述符(sd).
if(listen(sd,1) < 0){
perror("listen");
exit(errno);
}
由于使用流套接字,必須對內(nèi)核聲明接受外來連接請求的意愿,并設(shè)置連接隊列的尺寸。此處將隊列長度設(shè)為1,但是通常會將該值設(shè)的高一些,用于接受已建立的連接。在老版本的內(nèi)核中,該隊列被用于阻止SYN flood攻擊。由于listen系統(tǒng)調(diào)用之改為設(shè)定已建立連接的數(shù)量,該特性已被listen調(diào)用遺棄。內(nèi)核參數(shù)tcp_max_syn_backlog承擔了保護系統(tǒng)不受SYN flood攻擊的功能。
if((client = accept(sd, NULL, NULL)) < 0){
perror("accept");
exit(errno);
}
accept系統(tǒng)調(diào)用從待處理的已連接隊列中選取第一個連接請求,為之建立一個新的socket。accept調(diào)用的返回值是新建立連接的描述符;新的socket可以用于read、write和poll/select系統(tǒng)調(diào)用。
if((cnt = sendfile(client,fd,&off, BUFF_SIZE)) < 0){
perror("sendfile");
exit(errno);
}
printf("Server sent %d bytes.\n", cnt);
close(client);
在客戶socket描述符上已經(jīng)建立好連接,因此可以開始將數(shù)據(jù)傳輸至遠端系統(tǒng)——這時通過調(diào)用sendfile系統(tǒng)調(diào)用來完成。該調(diào)用在Linux中的原型為如下形式:
extern ssize_t
sendfile (int __out_fd, int __in_fd, off_t *offset, size_t __count) __THROW;
前兩個參數(shù)為文件描述符,第三個參數(shù)表示sendfile開始傳輸數(shù)據(jù)的偏移量。第四個參數(shù)是打算傳輸?shù)淖止?jié)數(shù)。為了sendfile可以使用"零拷貝“特性,網(wǎng)卡需要支持聚合操作,此外還應(yīng)具備校驗和計算能力。如果你的NIC不具備這些特性,仍可以是用sendfile來發(fā)送數(shù)據(jù),區(qū)別是內(nèi)核在傳輸前會將所有緩沖區(qū)的內(nèi)容合并。
移植性問題
sendfile系統(tǒng)調(diào)用的問題之一,總體上來看,是缺少標準化的實現(xiàn),這與open系統(tǒng)調(diào)用類些。sendfile在Linux、Solaris或HP-UX中的實現(xiàn)有很大的不同。這給希望在網(wǎng)絡(luò)傳輸代碼中利用"零拷貝"的開發(fā)者帶來了問題。
這些實現(xiàn)差異中的一點在于Linux提供的sendfile,是定義為用于兩個文件描述符之間和文件到socket之間的傳輸接口。另一方面,HP-UX和Solaris中,sendfile只能用于文件到socket的傳輸。
第二點差異,是Linux沒有實現(xiàn)向量化傳輸。Solaris和HP-UX 中的sendfile系統(tǒng)調(diào)用包含額外的參數(shù),用于消除為待傳輸數(shù)據(jù)添加頭部的開銷。
展望
Linux中“零拷貝”的實現(xiàn)還遠未結(jié)束,并很可能在不久的未來發(fā)生變化。更多的功能將會被添加,例如,現(xiàn)在的sendfile不支持向量化傳輸,而諸如Samba和Apache這樣的服務(wù)器不得不是用TCP_COKR標志來執(zhí)行多個sendfile調(diào)用。該標志告知系統(tǒng)還有數(shù)據(jù)要在下一個sendfile調(diào)用中到達。TCP_CORK和TCP_NODELAY不兼容,后者在我們希望為數(shù)據(jù)添加頭部時使用。這也正是一個完美的例子,用于說明支持向量化的sendfile將在那些情況下,消除目前實現(xiàn)所強制產(chǎn)生的多個sendfile調(diào)用和延遲。
當前sendfile一個相當令人不愉快的限制是它無法用戶傳輸大于2GB的文件。如此尺寸大小的文件,在今天并非十分罕見,不得不復(fù)制數(shù)據(jù)是十分令人失望的。由于這種情況下sendfile和mmap都是不可用的,在未來內(nèi)核版本中提供sendfile64,將會提供很大的幫助。
結(jié)論
盡管有一些缺點,"零拷貝"sendfile是一個很有用的特性。我希望讀者認為本文提供了足夠的信息以開始在程序中使用sendfile。如果你對這個主題有更深層次的興趣,敬請期待我的第二篇文章——"Zero Copy II: Kernel Perspective",在其中將更深一步的講述"零拷貝"的內(nèi)核內(nèi)部實現(xiàn)
作者:Dragan Stancevic,2003-01-01
原文地址:http://www.linuxjournal.com/article/6345
譯者:Love. Katherine,2007-03-25
譯文地址:http://blog.csdn.net/lovekatherine/archive/2007/03/25/1540291.aspx
轉(zhuǎn)載時務(wù)必以超鏈接形式標明文章原始出處及作者、譯者信息。
(解釋Linux中的"零拷貝"特性,為什么有用以及何處需要改進之處)
如今幾乎每個人都聽說過Linux中所謂的"零拷貝"特性,然而我經(jīng)常碰到?jīng)]有充分理解這個問題的人們。因此,我決定寫一些文章略微深入的講述這個問題,希望能將這個有用的特性解釋清楚。在本文中,將從用戶空間應(yīng)用程序的角度來闡述這個問題,因此有意忽略了復(fù)雜的內(nèi)核實現(xiàn)。
什么是”零拷貝”
為了更好的理解問題的解決法,我們首先需要理解問題本身。首先我們以一個網(wǎng)絡(luò)服務(wù)守護進程為例,考慮它在將存儲在文件中的信息通過網(wǎng)絡(luò)傳送給客戶這樣的簡單過程中,所涉及的操作。下面是其中的部分簡單代阿:
read(file, tmp_buf, len);
write(socket, tmp_buf, len);
看起來不能更簡單了。你也許認為執(zhí)行這兩個系統(tǒng)調(diào)用并未產(chǎn)生多少開銷。實際上,這簡直錯的一塌糊涂。在執(zhí)行這兩個系統(tǒng)調(diào)用的過程中,目標數(shù)據(jù)至少被復(fù)制了4次,同時發(fā)生了同樣多次數(shù)的用戶/內(nèi)核空間的切換(實際上該過程遠比此處描述的要復(fù)雜,但是我希望以簡單的方式描述之,以更好的理解本文的主題)。
為了更好的理解這兩句代碼所涉及的操作,請看圖1。圖的上半部展示了上下文切換,而下半部展示了復(fù)制操作。
圖1. Copying in Two Sample System Calls
步驟一:系統(tǒng)調(diào)用read導(dǎo)致了從用戶空間到內(nèi)核空間的上下文切換。DMA模塊從磁盤中讀取文件內(nèi)容,并將其存儲在內(nèi)核空間的緩沖區(qū)內(nèi),完成了第1次復(fù)制。
步驟二:數(shù)據(jù)從內(nèi)核空間緩沖區(qū)復(fù)制到用戶空間緩沖區(qū),之后系統(tǒng)調(diào)用read返回,這導(dǎo)致了從內(nèi)核空間向用戶空間的上下文切換。此時,需要的數(shù)據(jù)已存放在指定的用戶空間緩沖區(qū)內(nèi)(參數(shù)tmp_buf),程序可以繼續(xù)下面的操作。
步驟三:系統(tǒng)調(diào)用write導(dǎo)致從用戶空間到內(nèi)核空間的上下文切換。數(shù)據(jù)從用戶空間緩沖區(qū)被再次復(fù)制到內(nèi)核空間緩沖區(qū),完成了第3次復(fù)制。不過,這次數(shù)據(jù)存放在內(nèi)核空間中與使用的socket相關(guān)的特定緩沖區(qū)中,而不是步驟一中的緩沖區(qū)。
步驟四:系統(tǒng)調(diào)用返回,導(dǎo)致了第4次上下文切換。第4次復(fù)制在DMA模塊將數(shù)據(jù)從內(nèi)核空間緩沖區(qū)傳遞至協(xié)議引擎的時候發(fā)生,這與我們的代碼的執(zhí)行是獨立且異步發(fā)生的。你可能會疑惑:“為何要說是獨立、異步?難道不是在write系統(tǒng)調(diào)用返回前數(shù)據(jù)已經(jīng)被傳送了?write系統(tǒng)調(diào)用的返回,并不意味著傳輸成功——它甚至無法保證傳輸?shù)拈_始。調(diào)用的返回,只是表明以太網(wǎng)驅(qū)動程序在其傳輸隊列中有空位,并已經(jīng)接受我們的數(shù)據(jù)用于傳輸??赡苡斜姸嗟臄?shù)據(jù)排在我們的數(shù)據(jù)之前。除非驅(qū)動程序或硬件采用優(yōu)先級隊列的方法,各組數(shù)據(jù)是依照FIFO的次序被傳輸?shù)?圖1中叉狀的DMA copy表明這最后一次復(fù)制可以被延后)。
正如你所看到的,上面的過程中存在很多的數(shù)據(jù)冗余。某些冗余可以被消除,以減少開銷、提高性能。作為一名驅(qū)動程序開發(fā)人員,我的工作圍繞著擁有先進特性的硬件展開。某些硬件支持完全繞開內(nèi)存,將數(shù)據(jù)直接傳送給其他設(shè)備的特性。這一特性消除了系統(tǒng)內(nèi)存中的數(shù)據(jù)副本,因此是一種很好的選擇,但并不是所有的硬件都支持。此外,來自于硬盤的數(shù)據(jù)必須重新打包(地址連續(xù))才能用于網(wǎng)絡(luò)傳輸,這也引入了某些復(fù)雜性。為了減少開銷,我們可以從消除內(nèi)核緩沖區(qū)與用戶緩沖區(qū)之間的復(fù)制入手。
消除復(fù)制的一種方法是將read系統(tǒng)調(diào)用,改為mmap系統(tǒng)調(diào)用,例如:
tmp_buf = mmap(file, len);
write(socket, tmp_buf, len);
為了更好的理解這其中設(shè)計的操作,請看圖2。上下文切換部分與圖1保持一致。
圖2. Calling mmap
步驟一:mmap系統(tǒng)調(diào)用導(dǎo)致文件的內(nèi)容通過DMA模塊被復(fù)制到內(nèi)核緩沖區(qū)中,該緩沖區(qū)之后與用戶進程共享,這樣就內(nèi)核緩沖區(qū)與用戶緩沖區(qū)之間的復(fù)制就不會發(fā)生。
步驟二:write系統(tǒng)調(diào)用導(dǎo)致內(nèi)核將數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到與socket相關(guān)聯(lián)的內(nèi)核緩沖區(qū)中。
步驟三:DMA模塊將數(shù)據(jù)由socket的緩沖區(qū)傳遞給協(xié)議引擎時,第3次復(fù)制發(fā)生。
通過調(diào)用mmap而不是read,我們已經(jīng)將內(nèi)核需要執(zhí)行的復(fù)制操作減半。當有大量數(shù)據(jù)要進行傳輸是,這將有相當良好的效果。然而,性能的改進需要付出代價的;是用mmap與write這種組合方法,存在著一些隱藏的陷阱。例如,考慮一下在內(nèi)存中對文件進行映射后調(diào)用write,與此同時另外一個進程將同一文件截斷的情形。此時write系統(tǒng)調(diào)用會被進程接收到的SIGBUS信號中斷,因為當前進程訪問了非法內(nèi)存地址。對SIGBUS信號的默認處理是殺死當前進程并生成dump core文件——而這對于網(wǎng)絡(luò)服務(wù)器程序而言不是最期望的操作。
有兩種方式可用于解決該問題:
第一種方式是為SIGBUS信號設(shè)置信號處理程序,并在處理程序中簡單的執(zhí)行return語句。在這樣處理方式下,write系統(tǒng)調(diào)用返回被信號中斷前已寫的字節(jié)數(shù),并將errno全局變量設(shè)置為成功。必須指出,這并不是個好的解決方式——治標不治本。由于收到SIGBUS信號意味著進程發(fā)生了嚴重錯誤,我不鼓勵采取這種解決方式。
第二種方式應(yīng)用了文件租借(在Microsoft Windows系統(tǒng)中被稱為“機會鎖”)。這才是解勸前面問題的正確方式。通過對文件描述符執(zhí)行租借,可以同內(nèi)核就某個特定文件達成租約。從內(nèi)核可以獲得讀/寫租約。當另外一個進程試圖將你正在傳輸?shù)奈募財鄷r,內(nèi)核會向你的進程發(fā)送實時信號——RT_SIGNAL_LEASE。該信號通知你的進程,內(nèi)核即將終止在該文件上你曾獲得的租約。這樣,在write調(diào)用訪問非法內(nèi)存地址、并被隨后接收到的SIGBUS信號殺死之前,write系統(tǒng)調(diào)用就被RT_SIGNAL_LEASE信號中斷了。write的返回值是在被中斷前已寫的字節(jié)數(shù),全局變量errno設(shè)置為成功。下面是一段展示如何從內(nèi)核獲得租約的示例代碼。
if(fcntl(fd, F_SETSIG, RT_SIGNAL_LEASE) == -1) {
perror("kernel lease set signal");
return -1;
}
if(fcntl(fd, F_SETLEASE, l_type)){
perror("kernel lease set type");
return -1;
}
在對文件進行映射前,應(yīng)該先獲得租約,并在結(jié)束write操作后結(jié)束租約。這是通過在fcntl調(diào)用中指定租約類型為F_UNLCK了來實現(xiàn)的。
Sendfile
sendfile系統(tǒng)調(diào)用在內(nèi)核版本2.1中被引入,目的是簡化通過網(wǎng)絡(luò)在兩個本地文件之間進行的數(shù)據(jù)傳輸過程。sendfile系統(tǒng)調(diào)用的引入,不僅減少了數(shù)據(jù)復(fù)制,還減少了上下文切換的次數(shù)。使用方法如下:
sendfile(socket, file, len);
為了更好的理解所涉及的操作,請看圖3
圖3. Replacing Read and Write with Sendfile
步驟一:sendfile系統(tǒng)調(diào)用導(dǎo)致文件內(nèi)容通過DMA模塊被復(fù)制到某個內(nèi)核緩沖區(qū),之后再被復(fù)制到與socket相關(guān)聯(lián)的緩沖區(qū)內(nèi)。
步驟二:當DMA模塊將位于socket相關(guān)聯(lián)緩沖區(qū)中的數(shù)據(jù)傳遞給協(xié)議引擎時,執(zhí)行第3次復(fù)制。
你可能會在想,我們在調(diào)用sendfile發(fā)送數(shù)據(jù)的期間,如果另外一個進程將文件截斷的話,會發(fā)生什么事情?如果進程沒有為SIGBUS注冊任何信號處理函數(shù)的話,sendfile系統(tǒng)調(diào)用返回被信號中斷前已發(fā)送的字節(jié)數(shù),并將全局變量errno置為成功。
然而,如果在調(diào)用sendfile之前,從內(nèi)核獲得了文件租約,那么類似的,在sendfile調(diào)用返回前會收到RT_SIGNAL_LEASE。
到此為止,我們已經(jīng)能夠避免內(nèi)核進行多次復(fù)制,然而我們還存在一分多余的副本。這份副本也可以消除嗎?當然,在硬件提供的一些幫助下是可以的。為了消除內(nèi)核產(chǎn)生的素有數(shù)據(jù)冗余,需要網(wǎng)絡(luò)適配器支持聚合操作特性。該特性意味著待發(fā)送的數(shù)據(jù)不要求存放在地址連續(xù)的內(nèi)存空間中;相反,可以是分散在各個內(nèi)存位置。在內(nèi)核版本2.4中,socket緩沖區(qū)描述符結(jié)構(gòu)發(fā)生了改動,以適應(yīng)聚合操作的要求——這就是Linux中所謂的"零拷貝“。這種方式不僅減少了多個上下文切換,而且消除了數(shù)據(jù)冗余。從用戶層應(yīng)用程序的角度來開,沒有發(fā)生任何改動,所有代碼仍然是類似下面的形式:
sendfile(socket, file, len);
為了更好的理解所涉及的操作,請看圖4
Figure 4. Hardware that supports gather can assemble data from multiple memory locations, eliminating another copy.
步驟一:sendfile系統(tǒng)調(diào)用導(dǎo)致文件內(nèi)容通過DMA模塊被復(fù)制到內(nèi)核緩沖區(qū)中。
步驟二:數(shù)據(jù)并未被復(fù)制到socket關(guān)聯(lián)的緩沖區(qū)內(nèi)。取而代之的是,只有記錄數(shù)據(jù)位置和長度的描述符被加入到socket緩沖區(qū)中。DMA模塊將數(shù)據(jù)直接從內(nèi)核緩沖區(qū)傳遞給協(xié)議引擎,從而消除了遺留的最后一次復(fù)制。
由于數(shù)據(jù)實際上仍然由磁盤復(fù)制到內(nèi)存,再由內(nèi)存復(fù)制到發(fā)送設(shè)備,有人可能會聲稱這并不是真正的"零拷貝"。然而,從操作系統(tǒng)的角度來看,這就是"零拷貝",因為內(nèi)核空間內(nèi)不存在冗余數(shù)據(jù)。應(yīng)用"零拷貝"特性,出了避免復(fù)制之外,還能獲得其他性能優(yōu)勢,例如更少的上下文切換,更少的CPU cache污染以及沒有CPU必要計算校驗和。
現(xiàn)在我們明白了什么是"零拷貝",讓我們將理論付諸實踐,編寫一些代碼。你可以從www.xalien.org/articles/source/sfl-src.tgz處下載完整的源碼。執(zhí)行"tar -zxvf sfl-src.tgz"將源碼解壓。運行make命令,編譯源碼,并創(chuàng)建隨機數(shù)據(jù)文件data.bin
從頭文件開始介紹代碼:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/sendfile.h>
#include <arpa/inet.h>
#define BUFF_SIZE (10*1024)
除了基本socket操作所需要的 <sys/socket.h> 和<netinet/in.h>頭文件外,我們還需要包含sendfile系統(tǒng)調(diào)用的原型定義,這可以在<sys/sendfile.h>頭文件中找到。
服務(wù)器標志:
if(argv[1][0] == 's') is_server++;
sd = socket(PF_INET, SOCK_STREAM, 0);
if(is_server) fd = open("data.bin", O_RDONLY);
該程序既能以服務(wù)端/發(fā)送方,也能以客戶端/接收方的身份運行。我們需要檢查命令行參數(shù)中的一項,然后相應(yīng)的設(shè)置is_server標志。程序中大開了一個地址族為PF_INET的流套接字;作為服務(wù)端運行時需要向客戶發(fā)送數(shù)據(jù),因此要打開某個數(shù)據(jù)文件。由于程序中是用sendfile系統(tǒng)調(diào)用來發(fā)送數(shù)據(jù),因此不需要讀取文件內(nèi)容并存儲在程序的緩沖區(qū)內(nèi)。
接下來是服務(wù)器地址:
memset(&sa, 0, sizeof(struct sockaddr_in));
sa.sin_family = PF_INET;
sa.sin_port = htons(1033);
sa.sin_addr.s_addr = inet_addr(argv[2]);
將服務(wù)端地址結(jié)構(gòu)清零后設(shè)置協(xié)議族、端口和IP地址。服務(wù)端的IP地址作為命令行參數(shù)傳遞給程序。端口號硬編碼為1033,選擇該端口是因為它在要求root權(quán)限的端口范圍之上。
下面是服務(wù)端的分支代碼:
if(is_server){
int client;
printf("Server binding to [%s]\n", argv[2]);
if(bind(sd, (struct sockaddr *)&sa,sizeof(sa)) < 0){
perror("bind");
exit(errno);
}
作為服務(wù)端,需要為socket描述符分配一個地址,這是通過系統(tǒng)調(diào)用bind完成的,它將服務(wù)器地址(sa)分配給socket描述符(sd).
if(listen(sd,1) < 0){
perror("listen");
exit(errno);
}
由于使用流套接字,必須對內(nèi)核聲明接受外來連接請求的意愿,并設(shè)置連接隊列的尺寸。此處將隊列長度設(shè)為1,但是通常會將該值設(shè)的高一些,用于接受已建立的連接。在老版本的內(nèi)核中,該隊列被用于阻止SYN flood攻擊。由于listen系統(tǒng)調(diào)用之改為設(shè)定已建立連接的數(shù)量,該特性已被listen調(diào)用遺棄。內(nèi)核參數(shù)tcp_max_syn_backlog承擔了保護系統(tǒng)不受SYN flood攻擊的功能。
if((client = accept(sd, NULL, NULL)) < 0){
perror("accept");
exit(errno);
}
accept系統(tǒng)調(diào)用從待處理的已連接隊列中選取第一個連接請求,為之建立一個新的socket。accept調(diào)用的返回值是新建立連接的描述符;新的socket可以用于read、write和poll/select系統(tǒng)調(diào)用。
if((cnt = sendfile(client,fd,&off, BUFF_SIZE)) < 0){
perror("sendfile");
exit(errno);
}
printf("Server sent %d bytes.\n", cnt);
close(client);
在客戶socket描述符上已經(jīng)建立好連接,因此可以開始將數(shù)據(jù)傳輸至遠端系統(tǒng)——這時通過調(diào)用sendfile系統(tǒng)調(diào)用來完成。該調(diào)用在Linux中的原型為如下形式:
extern ssize_t
sendfile (int __out_fd, int __in_fd, off_t *offset, size_t __count) __THROW;
前兩個參數(shù)為文件描述符,第三個參數(shù)表示sendfile開始傳輸數(shù)據(jù)的偏移量。第四個參數(shù)是打算傳輸?shù)淖止?jié)數(shù)。為了sendfile可以使用"零拷貝“特性,網(wǎng)卡需要支持聚合操作,此外還應(yīng)具備校驗和計算能力。如果你的NIC不具備這些特性,仍可以是用sendfile來發(fā)送數(shù)據(jù),區(qū)別是內(nèi)核在傳輸前會將所有緩沖區(qū)的內(nèi)容合并。
移植性問題
sendfile系統(tǒng)調(diào)用的問題之一,總體上來看,是缺少標準化的實現(xiàn),這與open系統(tǒng)調(diào)用類些。sendfile在Linux、Solaris或HP-UX中的實現(xiàn)有很大的不同。這給希望在網(wǎng)絡(luò)傳輸代碼中利用"零拷貝"的開發(fā)者帶來了問題。
這些實現(xiàn)差異中的一點在于Linux提供的sendfile,是定義為用于兩個文件描述符之間和文件到socket之間的傳輸接口。另一方面,HP-UX和Solaris中,sendfile只能用于文件到socket的傳輸。
第二點差異,是Linux沒有實現(xiàn)向量化傳輸。Solaris和HP-UX 中的sendfile系統(tǒng)調(diào)用包含額外的參數(shù),用于消除為待傳輸數(shù)據(jù)添加頭部的開銷。
展望
Linux中“零拷貝”的實現(xiàn)還遠未結(jié)束,并很可能在不久的未來發(fā)生變化。更多的功能將會被添加,例如,現(xiàn)在的sendfile不支持向量化傳輸,而諸如Samba和Apache這樣的服務(wù)器不得不是用TCP_COKR標志來執(zhí)行多個sendfile調(diào)用。該標志告知系統(tǒng)還有數(shù)據(jù)要在下一個sendfile調(diào)用中到達。TCP_CORK和TCP_NODELAY不兼容,后者在我們希望為數(shù)據(jù)添加頭部時使用。這也正是一個完美的例子,用于說明支持向量化的sendfile將在那些情況下,消除目前實現(xiàn)所強制產(chǎn)生的多個sendfile調(diào)用和延遲。
當前sendfile一個相當令人不愉快的限制是它無法用戶傳輸大于2GB的文件。如此尺寸大小的文件,在今天并非十分罕見,不得不復(fù)制數(shù)據(jù)是十分令人失望的。由于這種情況下sendfile和mmap都是不可用的,在未來內(nèi)核版本中提供sendfile64,將會提供很大的幫助。
結(jié)論
盡管有一些缺點,"零拷貝"sendfile是一個很有用的特性。我希望讀者認為本文提供了足夠的信息以開始在程序中使用sendfile。如果你對這個主題有更深層次的興趣,敬請期待我的第二篇文章——"Zero Copy II: Kernel Perspective",在其中將更深一步的講述"零拷貝"的內(nèi)核內(nèi)部實現(xiàn)
總結(jié)
以上是生活随笔為你收集整理的sendfile详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 掌握这些PDF转Word方法,从此你就是
- 下一篇: 易学难精的Python入门之前应该了解哪