[轉]經過零拷貝實現有效數據傳輸

本文轉自:https://www.ibm.com/developerworks/cn/java/j-zerocopy/   寫的不錯,搬運過來java

本文解釋瞭如何經過一種稱爲零拷貝 的方法來提升運行於 Linux® 和 UNIX® 平臺上的 I/O 密集型 Java™ 應用程序的性能。零拷貝不只消除了中間緩衝區之間的冗餘數據拷貝,還減小了用戶空間和內核空間之間的上下文切換次數。緩存

 

不少 Web 應用程序都會提供大量的靜態內容,其數量多到至關於讀完整個磁盤的數據再將一樣的數據寫回響應套接字(socket)。此動做看似只需較少的 CPU 活動,但它的效率很是低:首先內核讀出全盤數據,而後將數據跨越內核用戶推到應用程序,而後應用程序再次跨越內核用戶將數據推回,寫出到套接字。應用程序實際上在這裏擔當了一個不怎麼高效的中介角色,將磁盤文件的數據轉入套接字。服務器

數據每遍歷用戶內核一次,就要被拷貝一次,這會消耗 CPU 週期和內存帶寬。幸運的是,您能夠經過一個叫 零拷貝— 很貼切 — 的技巧來消除這些拷貝。使用零拷貝的應用程序要求內核直接將數據從磁盤文件拷貝到套接字,而無需經過應用程序。零拷貝不只大大地提升了應用程序的性能,並且還減小了內核與用戶模式間的上下文切換。網絡

Java 類庫經過 java.nio.channels.FileChannel 中的 transferTo() 方法來在 Linux 和 UNIX 系統上支持零拷貝。可使用transferTo() 方法直接將字節從它被調用的通道上傳輸到另一個可寫字節通道上,數據無需流經應用程序。本文首先展現了經過傳統拷貝語義進行的簡單文件傳輸引起的開銷,而後展現了使用 transferTo() 零拷貝技巧如何提升性能。異步

數據傳輸:傳統方法

考慮一下從一個文件中讀出數據並將數據傳輸到網絡上另外一程序的場景(這個場景表述出了不少服務器應用程序的行爲,包括提供靜態內容的 Web 應用程序、FTP 服務器、郵件服務器等)。操做的核心在清單 1 的兩個調用中:socket

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

清單 1 的概念很簡單,但實際上,拷貝的操做須要四次用戶模式和內核模式間的上下文切換,並且在操做完成前數據被複制了四次。圖 1 展現了數據是如何在內部從文件移動到套接字的:性能

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

傳統的數據拷貝方法

圖 2 展現了上下文切換:操作系統

圖 2. 傳統上下文切換

傳統上下文切換

這裏涉及的步驟有:3d

  1. read() 調用(參見 圖 2)引起了一次從用戶模式到內核模式的上下文切換。在內部,發出 sys_read()(或等效內容)以從文件中讀取數據。直接內存存取(direct memory access,DMA)引擎執行了第一次拷貝(參見 圖 1),它從磁盤中讀取文件內容,而後將它們存儲到一個內核地址空間緩存區中。
  2. 所需的數據被從讀取緩衝區拷貝到用戶緩衝區,read() 調用返回。該調用的返回引起了內核模式到用戶模式的上下文切換(又一次上下文切換)。如今數據被儲存在用戶地址空間緩衝區。
  3. send() 套接字調用引起了從用戶模式到內核模式的上下文切換。數據被第三次拷貝,並被再次放置在內核地址空間緩衝區。可是這一次放置的緩衝區不一樣,該緩衝區與目標套接字相關聯。
  4. send() 系統調用返回,結果致使了第四次的上下文切換。DMA 引擎將數據從內核緩衝區傳到協議引擎,第四次拷貝獨立地、異步地發生 。

使用中間內核緩衝區(而不是直接將數據傳輸到用戶緩衝區)看起來可能有點效率低下。可是之因此引入中間內核緩衝區的目的是想提升性能。在讀取方面使用中間內核緩衝區,能夠容許內核緩衝區在應用程序不須要內核緩衝區內的所有數據時,充當 「預讀高速緩存(readahead cache)」 的角色。這在所需數據量小於內核緩衝區大小時極大地提升了性能。在寫入方面的中間緩衝區則可讓寫入過程異步完成。code

不幸的是,若是所需數據量遠大於內核緩衝區大小的話,這個方法自己可能成爲一個性能瓶頸。數據在被最終傳入到應用程序前,在磁盤、內核緩衝區和用戶緩衝區中被拷貝了屢次。

零拷貝經過消除這些冗餘的數據拷貝而提升了性能。

數據傳輸:零拷貝方法

再次檢查 傳統場景,您就會注意到第二次和第三次拷貝根本就是多餘的。應用程序只是起到緩存數據並將其傳回到套接字的做用而以,別無他用。數據能夠直接從讀取緩衝區傳輸到套接字緩衝區。transferTo() 方法就可以讓您實現這個操做。清單 2 展現了 transferTo() 的方法簽名:

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

transferTo() 方法將數據從文件通道傳輸到了給定的可寫字節通道。在內部,它依賴底層操做系統對零拷貝的支持;在 UNIX 和各類 Linux 系統中,此調用被傳遞到 sendfile() 系統調用中,如清單 3 所示,清單 3 將數據從一個文件描述符傳輸到了另外一個文件描述符:

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

清單 1 中的 file.read() 和 socket.send() 調用動做能夠替換爲一個單一的 transferTo() 調用,如清單 4 所示:

清單 4. 使用 transferTo() 將數據從磁盤文件拷貝到套接字
transferTo(position, count, writableChannel);

圖 3 展現了使用 transferTo() 方法時的數據路徑:

圖 3. 使用 transferTo() 方法的數據拷貝

使用 transferTo() 方法的數據拷貝

圖 4 展現了使用 transferTo() 方法時的上下文切換:

圖 4. 使用 transferTo() 方法的上下文切換

使用 transferTo() 方法的上下文切換

使用 清單 4 所示的 transferTo() 方法時的步驟有:

  1. transferTo() 方法引起 DMA 引擎將文件內容拷貝到一個讀取緩衝區。而後由內核將數據拷貝到與輸出套接字相關聯的內核緩衝區。
  2. 數據的第三次複製發生在 DMA 引擎將數據從內核套接字緩衝區傳到協議引擎時。

改進的地方:咱們將上下文切換的次數從四次減小到了兩次,將數據複製的次數從四次減小到了三次(其中只有一次涉及到了 CPU)。可是這個代碼還沒有達到咱們的零拷貝要求。若是底層網絡接口卡支持收集操做 的話,那麼咱們就能夠進一步減小內核的數據複製。在 Linux 內核 2.4 及後期版本中,套接字緩衝區描述符就作了相應調整,以知足該需求。這種方法不只能夠減小多個上下文切換,還能夠消除須要涉及 CPU 的重複的數據拷貝。對於用戶方面,用法仍是同樣的,可是內部操做已經發生了改變:

  1. transferTo() 方法引起 DMA 引擎將文件內容拷貝到內核緩衝區。
  2. 數據未被拷貝到套接字緩衝區。取而代之的是,只有包含關於數據的位置和長度的信息的描述符被追加到了套接字緩衝區。DMA 引擎直接把數據從內核緩衝區傳輸到協議引擎,從而消除了剩下的最後一次 CPU 拷貝。

圖 5 展現告終合使用 transferTo() 方法和收集操做的數據拷貝:

圖 5. 結合使用 transferTo() 和收集操做時的數據拷貝

結合使用 transferTo() 和收集操做時的數據拷貝

相關文章
相關標籤/搜索