零拷貝:用戶態視角

在Linux系統愈來愈多的人據說過所謂的零拷貝技術,可是我常常遇到不少對這個名詞沒有徹底理解的人。所以,我決定寫一些文章,深挖這個問題,但願能揭開這個有用的特性。在這篇文章,咱們從用戶態角度來看零拷貝,因此特地忽略大量內核細節。linux

什麼是零拷貝?

爲了更好的理解解決問題的方法,咱們首先須要理解問題自己。讓咱們看下網絡服務器將存儲的文件經過網絡發送給客戶端涉及的簡單流程,下面是簡單的代碼示例:緩存

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

看起來很簡單,你應該認爲這裏只有兩次系統調用。實際上,這遠遠不是事實。在這兩次調用之間,數據至少被複制了四次,而且執行了不少次用戶/內核之間的上下文切換(這個過程很複雜,我想簡單說下)。爲了更好的瞭解涉及的工程,請看圖1。頂部顯示上下文切換,底部顯示覆制操做。bash

第一步:讀取系統的調用致使用戶態到內核態的切換,第一次複製由DMA引擎執行,從磁盤讀取內容並存儲到內核空間的緩衝區。服務器

第二部:數據從內核緩衝區複製到用戶緩衝區,並返回讀取系統調用,從執行返回致使內核態切換回用戶態。如今數據存儲在用戶地址空間的緩衝區,能夠按照這種方法往下進行了。網絡

第三部:寫入系統調用致使從用戶態到內核態的上下文切換,第三次複製是將數據再一次放入內核地址空間緩衝區。可是這一次,數據被放到一個不一樣的緩衝區,一個和Socket關聯的緩衝區。異步

第四部:寫入系統執行返回,建立咱們的第四次上下文切換。獨立且異步,第四次複製經過DMA引擎將數據從內核緩衝區傳到協議引擎。你也許會問本身,什麼是獨立且異步?是否是在執行返回以前傳輸了數據?實際上執行返回,不能保證數據傳輸,它甚至不能保證傳輸的開始,只是意味着以太驅動程序隊列有空閒的描述符並能夠接收傳輸的數據。在咱們前面可能有不少數據包在排隊,除非驅動 / 硬件支持優先級響應或者隊列,數據會按照先進先出次序傳輸(圖1的DMA複製說明了最後一次複製事實上能夠延遲)。socket

正如所見,實際上並不須要大量的數據複製,能夠消除一些重複用來減小開銷並提升執行效率。做爲一名驅動工程師,我在工做中使用過一些具備高級特性的硬件。有的硬件能夠繞過主內存直接傳輸數據到另外一臺設備。這個特性消除了系統內存之間的複製,這是個好事,但不是全部的硬件都支持這個特性。這裏還存在磁盤數據爲網絡傳輸從新打包的問題,引入一些複雜度。爲了節省開銷,咱們從消除內核和用戶緩衝區之間的複製開始。tcp

消除複製的一種方法是跳過系統調用並用mmap調用代替。例如:函數

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

爲了更好的理解涉及的過程,圖2的上下文切換保持不變。編碼

mmap調用

第一步:mmap調用會觸發文件內容由DMA引擎複製到內存緩衝區。而後和用戶進程共享緩衝區,內核和用戶內存之間不執行任何複製操做。

第二步:寫入系統會觸發內核從原始內核緩衝區複製數據到與套接字關聯的內核緩衝區。

第三步:第三次複製發生在DMA引擎將數據從內核套接字緩衝區到協議引擎。

經過使用mmap代替讀取,咱們能夠減小一半內核複製數據量。當大量數據複製時會獲得至關好的結果。固然,這種改進並不是沒有代價,使用mmap+寫入方法存在隱藏的陷阱。當內存映射一個文件而後調用另外一個進程截取同一個文件時調用write方法時就會陷入其中一個。因爲執行了錯誤的內存訪問,你的寫入系統調用會被總線錯誤信號SIGBUS中斷。爲這個信號設置的默認行爲時殺死進程並記錄核心數據-而不是大多數網絡服務器但願的那樣。有兩種方法能夠解決這個問題。

第一種方案是爲SIGBUS信號安裝信號處理程序,而後在處理程序中簡單的低矮用return方法。這樣作的話,寫入系統會在中斷以前寫入一部分字節並設置異常爲成功。在我看來這是個很差的解決方案,治標不治本。由於SIGBUS信號表示進程發生了嚴重的錯誤,我不鼓勵這樣處理。

第二種方案涉及到內核的文件租用(微軟稱做「opportunistic locking」)。這是解決問題的正確方法。經過在文件描述符使用租用,能夠在特定文件上使用內核。你能夠從內核租借讀/寫操做。當你在傳輸時另外一個進程嘗試截取文件時,內核會爲你發送一條實時信號。在程序訪問非法地址以前,你的寫入調用會被中斷並被SIGBUS信號殺掉。中斷以前會返回傳輸的字節數,error會被設置爲成功。這裏有個從內核調用租約的例子:

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;
}

你能夠在獲取文件以前得到租約,並在完成操做後終止。這是經過租約類型F_UNLCK調用fcntl F_SETLEASE實現的。

在2.1的內核版本,爲了簡化網絡和兩個本地文件之間的簡單調用引入了文件發送系統。引入不只僅爲了下降數據複製,也是爲了減小上下文切換。使用方法以下:

sendfile(socket, file, len);

爲了更好的理解涉及的過程,請查看圖3

用Sendfile代替讀寫

第一步:文件發送系統調用會經過DMA引擎複製文件內容到內核緩衝區。而後內核將數據複製到和套接字相關聯的內核緩衝區。

第二步:第三次複製發生在DMA引擎從內核緩衝區傳送數據到協議引擎。 你可能會想若是咱們使用文件發送系統傳輸數據時另外一個進程截取文件會發生什麼。若是咱們沒有註冊信號處理程序,文件發送程序會在中斷以前返回已經發送的字節數嗎,error會被設置爲成功。 若是咱們在調用文件發送程序以前從內核得到租約,不管如何,精準返回狀態時相同的。咱們也能夠在遞送會回以前獲取RT_SIGNAL_LEASE信號。 到如今爲止,咱們可以避免內核生成多個重複副本,可是咱們仍有一個副本。這個也能夠避免嗎?固然,藉助硬件的一點幫助。爲了消除內核的數據複製,咱們須要支持蒐集操做的網絡接口。這僅僅意味着等待傳輸時不須要連續內存,能夠分散到多個內存位置。在2.4的內核版本,修改了套接字緩衝區描述符來適應這些請求-也就是linux所說的零拷貝。這種方法不只減小了屢次上下文切換,也消除了處理器之間的數據複製,所以代碼以下:

sendfile(socket, file, len);

爲了更好的理解涉及的過程,請查看圖4.

支持收集的硬件能夠從多個內存位置組裝數據,從而消除了其它複製

第一步:文件發送系統調用觸發DMA引擎將文件內容複製到內核緩衝區。

第二步:沒有須要複製到套接字緩衝區的數據,相反,只有文件的地址和長度相關的描述符追加到套接字緩衝區。DMA引擎將內核緩衝區的數據直接傳輸到協議引擎,這樣消除了保留的最後複製。 由於數據實際上仍從磁盤到內存,從內存到線路,一些人認爲這不是真正的零拷貝。從操做系統的角度來看這就是零拷貝,由於內核緩衝區之間的數據沒有重複。當使用零拷貝除了避免複製發生外,還有其它操做的好處,好比更少的上下文切換,更少的CPU數據緩存污染,也不須要CPU校驗和計算。

如今咱們知道零拷貝是什麼了,讓咱們寫一些代碼實踐下理論。你能夠從ww.xalien.org/articles/source/sfl-src.tgz中下載所有的源碼。要解壓縮源代碼,請在提示符下鍵入tar -zxvf sfl-src.tgz。要編譯代碼並建立隨機數據文件data.bin,運行make。

查看以頭文件開頭的代碼

/* 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 */

除了<sys/socket.h>和<netinet/in.h>須要的基本套接字操做,咱們須要文件發送調用的原型定義。能夠在<sys / sendfile.h>中找到:

/ *咱們發送或接收* /
if(argv [1] [0] =='s')is_server ++;
/ *開放描述符* /
sd = socket(PF_INET,SOCK_STREAM,0);
if(is_server)fd = open(「data.bin」,O_RDONLY);

相同的程序能夠充當服務端/發送者或客戶端/接收者。咱們須要檢查命令提示符其中的參數,而後設置標誌is_server來運行發送者模式。咱們也能夠打開INET協議族的套接字流。做爲服務器模式的組成部分咱們須要傳送到客戶端一些數據類型,素以咱們打開數據文件。使用文件發送系統來傳輸數據,咱們不須要讀取文件實際內容並存儲帶咱們的程序內存緩衝區。這是服務器地址:

/* 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地址。服務器的地址經過命令行參數傳遞。端口數字是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);
    }

做爲服務器,咱們須要設置套接字描述符的地址,經過系統調用bind實現,爲套接字描述符(sd)設定服務器地址(sa):

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

因爲咱們使用套接字流,須要聲明咱們願意接收鏈接並設置鏈接隊列的大小。我已將積壓隊列大小設置爲1,不過爲了應答已經創建的鏈接,一般會將積壓隊列設置的更高一點。在內核的舊版本,積壓隊列被用來防止syn flood攻擊。由於系統調用只能監聽到肯定鏈接的參數修改。內核參數tcp_max_syn_backlog接管了保護系統不受syn flood 攻擊的角色:

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

系統調用accept從掛起的鏈接隊列上第一個鏈接請求建立新的鏈接套接字。返回值只是新建立鏈接的描述符;套接字如今已經準備好讀取,寫入或者輪詢/選擇系統調用:

if((cnt = sendfile(client,fd,&off,
                          BUFF_SIZE)) < 0){
    perror("sendfile");
    exit(errno);
}
printf("Server sent %d bytes.\n", cnt);
close(client);

在客戶端套接字描述符創建鏈接後,咱們開始傳輸數據到遠程系統。咱們作的只是調用文件發送系統,在Linux下經過如下原型實現:

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

開始的兩個參數都是文件描述符,第三個參數指的是發送文件的起點,第四個參數是咱們想傳輸的字節數。爲了在數據傳輸時使用零拷貝,你須要從網卡獲取內存蒐集操做支持,還須要爲協議提供校驗能力,好比TCP或者UDP。若是你的NIC已通過期且不支持這些特性,你仍須要使用sendfile傳輸文件。不一樣點在於內核在傳輸以前會合並緩衝區。

常見問題

文件發送系統調用的一個問題是缺乏標準實現,正如開放系統調用,文件發送的實如今Linux,Solaris或者HP-UX都不相同。這會給開發者在他們網絡數據傳輸代碼中使用零拷貝帶來問題。

第二個差別是linux不支持向量傳輸,Solaris和HP-UX的sendfile爲了消除爲數據傳輸準備的頭部信息,須要額外的參數。

前景展望

linux下實現的零拷貝還遠未完成而且極可能在不久發生變化。更多的函數湖北添加。好比sendfile調用並不支持向量傳輸,像Samba和Apache這樣的服務器必須使用設置了TCP_CORK標誌實現多個sendfile調用。這個標誌告訴系統下一次sendfile調用會有更多的數據經過。TCP-CORK也與TCP_NODELAY不兼容,而且在咱們想要在數據前添加或附加標頭時使用。這是一個完美的例子,其中向量調用將消除對當前實現所強制的多個sendfile調用和延遲的須要。

還有一個使人不快的限制是當前的sendfile不支持超過2GB的數據傳輸。這個大小在當前很常見,而且用這種方法複製全部一樣數據使人失望。因爲sendfile和mmap方法在這種場景下不適用,sendfile64會在將來內核版本中使用。

結尾

儘管有一些缺點,零複製sendfile是一個有用的功能,我但願你已經發現這篇文章的信息足以開始在你的程序中使用它.

翻譯自https://www.linuxjournal.com/article/6345.

相關文章
相關標籤/搜索