到目前爲止,每一個人都據說過Linux下所謂的零拷貝功能,但我遇到有些人對這個主題沒有徹底理解,正由於如此,我決定寫幾篇文章來更深刻研究下這個問題,但願可以闡明這個有用的特性;這本文中,咱們將從用戶模式的應用程序角度來看看零拷貝,故省去複雜的內核級別細節。api
什麼是零拷貝?緩存
爲了更好的理解問題的解決方案,咱們須要首先來理解下問題自己,讓咱們來看看網絡客戶端下載存儲在dæmon服務器的一個文件的簡單過程,下面是一些實例代碼:服務器
read(file, tmp_buf, len); write(socket, tmp_buf, len);
看起來很簡單,你可能認爲只有兩個系統調用並無太多的開銷,實際上這與實際狀況相差甚遠。在這兩個調用背後,數據至少被複制了四次,而且執行了幾乎數量同樣多的用戶/內核上下文的切換(實際上這個過程還要更加複雜,但這裏我只想保持簡單),爲了更好的瞭解涉及的過程,請看圖1,上半部分表示上下文切換,下半部分表示數據複製操做網絡
圖1所示,兩個系統調用過程當中的數據複製。異步
第一步: 讀系統調用致使用戶空間切換到內核空間,第一次數據複製由DMA引擎執行,該引擎讀取文件內容而且存儲到內核地址空間緩衝區socket
第二步:數據從內核緩衝區複製到用戶緩衝區,而後讀系統調用返回。調用返回致使上下文從內核切換回用戶模式,如今數據存儲在用戶地址空間的緩衝區,而且能夠再次開始向下複製數據。性能
第三步:寫系統調用致使上下文從用戶模式切換到內核模式。第三次數據複製是再次執行把數據複製到內核地址空間的緩衝區,不過這一次,數據被放到了不一樣的緩衝區,這個緩衝區是跟套接字相關聯的。spa
第四步:寫系統調用返回,創造了第四次上下文切換。第四次數據複製是由DMA引擎獨立、異步的從內核緩衝區傳遞到協議引擎。你可能會問本身,獨立和異步是什麼意思?數據不是在系統調用以前傳輸的嗎?系統調用返回,實際上並不能保證傳輸,甚至不能保證傳輸的開始。這僅僅意味着以太網驅動程序在隊列中有空閒的描述符,而且接受了咱們的數據進行傳輸,在咱們以前可能有不少的數據包在排隊,除非驅動程序/硬件實現優先級的環或隊列,不然數據是以先到先出的方式傳輸的(上圖中DMA複製說明了最後一個複製能夠延遲的事實)操作系統
正如您看到的,不少的數據複製並非須要的,能夠消除一些重複的複製,以減小開銷並提升性能;做爲一個驅動程序開發人員,我使用一些硬件的高級特性,能夠徹底繞開主存儲器直接傳輸數據到另外一個設備,這個特性消除了系統內存中的數據複製,是一個好東西,但不是全部的硬件都支持它。還存在磁盤數據必須從新轉換成網絡數據的問題,這帶來了一些複雜性;爲了消除開銷,咱們能夠從消除內核與用戶緩衝區之間的一些數據複製開始。code
消除複製的一種方法就是跳過read
調用,轉而調用mmap
,列如:
tmp_buf = mmap(file, len); write(socket, tmp_buf, len);
爲了更好的瞭解所涉及的過程,請看圖2,上下文切換保持不變。
圖2,mmap調用
第一步:mmap系統調用致使文件內容被DMA引擎複製到內核緩衝區中。而後與用戶進程共享緩衝區,而不須要在內核和用戶內存空間之間執行任何數據複製。
第二步:寫系統調用使內核將原始內核緩衝區中的數據複製到與套接字關聯的內核緩衝區中。
第三步:第三次複製發生在DMA引擎將數據從內核套接字緩衝區傳遞到協議引擎時。
使用mmap
代替read
,咱們將內核的數據複製減小了一半,這在傳輸大量數據時產生了至關好的效果,然而這種改進並非沒有代價的,使用mmap+write方式存在一些隱藏的缺陷。當您在內存映射了一個文件時, 這時若是正好有一個進程使用write
修改了這個文件使之變小了,這時有可能會訪問到映射文件以外的內存,進程將收到SIGBUS信號而退出,這不是網絡服務器最理想的操做,有兩種方法能夠解決這個問題 。
第一種方法是爲SIGBUS信號安裝一個信號處理程序,而後在處理程序中簡單地調用return
。經過這樣作,write系統調用返回它在被中斷以前所寫的字節數,errno設置爲成功。讓我指出,這將是一個壞的解決方案,只看到了問題的表面而沒有解決問題的本質,由於SIGBUS信號代表這個過程出了嚴重問題,因此我不鼓勵將此做爲解決方案使用。
第二種解決方案涉及從內核中租借文件(在Microsoft Windows中稱爲「opportunistic locking」)。這是解決這個問題的正確方法。經過在文件描述符上使用租借,能夠在特定文件上使用內核。而後,您能夠從內核請求讀/寫租約。當另外一個進程試圖截斷您要傳輸的文件時,內核會向您發送實時信號RT_SIGNAL_LEASE信號。它告訴您,內核正在破壞該文件上的寫或讀租約。在程序訪問無效地址並被SIGBUS信號殺死以前,寫調用被中斷。write調用的返回值是在中斷以前寫入的字節數,errno將被設置爲success。下面是一些示例代碼,展現瞭如何從內核得到租約:
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; }
您應該在mmaping
文件以前得到您的租約,並在完成以後破壞您的租約。這是經過使用F_UNLCK
的租賃類型調用fcntl F_SETLEASE
來實現的。
Sendfile
在內核版本2.1中,引入了sendfile系統調用,以簡化經過網絡和兩個本地文件之間的數據傳輸。sendfile的引入不只減小了數據複製,也減小了上下文切換。像這樣使用它:
sendfile(socket, file, len);
爲了更好地瞭解所涉及的流程,請看圖3
圖3,用Sendfile代替讀和寫
第一步:sendfile系統調用致使文件內容被DMA引擎複製到內核緩衝區中。而後內核將數據複製到與套接字關聯的內核緩衝區中。
第二步:第三次複製發生在DMA引擎將數據從內核套接字緩衝區傳遞到協議引擎時。
您可能想知道,若是另外一個進程截斷了咱們經過sendfile系統調用傳輸的文件,會發生什麼狀況。若是咱們不註冊任何信號處理程序,sendfile調用只返回它在中斷以前傳輸的字節數,errno將被設置爲成功。
可是,若是在調用sendfile以前從內核得到文件的租約,則行爲和返回狀態徹底相同。在sendfile調用返回以前,咱們還獲得了RT_SIGNAL_LEASE
信號。
到目前爲止,咱們已經可以避免讓內核複製幾個副本,可是仍然只剩下一個副本。這也能避免嗎?固然,在硬件的幫助下。爲了消除內核所作的全部數據重複,咱們須要一個支持收集操做的網絡接口。這僅僅意味着等待傳輸的數據不須要在連續內存中;它能夠分散在不一樣的內存位置。在內核版本2.4中,套接字緩衝區描述符被修改,以適應這些需求——Linux下稱爲零拷貝。這種方法不只減小了多個上下文切換,還消除了處理器所作的數據重複。對於用戶級應用程序,沒有任何變化,因此代碼仍然是這樣的:
sendfile(socket, file, len);
爲了更好地瞭解所涉及的流程,請看圖4
圖4,支持gather的硬件能夠從多個內存位置組裝數據,從而消除另外一個副本
第一步:sendfile系統調用致使文件內容被DMA引擎複製到內核緩衝區中。
第二步:沒有數據複製到套接字緩衝區中。相反,只有包含有關數據位置和長度信息的描述符被附加到套接字緩衝區,DMA引擎直接將數據從內核緩衝區傳遞到協議引擎,從而消除了剩餘的最終副本。
由於數據實際上仍然是從磁盤複製到內存,從內存寫出去,有些人可能會說這不是一個真正的零拷貝。可是,從操做系統的角度來看,這是零副本,由於數據不會在內核緩衝區之間重複。當使用零副本時,除了避免複製以外,還能夠得到其餘性能優點,例如更少的上下文切換、更少的CPU數據緩存污染和沒有CPU校驗和計算。
Linux下的zero copy的實現還遠未完成,在不久的未來可能會發生變化。應該添加更多的功能。例如,sendfile調用不支持向量傳輸,服務器(如Samba和Apache)必須使用多個sendfile調用並設置TCP_CORK標誌。TCP_CORK也與TCP_NODELAY不兼容,當咱們想給數據添加頭信息時使用。這是一個很好的例子,說明了一個vectored調用能夠消除對多個sendfile調用的需求和當前實現強制執行的延遲。
當前sendfile中一個至關使人不快的限制是,在傳輸大於2GB的文件時不能使用它。這樣大的文件在今天並很多見,並且在退出時必須複製全部的數據是至關使人失望的。由於sendfile和mmap方法在本例中都不可用,因此sendfile64在將來的內核版本中很是有用。
結論
儘管有一些缺點,可是zero-copy sendfile是一個有用的特性,我但願您已經發現本文提供了足夠的信息,能夠開始在您的程序中使用它