本文探討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呼叫:<pre class="brush:php;toolbar:false">buf = mmap(diskfd, len);
write(sockfd, buf, len);</pre>
應用程式呼叫mmap()
,磁碟上的資料會透過DMA
被拷貝的核心緩衝區,接著作業系統會把這段核心緩衝區與應用程式共享,這樣就不需要把核心緩衝區的內容往用戶空間拷貝。應用程式再呼叫write()
,作業系統直接將核心緩衝區的內容拷貝到socket
緩衝區中,這一切都發生在內核態,最後,
socket
同樣的,看圖很簡單:
使用mmap替代read很明顯減少了一次拷貝,當拷貝資料量很大時,無疑提升了效率。但是使用
mmap
是有代價的。當你使用mmap
時,你可能會遇到一些隱藏的陷阱。例如,當你的程式map
了一個文件,但是當這個文件被另一個進程截斷(truncate)時, write系統呼叫會因為存取非法位址而被SIGBUS
訊號終止。 SIGBUS
訊號預設會殺死你的行程並產生一個
通常我们使用以下解决方案避免这种问题:
1、为SIGBUS信号建立信号处理程序
当遇到SIGBUS
信号时,信号处理程序简单地返回,write
系统调用在被中断之前会返回已经写入的字节数,并且errno
会被设置成success,但是这是一种糟糕的处理办法,因为你并没有解决问题的实质核心。
2、使用文件租借锁
通常我们使用这种方法,在文件描述符上使用租借锁,我们为文件向内核申请一个租借锁,当其它进程想要截断这个文件时,内核会向我们发送一个实时的RT_SIGNAL_LEASE
信号,告诉我们内核正在破坏你加持在文件上的读写锁。这样在程序访问非法内存并且被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> ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);</sys>
系统调用sendfile()
在代表输入文件的描述符in_fd
和代表输出文件的描述符out_fd
之间传送文件内容(字节)。描述符out_fd
必须指向一个套接字,而in_fd
指向的文件必须是可以mmap
的。这些局限限制了sendfile
的使用,使sendfile
只能将数据从文件传递到套接字上,反之则不行。
使用sendfile
不仅减少了数据拷贝的次数,还减少了上下文切换,数据传送始终只发生在kernel space
。
在我们调用sendfile
时,如果有其它进程截断了文件会发生什么呢?假设我们没有设置任何信号处理程序,sendfile
调用仅仅返回它在被中断之前已经传输的字节数,errno
会被置为success。如果我们在调用sendfile之前给文件加了锁,sendfile
的行为仍然和之前相同,我们还会收到RT_SIGNAL_LEASE的信号。
目前为止,我们已经减少了数据拷贝的次数了,但是仍然存在一次拷贝,就是页缓存到socket缓存的拷贝。那么能不能把这个拷贝也省略呢?
借助于硬件上的帮助,我们是可以办到的。之前我们是把页缓存的数据拷贝到socket缓存中,实际上,我们仅仅需要把缓冲区描述符传到socket
缓冲区,再把数据长度传过去,这样DMA
控制器直接将页缓存中的数据打包发送到网络中就可以了。
总结一下,sendfile
系统调用利用DMA
引擎将文件内容拷贝到内核缓冲区去,然后将带有文件位置和长度信息的缓冲区描述符添加socket缓冲区去,这一步不会将内核中的数据拷贝到socket缓冲区中,DMA
引擎会将内核缓冲区的数据拷贝到协议引擎中去,避免了最后一次拷贝。
不过这一种收集拷贝功能是需要硬件以及驱动程序支持的。
使用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);</fcntl.h>
splice调用在两个文件描述符之间移动数据,而不需要数据在内核空间和用户空间来回拷贝。他从fd_in
拷贝len
长度的数据到fd_out
,但是有一方必须是管道设备,这也是目前splice
的一些局限性。flags
参数有以下几种取值:
pipe
移動資料或pipe
的快取不是一個整頁,仍然需要拷貝資料。 Linux最初的實作有些問題,所以從2.6.21
開始這個選項不起作用,後面的Linux版本應該會實作。 splice
操作不會被封鎖。然而,如果檔案描述子沒有被設定為不可被阻塞方式的 I/O ,那麼呼叫 splice 有可能仍然被阻塞。 splice
呼叫會有更多的資料。 splice呼叫利用了Linux提出的管道緩衝區機制, 所以至少一個描述符要為管道。
以上幾種零拷貝技術都是減少資料在使用者空間和核心空間拷貝技術實現的,但是有些時候,資料必須在使用者空間和核心空間之間拷貝。這時候,我們只能針對資料在使用者空間和核心空間拷貝的時機上下功夫了。 Linux通常利用寫時複製(copy on write)來減少系統開銷,這個技術又時常稱為COW
。
由於篇幅原因,本文不詳細介紹寫時複製。大概描述下就是:如果多個程式同時存取同一塊數據,那麼每個程式都擁有指向這塊數據的指針,在每個程式看來,自己都是獨立擁有這塊數據的,只有當程式需要對資料內容進行修改時,才會把資料內容拷貝到程式自己的應用程式空間裡去,這時候,資料才變成該程式的私有資料。如果程式不需要對資料進行修改,那麼永遠都不需要拷貝資料到自己的應用空間。這樣就減少了資料的拷貝。寫時複製的內容可以再寫一篇文章了。 。 。
除此之外,還有一些零拷貝技術,例如傳統的Linux I/O中加上O_DIRECT
標記可以直接I/O
,避免了自動緩存,還有尚未成熟的fbufs
技術,本文尚未涵蓋所有零拷貝技術,只是介紹常見的一些,如有興趣,可以自行研究,一般成熟的服務端項目也會自己改造內核中有關I/O的部分,提高自己的資料傳輸速率。
推薦教學:《Linux運維》
以上是談談Linux的幾種零拷貝技術和適用的場景的詳細內容。更多資訊請關注PHP中文網其他相關文章!