— 1、概述
許多Web應用程序提供大量靜態內容,相當於從磁盤讀取數據原封不動地寫回響應套接字。
這個操作似乎只需要相對較少的CPU時間,但它的效率有點低:內核從磁盤讀取數據,並跨越內核空間 - 用戶空間邊界
,將其推到應用程序,然後應用程序將其寫到套接字中。實際上,應用程序充當了一個低效的中介 - 將數據從磁盤文件挪到Socket。
每次數據穿越內核空間 - 用戶空間邊界
時,都必須進行復制,這會消耗CPU週期和內存帶寬。
幸運的是,您可以通過一種稱爲足夠的零拷貝的技術來消除這些副本。使用零拷貝的應用程序請求內核直接將數據從磁盤文件複製到套接字,而不經過應用程序。
零拷貝極大地提高了應用程序的性能,減少了內核態
和用戶態
之間的上下文切換次數。
在Linux和UNIX系統上,Java類庫通過java.nio.channels.FileChannel
中的long transferTo(...)
方法支持零拷貝。可以使用transferTo()
方法將字節數據直接從調用它的通道傳輸到另一個可寫的字節通道,而數據不需要流經應用程序。
本文先演示通過傳統複製語義進行的簡單文件傳輸所帶來的開銷,然後展示使用transferTo()
的零複製技術如何獲得更好的性能。
— 2、傳統數據傳輸方式
考慮一下從文件中讀取數據,然後通過網絡將數據傳輸到另一個程序的場景。(此場景描述了許多服務器應用程序的行爲,包括提供靜態內容的Web應用程序、FTP服務器、郵件服務器等。)
兩個核心方法調用見 Listing 1 📚(點擊下載完整的示例代碼):
📚 Listing 1. Copying bytes from a file to a socket
File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);
雖然Listing 1📚 代碼很簡單,但是在內部實現上,複製操作需要在用戶態和內核態之間進行四次上下文切換,並且在操作完成之前複製數據四次。
Figure 1 顯示了內部是如何從文件移動到套接字的:
Figure 1. 傳統數據拷貝方式 Traditional data copying approach |
步驟如下:
-
用戶程序調用
read()
方法,導致OS從用戶態到內核態的1️⃣🔃第一次上下文切換(參見Figure 2)。在OS內部,會調用sys_read()
系統調用(或等效方法)來從文件中讀取數據。第一個副本(參見圖1)由 直接內存訪問(DMA) 引擎執行,該引擎從磁盤讀取文件內容並將其存儲到內核地址空間緩衝區中(第一次複製1️⃣📘)。 -
請求的大量數據從
讀緩衝區
複製到用戶緩衝區
(第二次複製2️⃣📘),然後read()
方法返回。方法返回導致2️⃣🔃第二次內核態到用戶態的上下文切換 。現在數據存儲在用戶緩衝區
(用戶地址空間緩衝區)中。 -
Socket.send()方法調用導致從用戶態切換內核態(3️⃣🔃第三次上下文切換)。,再次將數據放入
內核地址空間緩衝區
(執行第三次複製3️⃣📘)。不過,這一次數據被放入一個不同的緩衝區(Socket Buffer),這個緩衝區與目標 Socket 相關聯。 -
send()方法返回,導致4️⃣🔃第四次上下文切換。DMA引擎將數據從內核緩衝區傳遞到協議引擎(這是第四次複製4️⃣📘),這個過程是DMA引擎獨立且異步進行的。
Figure 2 顯示了上下文切換
Figure 2. Traditional context switches |
使用中間的內核緩衝區(而不是直接將數據傳輸到用戶緩衝區)可能看起來效率很低。但是進程中引入了中間內核緩衝區就是爲了提高性能。
在讀取端使用中間緩衝區允許內核緩衝區在應用程序沒有請求內核緩衝區所持有的數據時充當“預讀緩存”,當請求的數據量小於內核緩衝區大小時,這將顯著提高性能。
寫入端的中間緩衝區允許異步完成寫操作。
不幸的是,如果所請求的數據的大小遠遠大於內核緩衝區的大小,這種方式本身就會成爲性能瓶頸。數據在最終交付給應用程序之前,會在磁盤、內核緩衝區和用戶緩衝區之間複製多次。
零拷貝通過消除這些冗餘的數據拷貝以提升性能。
— 3、零拷貝方式數據傳輸
如果重新檢查傳統的場景,您會注意到實際上並不需要第二和第三個數據副本。應用程序只是緩存數據並將其傳輸回套接字緩衝區。相反,數據可以直接從讀緩衝區
傳輸到Socket緩衝區
。transferTo()
方法允許您完成這一操作。
Listing 2📚顯示了transferTo()
方法的聲明:
📚Listing 2. The transferTo() method
public void transferTo(long position, long count, WritableByteChannel target);
transferTo()
方法將數據從文件通道傳輸到給定的可寫字節通道。在內部,它依賴於底層操作系統對零拷貝的支持;
在UNIX和各種Linux中,這個調用被路由到sendfile()
系統調用,如 Listing 3 📚所示,它將數據從一個文件描述符傳輸到另一個文件描述符:
📚Listing 3. The sendfile() system call
#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
Listing 1 中的 file.read()
和socket.send()
調用的操作可以被一個transferTo()調用代替,如清單4所示:
The action of the file.read() and socket.send() calls in Listing 1 can be replaced by a single transferTo() call, as shown in Listing 4:
📚Listing 4. Using transferTo() to copy data from a disk file to a socket
transferTo(position, count, writableChannel);
Figure 3 shows the data path when the transferTo() method is used:
Figure 3. Data copy with transferTo()
Figure 4 shows the context switches when the transferTo() method is used:
Figure 4. Context switching with transferTo()
在清單4中使用transferTo()
時採取的步驟是:
- transferTo()方法導致DMA引擎將文件內容複製到一個讀取緩衝區中。然後,內核將數據複製到與輸出Socket關聯的內核緩衝區中。
- 當DMA引擎將數據從內核套接字緩衝區傳遞到協議引擎時,會發生第三次複製。
改進:我們將上下文切換從4次減少到2次,並將數據複製從4次減少到3次(其中只有一個涉及到CPU)。但這並沒有讓我們達到零拷貝的目標。
如果底層網絡接口卡支持收集操作,我們可以進一步減少內核所做的數據重複。在Linux內核2.4及更高版本中,修改了套接字緩衝區描述符以適應這一需求。這種方法不僅減少了多個上下文切換,還消除了需要CPU參與的重複數據賦值,用戶端仍然保持不變,但本質已經改變:
- transferTo()方法導致DMA引擎將文件內容複製到內核緩衝區中。
- 沒有數據被複制到套接字緩衝區。相反,只有包含數據位置和長度信息的描述符纔會被附加到套接字緩衝區中。DMA引擎直接將數據從內核緩衝區傳遞到協議引擎,從而消除了CPU拷貝。
圖5顯示了具有數據收集功能的transferTo()方法的數據拷貝過程
Figure 5 shows the data copies using transferTo() with the gather operation:
Figure 5. Data copies when transferTo() and gather operations are used
— 4、構建文件服務器
現在,讓我們使用在客戶機和服務器之間傳輸文件的相同示例來實踐零拷貝(有關示例代碼,請參閱下載)。
TraditionalClient.java
和 TraditionalClient.java
基於傳統的複製語義,使用File.read()和Socket.send()。
TraditionalClient.java
是一個服務器程序,它監聽客戶端要連接的特定端口,然後每次從套接字中讀取4K字節的數據。
TraditionalClient.java
連接到服務器,從文件中讀取(使用file .read()) 4K字節的數據,並通過socket將內容發送給服務器(使用socket.send())
Table 1. Performance comparison: Traditional approach vs. zero copy |
可以看到,與傳統方法相比,transferTo() API減少了大約65%的時間。對於需要將大量數據從一個I/O通道複製到另一個通道(如Web服務器)的應用程序,這可能會顯著提高性能。
As you can see, the transferTo() API brings down the time approximately 65 percent compared to the traditional approach. This has the potential to increase performance significantly for applications that do a great deal of copying of data from one I/O channel to another, such as Web servers.
—5、總結
我們已經演示了相較於從一個通道讀取數據然後將其寫入另一個通道, 使用transferTo()的性能優勢。中間緩衝區的拷貝(哪怕他隱藏在內核中)的成本是可以測量的。
在需要在通道之間進行大量數據複製的應用程序中,零複製技術可以顯著提高性能。