Zero-Copy詳細的技術細節

許多Web應用程序服務的靜態內容顯著量,這相當於讀數據從盤的和寫入完全相同的數據迴響套接字這種活動可能出現需要相對少的CPU的活性,但它是有些低效:內核讀取數據從磁盤的並推動它穿過內核用戶邊界的應用程序,然後該應用程序推回跨內核用戶界寫出到套接字。實際上,應用程序充當低效的中介從磁盤文件到套接字獲取數據。

每次數據遍歷用戶內核邊界,它必須被複制,這會消耗CPU週期和存儲器帶寬。幸運的是,你可以通過消除一種被稱爲這些副本-恰如其分地- 零拷貝使用零複製請求,內核將數據直接從磁盤文件到套接字複製,而不需要通過應用去的應用程序。零拷貝大大提高應用程序的性能,並減少內核模式和用戶模式之間的上下文切換的數量。

Java類庫支持在Linux和UNIX系統零拷貝通過transferTo()的方法 java.nio.channels.FileChannel。你可以使用 transferTo()方法直接從它被調用到另一個可寫字節通道的通道傳輸的字節,而無需數據通過應用程序流。本文首先展示了通過傳統拷貝語義進行簡單文件傳輸的開銷,然後展示瞭如何使用零拷貝技術, transferTo()實現了更好的性能。

日期傳輸:傳統方法

考慮從文件中讀取,並通過網絡將數據傳輸到另一個節目的場景。(此方案描述的許多服務器應用程序的行爲,包括提供靜態內容的Web應用程序,FTP服務器,郵件服務器,等等。)操作的核心是在這兩個通話清單1中(見下載的鏈接完整的示例代碼):

清單1.複製字節從文件到套接字
1
2
File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);

雖然清單1的概念很簡單,在內部,複製操作需要用戶模式和內核模式之間的四個上下文切換和數據複製四次之前操作完成。圖1顯示了數據是如何在內部從文件到套接字流程:

圖1.傳統的數據拷貝方法
傳統的數據拷貝方法

圖2顯示了上下文切換:

圖2.傳統上下文切換
傳統上下文切換

涉及的步驟如下:

  1. read()調用導致上下文切換(參見圖2)從用戶模式到內核模式。內部一個sys_read()(或等效物)發出從文件讀出的數據。第一個副本(參見圖1)由直接存儲器存取(DMA)引擎,它從磁盤讀出的文件的內容,並將它們存入一個內核地址空間緩衝液進行。
  2. 請求的數據量從讀緩衝器到用戶的緩衝區,以及複製的read()呼叫返回。從調用的返回導致從內核另一個上下文切換回用戶模式。現在該數據被存儲在用戶地址空間的緩衝區。
  3. send()套接字調用導致從用戶模式到內核模式的上下文切換。進行第三副本將數據放到一個內核地址空間緩衝區一次。此時,雖然,數據被放入不同的緩衝區,即與目的插槽相關聯的一個。
  4. send()系統調用返回,創造了第四個上下文切換。自主,異步,第四拷貝發生,因爲DMA引擎從內核緩衝區協議引擎傳送數據。

中間內核緩衝區(而不是直接傳送數據到用戶緩衝器)的使用似乎沒有效率。但中間內核緩衝區被引入處理以提高性能。在讀取方面使用中間緩衝區允許內核緩衝區充當“預讀緩存”的時候,應用程序並沒有要求儘可能多的數據內核緩衝區成立。當所請求的數據量小於內核緩衝區大小這顯著提高性能。在寫入側中間緩衝器允許寫入異步完成。

不幸的是,這種方法本身可成爲性能瓶頸如果被請求的數據的大小大於內核緩衝區大小大得多。得到的數據的磁盤,內核緩衝區,和用戶緩衝器之間複製多次之前,最後交付給應用程序。

零拷貝通過消除這些冗餘的數據拷貝提高性能。

數據傳輸:零拷貝方法

如果你重新審視傳統的情況下,您會發現,第二和第三個數據副本實際上並不是必需的。該應用程序,不外乎緩存數據並傳送回套接字緩衝區。相反,數據可以直接從讀緩衝器向套接字緩衝區傳送。transferTo() 方法可以讓你這樣做正是這一點。清單2所示的方法簽名 transferTo()

清單2. transferTo() 方法
1
public void transferTo(long position, long count, WritableByteChannel target);

transferTo()從文件通道給定的可寫字節信道方式傳輸數據。在內部,它依賴於底層操作系統對零拷貝的支持; 在UNIX和Linux的各種口味,這個呼叫路由到sendfile() 系統調用,如清單3中所示,從一個文件描述符傳輸到另一個數據:

清單3. sendfile()系統調用
1
2
#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

該的作用file.read()以及socket.send() 在通話清單1可以通過一個單一的替代 transferTo()調用,如清單4所示:

清單4.使用 transferTo()將數據從一個磁盤文件複製到套接字
1
transferTo(position, count, writableChannel);

圖3示出了當數據路徑transferTo()使用方法:

圖3.數據與複製 transferTo()
與數據複製的transferTo()

圖4示出了當在上下文切換transferTo() 時使用的方法:

圖4.上下文切換與 transferTo()
上下文中使用的transferTo切換時()

當您使用採取的步驟transferTo()清單4分別是:

  1. transferTo()方法會導致文件內容被複制到由DMA引擎讀取緩衝器。然後該數據由內核與輸出套接字相關聯的內核緩衝區複製。
  2. 第三個拷貝發生,因爲DMA引擎通過從內核套接字緩衝區的協議引擎的數據。

這是一種進步:我們減少上下文切換的數目從四個兩個,降低了數據的份數從四個三(其中只有一個涉及CPU)。但是,這還沒有讓我們對我們的零拷貝的目標。我們可以進一步減少內核做,如果底層網絡接口卡支持重複數據收集操作在Linux內核2.4及更高版本,套接字緩衝區描述符被修改,以適應這一要求。這種方法不僅可以減少多個上下文切換,還消除了需要CPU參與複製的數據副本。用戶端使用仍保持不變,但內部函數發生了變化:

  1. transferTo()方法會導致文件內容被複制到由DMA引擎內核緩衝區。
  2. 沒有數據被複制到套接字緩衝區。而是,只提供有關數據的位置和長度的信息被附加在套接字緩衝區描述符。DMA引擎直接從內核緩衝區協議引擎通過數據,從而消除了剩下的最後一個CPU副本。

圖5示出了使用數據的副本transferTo()與收集操作:

圖5.數據拷貝時 transferTo(),收集使用操作
當使用的transferTo()和收集操作的數據拷貝

構建一個文件服務器

現在,讓我們把零拷貝到實踐中,使用傳輸客戶端和服務器(請參閱之間文件的同一個例子下載的示例代碼)TraditionalClient.java和 TraditionalServer.java基於傳統的複製語義,使用File.read()Socket.send()TraditionalServer.java是監聽服務器程序爲客戶端的特定端口進行連接,然後在從套接字一次讀取4K字節的數據。TraditionalClient.java連接到服務器,讀取(使用File.read()從文件)4K字節的數據,併發送(使用socket.send())中的內容到服務器經由套接字。

同樣地,TransferToServer.java和 TransferToClient.java執行相同的功能,而是使用transferTo()方法(以及反過來的sendfile()系統調用)的文件從服務器傳送到客戶端。

性能比較

我們執行運行2.6內核的Linux系統上的示例程序和測量以毫秒爲單位既有傳統方法和運行時間transferTo()爲各種尺寸的方法。表1示出其結果:

表1.性能對比:傳統方法與零拷貝
文件大小 普通文件傳輸(毫秒) 的transferTo(毫秒)
7MB 156 45
21MB 337 128
63MB 843 387
98MB 1320 617
200MB 2124 1150
350MB 3631 1762
700MB 13498 4422
1GB 18399 8537

正如你所看到的,transferTo()API帶來的下跌比例爲傳統方法的時間大約爲65%。這具有提高性能顯著爲做數據的拷貝的一個很大的從一個I / O通道到另一個應用程序,如Web服務器的潛力。

概要

我們已經證明使用的性能優勢 transferTo()相比從一個信道讀出和寫入相同的數據到另一個。中間緩衝區拷貝-即使是那些隱藏在內核-能有一個可衡量的成本。在於做通道間數據的拷貝的一個很大的應用中,零拷貝技術可以提供一個顯著性能改進。

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