深刻了解Netty【二】零拷貝


引言

如下翻譯自:Zero Copy I: User-Mode Perspectivehtml

零拷貝是什麼?linux

爲了更好地理解問題的解決方案,咱們首先須要理解問題自己。讓咱們來看看什麼是參與網絡服務器的簡單過程dæmon服務數據存儲在一個文件經過網絡客戶端。下面是一些示例代碼:緩存

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

看起來很簡單;您會認爲只有這兩個系統調用不會帶來太多開銷。事實上,這與事實相去甚遠。在這兩個調用以後,數據至少複製了四次,而且幾乎執行了相同數量的用戶/內核上下文切換。(實際上這個過程要複雜得多,但我想讓它保持簡單)。爲了更好地瞭解所涉及的流程,請看圖1。頂部顯示上下文切換,底部顯示覆制操做。服務器

Copying in Two Sample System Calls.jpg

圖1。複製兩個示例系統調用網絡

第一步:read系統調用致使上下文從用戶模式切換到內核模式。第一個副本由DMA引擎執行,它從磁盤讀取文件內容並將其存儲到內核地址空間緩衝區中。異步

第二步:將數據從內核緩衝區複製到用戶緩衝區,read系統調用返回。調用的返回致使上下文從內核切換回用戶模式。如今數據存儲在用戶地址空間緩衝區中,它能夠再次開始向下移動。socket

第三步:write系統調用致使上下文從用戶模式切換到內核模式。執行第三次複製,再次將數據放入內核地址空間緩衝區。不過,這一次,數據被放入一個不一樣的緩衝區,一個專門與套接字關聯的緩衝區。性能

第四步:write系統調用返回,建立咱們的第四個上下文切換。當DMA引擎將數據從內核緩衝區傳遞到協議引擎時,會獨立地、異步地進行第四次複製。你可能會問本身,「獨立和異步是什麼意思?」在呼叫返回以前,數據沒有傳輸嗎?「呼叫返回,實際上並不保證傳輸;它甚至不能保證傳輸的開始。它只是意味着以太網驅動程序在它的隊列中有空閒的描述符,而且已經接受咱們的數據進行傳輸。可能有許多包在咱們的前面排隊。除非驅動程序/硬件實現優先級環或隊列,不然數據是在先進先出的基礎上傳輸的。(圖1中分叉的DMA副本演示了最後一個副本能夠延遲的事實)。操作系統

正如您所看到的,不少數據複製實際上並非必要的。能夠消除一些重複,以減小開銷並提升性能。做爲一名驅動程序開發人員,我使用的硬件具備一些很是高級的特性。一些硬件能夠徹底繞過主存,直接將數據傳輸到另外一個設備。這個特性消除了系統內存中的副本,這是一個很好的特性,可是並非全部的硬件都支持它。還有一個問題是來自磁盤的數據必須爲網絡從新打包,這帶來了一些複雜性。爲了消除開銷,咱們能夠從消除內核和用戶緩衝區之間的一些複製開始。翻譯

消除副本的一種方法是跳過調用read,而是調用mmap。例如:

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

爲了更好地瞭解所涉及的流程,請看圖2。上下文切換保持不變。

Calling mmap.jpg

圖2。調用mmap

第一步:mmap系統調用致使DMA引擎將文件內容複製到內核緩衝區。而後與用戶進程共享緩衝區,而不須要在內核和用戶內存空間之間執行任何複製。

第二步:write系統調用致使內核將原始內核緩衝區中的數據複製到與套接字相關的內核緩衝區中。

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

經過使用mmap而不是read,咱們減小了內核必須複製的數據量的一半。當傳輸大量數據時,這將產生至關好的結果。然而,這種改善不是沒有代價的;在使用mmap write方法時存在一些隱藏的陷阱。當您在內存中映射一個文件,而後調用write,而另外一個進程截斷相同的文件時,您將陷入其中之一。您的寫系統調用將被總線錯誤信號SIGBUS中斷,由於您執行了錯誤的內存訪問。該信號的默認行爲是終止進程並轉儲內核——這對於網絡服務器來講不是最理想的操做。有兩種方法能夠解決這個問題。

第一種方法是爲SIGBUS信號安裝一個信號處理程序,而後在處理程序中簡單地調用return。經過這樣作,write系統調用將返回它在被中斷以前寫入的字節數,並將errno設置爲成功。讓我指出,這將是一個糟糕的解決方案,只解決症狀,而不是問題的根源。由於SIGBUS信號代表進程出現了嚴重錯誤,因此我不建議使用它做爲解決方案。

第二種解決方案涉及從內核租用文件(在Microsoft Windows中稱爲「機會鎖定」)。這是解決這個問題的正確方法。經過在文件描述符上使用租借,您能夠對特定文件的內核進行租借。而後能夠從內核請求讀/寫租約。當另外一個進程試圖截斷您正在傳輸的文件時,內核會向您發送實時信號,即RT_SIGNAL_LEASE信號。它告訴您內核正在破壞您對該文件的讀或寫租約。在程序訪問無效地址並被SIGBUS信號終止以前,寫調用被中斷。write調用的返回值是在中斷以前寫入的字節數,errno將被設置爲成功。下面是一些示例代碼,展現瞭如何從內核得到租約:

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實現的。

Sendfile

在內核版本2.1中,引入了sendfile系統調用,以簡化經過網絡和兩個本地文件之間的數據傳輸。sendfile的引入不只減小了數據複製,還減小了上下文切換。像這樣使用它:

sendfile(socket, file, len);

爲了更好地瞭解所涉及的流程,請看圖3。

Replacing Read and Write with Sendfile.jpg

圖3。用Sendfile替換讀和寫

第一步:sendfile系統調用致使DMA引擎將文件內容複製到內核緩衝區。而後,內核將數據複製到與套接字關聯的內核緩衝區中。

步驟2:當DMA引擎將數據從內核套接字緩衝區傳遞到協議引擎時,發生第三次複製。

您可能想知道若是另外一個進程截斷了咱們使用sendfile系統調用傳輸的文件,會發生什麼狀況。若是咱們不註冊任何信號處理程序,sendfile調用只返回它在中斷以前傳輸的字節數,errno將被設置爲成功。

可是,若是咱們在調用sendfile以前從內核得到文件的租約,則行爲和返回狀態是徹底相同的。咱們還將在sendfile調用返回以前得到RT_SIGNAL_LEASE信號。

到目前爲止,咱們已經可以避免讓內核複製幾個副本,可是仍然只剩下一個副本。這也能避免嗎?固然,在硬件的幫助下。爲了消除內核所作的全部數據重複,咱們須要一個支持收集操做的網絡接口。這僅僅意味着等待傳輸的數據不須要在連續的內存中;它能夠分散在不一樣的內存位置。在內核版本2.4中,修改了套接字緩衝區描述符以適應那些需求——在Linux下稱爲零拷貝。這種方法不只減小了多個上下文切換,還消除了處理器形成的數據重複。對於用戶級應用程序,一切都沒有改變,因此代碼仍然是這樣的:

sendfile(socket, file, len);

爲了更好地瞭解所涉及的流程,請看圖4。

Hardware that supports gather can assemble data from multiple memory locations, eliminating another copy.jpg 圖4。支持收集的硬件能夠從多個內存位置收集數據,從而消除了另外一個副本。

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

第二步:沒有數據被複制到套接字緩衝區。相反,只有包含關於數據位置和長度信息的描述符纔會被附加到套接字緩衝區中。DMA引擎直接將數據從內核緩衝區傳遞到協議引擎,從而消除了剩餘的最終副本。

由於數據實際上仍然是從磁盤複製到內存,從內存複製到鏈接,因此有些人可能會認爲這不是真正的零拷貝。可是,從操做系統的角度來看,這是零拷貝,由於數據不是在內核緩衝區之間複製的。在使用零拷貝時,除了避免拷貝以外,還能夠得到其餘性能優點,好比更少的上下文切換、更少的CPU數據緩存污染和更少的CPU校驗和計算。

下面是兩篇很是好的文章,收藏:

Linux 中的零拷貝技術,第 1 部分

Linux 中的零拷貝技術,第 2 部分

tencent.jpg

相關文章
相關標籤/搜索