可算是有文章,把Linux零拷貝講透徹了!

擊上方“朱小廝的博客”,選擇“設爲星標”

後臺回覆"加羣",加入組織

來源:22j.co/brVn

本文探討 Linux 中主要的幾種零拷貝技術以及零拷貝技術的適用場景。爲了迅速建立起零拷貝的概念,我們拿一個常用的場景進行引入。在寫一個服務端程序時(Web Server或者文件服務器),文件下載是一個基本功能。

這時候服務端的任務是:將服務端主機磁盤中的文件不做修改地從已連接的 Socket 發出去。

我們通常用下面的代碼完成:

while((n = read(diskfd, buf, BUF_SIZE)) > 0)
    write(sockfd, buf , n);

基本操作就是循環的從磁盤讀入文件內容到緩衝區,再將緩衝區的內容發送到 Socket。但是由於 Linux 的 I/O 操作默認是緩衝 I/O。

這裏面主要使用的也就是 Read 和 Write 兩個系統調用,我們並不知道操作系統在其中做了什麼。實際上在以上 I/O 操作中,發生了多次的數據拷貝。

當應用程序訪問某塊數據時,操作系統首先會檢查,是不是最近訪問過此文件,文件內容是否緩存在內核緩衝區。

如果是,操作系統則直接根據 Read 系統調用提供的 buf 地址,將內核緩衝區的內容拷貝到 buf 所指定的用戶空間緩衝區中去。

如果不是,操作系統則首先將磁盤上的數據拷貝的內核緩衝區,這一步目前主要依靠 DMA 來傳輸,然後再把內核緩衝區上的內容拷貝到用戶緩衝區中。

接下來,Write 系統調用再把用戶緩衝區的內容拷貝到網絡堆棧相關的內核緩衝區中,最後 Socket 再把內核緩衝區的內容發送到網卡上。

說了這麼多,不如看圖清楚:

數據拷貝

從上圖中可以看出,共產生了四次數據拷貝,即使使用了 DMA 來處理了與硬件的通訊,CPU 仍然需要處理兩次數據拷貝。

與此同時,在用戶態與內核態也發生了多次上下文切換,無疑也加重了 CPU 負擔。

在此過程中,我們沒有對文件內容做任何修改,那麼在內核空間和用戶空間來回拷貝數據無疑就是一種浪費,而零拷貝主要就是爲了解決這種低效性。

什麼是零拷貝技術(zero-copy)?

零拷貝主要的任務就是避免 CPU 將數據從一塊存儲拷貝到另外一塊存儲。

主要就是利用各種零拷貝技術,避免讓 CPU 做大量的數據拷貝任務,減少不必要的拷貝,或者讓別的組件來做這一類簡單的數據傳輸任務,讓 CPU 解脫出來專注於別的任務。這樣就可以讓系統資源的利用更加有效。

我們繼續回到上文中的例子,我們如何減少數據拷貝的次數呢?一個很明顯的着力點就是減少數據在內核空間和用戶空間來回拷貝,這也引入了零拷貝的一個類型:讓數據傳輸不需要經過 user space。

使用 mmap

我們減少拷貝次數的一種方法是調用 mmap() 來代替 read 調用:

buf = mmap(diskfd, len);
write(sockfd, buf, len);

應用程序調用 mmap(),磁盤上的數據會通過 DMA 被拷貝的內核緩衝區,接着操作系統會把這段內核緩衝區與應用程序共享,這樣就不需要把內核緩衝區的內容往用戶空間拷貝。

應用程序再調用 write(),操作系統直接將內核緩衝區的內容拷貝到 Socket 緩衝區中,這一切都發生在內核態,最後,Socket 緩衝區再把數據發到網卡去。

同樣的,看圖很簡單:

mmap

使用 mmap 替代 Read 很明顯減少了一次拷貝,當拷貝數據量很大時,無疑提升了效率。

但是使用 mmap 是有代價的。當你使用 mmap 時,你可能會遇到一些隱藏的陷阱。

例如,當你的程序 map 了一個文件,但是當這個文件被另一個進程截斷 (truncate) 時,Write 系統調用會因爲訪問非法地址而被 SIGBUS 信號終止。

SIGBUS 信號默認會殺死你的進程併產生一個 coredump,如果你的服務器這樣被中止了,那會產生一筆損失。

通常我們使用以下解決方案避免這種問題:

①爲 SIGBUS 信號建立信號處理程序

當遇到 SIGBUS 信號時,信號處理程序簡單地返回,Write 系統調用在被中斷之前會返回已經寫入的字節數,並且 errno 會被設置成 success,但是這是一種糟糕的處理辦法,因爲你並沒有解決問題的實質核心。

②使用文件租借鎖

通常我們使用這種方法,在文件描述符上使用租借鎖,我們爲文件向內核申請一個租借鎖。

當其他進程想要截斷這個文件時,內核會向我們發送一個實時的 RTSIGNALLEASE 信號,告訴我們內核正在破壞你加持在文件上的讀寫鎖。

這樣在程序訪問非法內存並且被 SIGBUS 殺死之前,你的 Write 系統調用會被中斷。Write 會返回已經寫入的字節數,並且置 errno 爲 success。

我們應該在 mmap 文件之前加鎖,並且在操作完文件後解鎖:

if(fcntl(diskfd, F_SETSIG, RT_SIGNAL_LEASE) == -1) {
    perror("kernel lease set signal");
    return -1;
}
/* l_type can be F_RDLCK F_WRLCK  加鎖*/
/* l_type can be  F_UNLCK 解鎖*/
if(fcntl(diskfd, F_SETLEASE, l_type)){
    perror("kernel lease set type");
    return -1;
}

使用 sendfile

從 2.1 版內核開始,Linux 引入了 sendfile 來簡化操作:

#include<sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

系統調用 sendfile() 在代表輸入文件的描述符 infd 和代表輸出文件的描述符 outfd 之間傳送文件內容(字節)。

描述符 outfd 必須指向一個套接字,而 infd 指向的文件必須是可以 mmap 的。

這些侷限限制了 sendfile 的使用,使 sendfile 只能將數據從文件傳遞到套接字上,反之則不行。

使用 sendfile 不僅減少了數據拷貝的次數,還減少了上下文切換,數據傳送始終只發生在 kernel space。

sendfile 系統調用過程

在我們調用 sendfile 時,如果有其它進程截斷了文件會發生什麼呢?假設我們沒有設置任何信號處理程序,sendfile 調用僅僅返回它在被中斷之前已經傳輸的字節數,errno 會被置爲 success。

如果我們在調用 sendfile 之前給文件加了鎖,sendfile 的行爲仍然和之前相同,我們還會收到 RTSIGNALLEASE 的信號。

目前爲止,我們已經減少了數據拷貝的次數了,但是仍然存在一次拷貝,就是頁緩存到 Socket 緩存的拷貝。那麼能不能把這個拷貝也省略呢?

藉助於硬件上的幫助,我們是可以辦到的。之前我們是把頁緩存的數據拷貝到 Socket 緩存中。

實際上,我們僅僅需要把緩衝區描述符傳到 Socket 緩衝區,再把數據長度傳過去,這樣 DMA 控制器直接將頁緩存中的數據打包發送到網絡中就可以了。

總結一下:sendfile 系統調用利用 DMA 引擎將文件內容拷貝到內核緩衝區去,然後將帶有文件位置和長度信息的緩衝區描述符添加 Socket 緩衝區去。

這一步不會將內核中的數據拷貝到 Socket 緩衝區中,DMA 引擎會將內核緩衝區的數據拷貝到協議引擎中去,避免了最後一次拷貝。

帶 DMA 的 sendfile

不過這一種收集拷貝功能是需要硬件以及驅動程序支持的。

使用 splice

sendfile 只適用於將數據從文件拷貝到套接字上,限定了它的使用範圍。

Linux 在 2.6.17 版本引入 splice 系統調用,用於在兩個文件描述符中移動數據:

#define _GNU_SOURCE         /* See feature_test_macros(7) */
#include <fcntl.h>
ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags);

splice 調用在兩個文件描述符之間移動數據,而不需要數據在內核空間和用戶空間來回拷貝。

他從 fdin 拷貝 len 長度的數據到 fdout,但是有一方必須是管道設備,這也是目前 splice 的一些侷限性。

flags 參數有以下幾種取值:

  • SPLICEFMOVE:嘗試去移動數據而不是拷貝數據。這僅僅是對內核的一個小提示:如果內核不能從 pipe 移動數據或者 pipe 的緩存不是一個整頁面,仍然需要拷貝數據。

    Linux 最初的實現有些問題,所以從 2.6.21 開始這個選項不起作用,後面的 Linux 版本應該會實現。

  • SPLICEFNONBLOCK:splice 操作不會被阻塞。然而,如果文件描述符沒有被設置爲不可被阻塞方式的 I/O ,那麼調用 splice 有可能仍然被阻塞。

  • SPLICEFMORE:後面的 splice 調用會有更多的數據。

splice 調用利用了 Linux 提出的管道緩衝區機制, 所以至少一個描述符要爲管道。

以上幾種零拷貝技術都是減少數據在用戶空間和內核空間拷貝技術實現的,但是有些時候,數據必須在用戶空間和內核空間之間拷貝。

這時候,我們只能針對數據在用戶空間和內核空間拷貝的時機上下功夫了。

Linux 通常利用寫時複製(copy on write)來減少系統開銷,這個技術又時常稱作 COW。

由於篇幅原因,本文不詳細介紹寫時複製。大概描述下就是:如果多個程序同時訪問同一塊數據,那麼每個程序都擁有指向這塊數據的指針,在每個程序看來,自己都是獨立擁有這塊數據的。

只有當程序需要對數據內容進行修改時,纔會把數據內容拷貝到程序自己的應用空間裏去。

這時候,數據才成爲該程序的私有數據。如果程序不需要對數據進行修改,那麼永遠都不需要拷貝數據到自己的應用空間裏,這樣就減少了數據的拷貝。

除此之外,還有一些零拷貝技術,比如傳統的 Linux I/O 中加上 O_DIRECT 標記可以直接 I/O,避免了自動緩存,還有尚未成熟的 fbufs 技術,本文尚未覆蓋所有零拷貝技術,只是介紹常見的一些,如有興趣,可以自行研究。

一般成熟的服務端項目也會自己改造內核中有關 I/O 的部分,提高自己的數據傳輸速率。

想知道更多?描下面的二維碼關注我

後臺回覆”加羣“獲取公衆號專屬羣聊入口

【精彩推薦】

點個在看少個 bug ????

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章