zero copy實(shí)現(xiàn)高效的數(shù)據(jù)傳輸
許多web應(yīng)用系統(tǒng)都會(huì)向用戶提供大量的靜態(tài)內(nèi)容闸天,這也就是說會(huì)有大量地從磁盤讀取文件數(shù)據(jù),并把讀取后的數(shù)據(jù)寫回到響應(yīng)套接字中驯妄。這個(gè)活動(dòng)似乎看起來幾乎不涉及到CPU計(jì)算跋破,但是它卻有點(diǎn)低效:系統(tǒng)內(nèi)核從磁盤讀取數(shù)據(jù),并借由內(nèi)核空間-用戶空間的切換把數(shù)據(jù)推送給應(yīng)用系統(tǒng)晒喷,之后應(yīng)用系統(tǒng)又借由內(nèi)核空間-用戶空間的切換把數(shù)據(jù)寫出到套接字。從實(shí)際上來看访敌,在把數(shù)據(jù)從磁盤文件傳輸?shù)教捉幼值倪^程中凉敲,應(yīng)用系統(tǒng)其實(shí)是一個(gè)無效的中間媒介。
數(shù)據(jù)每次在用戶空間-內(nèi)核空間移動(dòng)時(shí)寺旺,它都需要被拷貝爷抓,而這樣就消耗了cpu的周期和內(nèi)存的帶寬。不過幸運(yùn)地是阻塑,你可以通過zero copy技術(shù)來消除這些無效的拷貝操作蓝撇。利用zero copy的系統(tǒng)可以直接請(qǐng)求內(nèi)核把數(shù)據(jù)直接從磁盤文件復(fù)制到套接字,而無需經(jīng)由應(yīng)用系統(tǒng)陈莽。零拷貝(zero copy)極大地提供了應(yīng)用性能唉地,并減少了在內(nèi)核空間和用戶空間的切換次數(shù)。
Java類庫對(duì)于Linux和UNIX上的零拷貝支持是通過 java.nio.channels.FileChannel類的transferTo()方法來實(shí)現(xiàn)的传透。你可以使用transferTo()方法直接把一個(gè)channel中的字節(jié)數(shù)據(jù)傳輸?shù)搅硪粋€(gè)可寫的字節(jié)channel中耘沼,而無需數(shù)據(jù)流經(jīng)應(yīng)用系統(tǒng)。本文首先將展示一下使用傳統(tǒng)的拷貝語義來完成文件傳輸時(shí)所產(chǎn)生的消耗朱盐,之后再展示一下使用transferTo()的零拷貝技術(shù)是如何實(shí)現(xiàn)更高性能的群嗤。
數(shù)據(jù)傳輸: 傳統(tǒng)的做法
設(shè)想一下這樣一個(gè)場(chǎng)景: 讀取一個(gè)文件,并通過網(wǎng)絡(luò)把文件中的數(shù)據(jù)傳輸?shù)搅硪粋€(gè)程序中兵琳。這個(gè)操作的核心就是代碼示例1中的倆個(gè)調(diào)用狂秘。
代碼示例1:把文件中的字節(jié)復(fù)制到套接字
File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);
雖然代碼示例1比較簡單,但是其內(nèi)部的拷貝操作卻需要在用戶空間和內(nèi)核空間進(jìn)行四次上下文切換躯肌,并且數(shù)據(jù)需要被復(fù)制四次者春。圖1展示了在系統(tǒng)內(nèi)部數(shù)據(jù)是如何從文件中被移動(dòng)到套接字中的。
傳統(tǒng)數(shù)據(jù)拷貝方法
傳統(tǒng)的上下文切換
上面的圖中涉及到的步驟有:
1清女、read()調(diào)用導(dǎo)致上下文從用戶模式切換到內(nèi)核模式钱烟。其內(nèi)部,sys_read()會(huì)從一個(gè)文件中讀取數(shù)據(jù)嫡丙。第一次的拷貝操作是由DMA(direct memory access)引擎執(zhí)行的,它會(huì)從磁盤中讀取文件內(nèi)容拴袭,并把它們存儲(chǔ)到內(nèi)核地址空間緩存中。
2曙博、大量數(shù)據(jù)從read buffer中拷貝到用戶空間地址緩存中拥刻,read()調(diào)用結(jié)束并返回。read()調(diào)用返回之后會(huì)導(dǎo)致另一個(gè)上下文切換---從內(nèi)核模式切換到用戶模式父泳。此時(shí)般哼,數(shù)據(jù)被存儲(chǔ)在了用戶地址空間緩存中吴汪。
- 3、之后蒸眠,send()套接字調(diào)用又導(dǎo)致了一次上下文切換-從用戶模式到內(nèi)核模式浇坐。執(zhí)行第三次拷貝操作,此數(shù)據(jù)被再次放入內(nèi)核地址空間黔宛。這次近刘,數(shù)據(jù)被放入了一個(gè)不同的buffer中,此buffer和一個(gè)目地套接字相關(guān)臀晃。
- 4觉渴、send()系統(tǒng)調(diào)用返回,第四次上下文切換發(fā)生徽惋。當(dāng)DMA引擎把內(nèi)核中的數(shù)據(jù)傳遞到協(xié)議引擎時(shí)案淋,發(fā)生了第四次拷貝。
內(nèi)核buffer這個(gè)中間媒介的使用似乎看起來是低效的险绘。但是踢京,內(nèi)核buffer當(dāng)初作為一個(gè)中間媒介被引入這個(gè)過程卻是為了提供性能的。當(dāng)應(yīng)用系統(tǒng)請(qǐng)求的數(shù)據(jù)不超過內(nèi)核緩存所能容納的大小的時(shí)候宦棺,在讀操作的一端瓣距,使用內(nèi)核這個(gè)中間媒介使得內(nèi)核buffer可以起到“readahead cache”的作用。這在所請(qǐng)求數(shù)據(jù)遠(yuǎn)小于內(nèi)核buffer的情況下代咸,可以極大地性能蹈丸。在寫操作的一端,內(nèi)核這個(gè)中間媒介可以實(shí)現(xiàn)異步寫入呐芥。
不幸的是逻杖,如果請(qǐng)求的數(shù)據(jù)遠(yuǎn)比內(nèi)核緩存大的情況下,這種方法本身也可能導(dǎo)致性能瓶頸思瘟。數(shù)據(jù)在被最終傳送應(yīng)用系統(tǒng)之前荸百,在磁盤、內(nèi)核buffer滨攻、用戶buffer之間進(jìn)行了多次拷貝操作够话。
通過消除這些冗余的數(shù)據(jù)拷貝,零拷貝可以極大地提高性能铡买。
數(shù)據(jù)傳輸: 零拷貝方法
如果你重新檢查一下上面一個(gè)傳統(tǒng)的場(chǎng)景更鲁,你將發(fā)現(xiàn)第二次和第三次的數(shù)據(jù)拷貝其實(shí)是不必要的。應(yīng)用系統(tǒng)其實(shí)就是在緩存數(shù)據(jù)奇钞,并把緩存的數(shù)據(jù)
寫入到套接字。反之漂坏,數(shù)據(jù)可以被直接地從read buffer中傳輸?shù)教捉幼謆uffer中景埃。transferTo()方法可以幫助你做到這一點(diǎn)媒至。
示例代碼2: transferTo()方法
public void transferTo(long position, long count, WritableByteChannel target);
transferTo()方法可以直接把數(shù)據(jù)從一個(gè)file channel中傳輸?shù)揭粋€(gè)給定的writable byte channel中。內(nèi)部的實(shí)現(xiàn)取決于底層的操作系統(tǒng)對(duì)于
零拷貝的支持谷徙。在UNIX和各種Linux系統(tǒng)中拒啰,transferTo調(diào)用會(huì)被路由到sendfile()系統(tǒng)調(diào)用,就像示例3所展示的
示例代碼3:sendfile()系統(tǒng)調(diào)用
#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
在代碼示例1中的file.read()和sockect.send()操作可以被直接替換為單個(gè)的transferTo()調(diào)用完慧。
示例代碼4:使用transferTo()方法把數(shù)據(jù)從磁盤文件復(fù)制到套接字
transferTo(position, count, writableChannel);
使用transferTo時(shí)的數(shù)據(jù)路徑
使用transferTo方法時(shí)的上下文切換
當(dāng)你使用transferTo()方法時(shí)谋旦,會(huì)執(zhí)行如下動(dòng)作:
1、transferTo()方法的執(zhí)行屈尼,會(huì)讓DMA引擎把文件內(nèi)容拷貝到read buffer中册着,
之后,內(nèi)核會(huì)把數(shù)據(jù)從內(nèi)核buffer拷貝到一個(gè)和輸出套接字相關(guān)的內(nèi)核buffer中2脾歧、DMA引擎把數(shù)據(jù)從內(nèi)核套接字緩存?zhèn)鬟f到協(xié)議引擎
這是一個(gè)進(jìn)步: 我們已經(jīng)減少了上下文切換的次數(shù)甲捏。由原來的4次減少為2次,并減少了數(shù)據(jù)拷貝的次數(shù)從4次降低為3次鞭执。但是這還沒有達(dá)到我們的零拷貝目標(biāo)司顿。我們可以進(jìn)一步減少數(shù)據(jù)復(fù)制的次數(shù),如果底層的網(wǎng)絡(luò)接口支持聚合操作兄纺。從linux kernel 2.4以及其后的系統(tǒng)大溜,套接字緩存描述符都做了修改以適應(yīng)這種需求。 這種方法不僅減少了多次上下文切換而且也消除了涉及到CPU的數(shù)據(jù)拷貝操作估脆。雖然用戶端的使用還是像以前一樣猎提,但其內(nèi)部的運(yùn)行機(jī)制已經(jīng)發(fā)生了改變:
1、 transferTo()方法的調(diào)用旁蔼,使得DMA引擎把文件內(nèi)容復(fù)制到內(nèi)核緩存锨苏。
2、沒有數(shù)據(jù)再被復(fù)制進(jìn)套接字緩存棺聊。相反伞租,只有相關(guān)位置和數(shù)據(jù)長度信息的描述符被追加進(jìn)套接字緩存中。DMA引擎
直接把套接字緩存中的數(shù)據(jù)傳輸?shù)絽f(xié)議引擎中限佩,也就因此消除了最后一個(gè)cpu拷貝操作葵诈。
transferTo和聚合操作的同時(shí)使用
創(chuàng)建一個(gè)文件服務(wù)器
現(xiàn)在,我們使用在客戶端和服務(wù)器之間傳輸文件的相同示例祟同,來實(shí)踐零副本(示例代碼請(qǐng)參見下載)作喘。 TraditionalClient.java和TraditionalServer.java基于傳統(tǒng)的復(fù)制語義,使用File.read()和Socket.send()晕城。TraditionalServer.java是一個(gè)服務(wù)器程序泞坦,該程序在特定的端口上偵聽客戶端進(jìn)行連接,然后一次從套接字讀取4K字節(jié)的數(shù)據(jù)砖顷。 TraditionalClient.java連接到服務(wù)器贰锁,從文件中讀仍呶唷(使用File.read())4K字節(jié)數(shù)據(jù),然后通過套接字將內(nèi)容(使用socket.send())發(fā)送到服務(wù)器豌熄。
同樣授嘀,TransferToServer.java和TransferToClient.java執(zhí)行相同的功能,但改用transferTo()方法(進(jìn)而使用sendfile()系統(tǒng)調(diào)用)將文件從服務(wù)器傳輸?shù)娇蛻舳恕?/p>
性能比較
我們?cè)趌inux2.6上執(zhí)行上面的示例程序锣险,并測(cè)量使用傳統(tǒng)方法和使用transferTo方法所消耗的時(shí)間對(duì)比蹄皱。
表1:性能對(duì)比: 傳統(tǒng)方法 VS 零拷貝
文件大小 | 傳統(tǒng)的文件傳輸方法 (ms) | transferTo方法 (ms) |
---|---|---|
7MB | 156 | 45 |
21MB | 337 | 128 |
63MB | 843 | 387 |
98MB | 1320 | 617 |
200MB | 2124 | 1150 |
350MB | 3631 | 1762 |
700MB | 13498 | 4422 |
1GB | 18399 | 8537 |
總結(jié)
我們上面展示了相較于使用傳統(tǒng)方法,使用TransferTo()的性能優(yōu)勢(shì)芯肤。中間媒介的buffer拷貝---即使它們隱藏在內(nèi)核層面巷折,依舊產(chǎn)生了相當(dāng)可觀的消耗。
如果一個(gè)應(yīng)用系統(tǒng)需要在channel間處理大量的數(shù)據(jù)拷貝的話纷妆,零拷貝技術(shù)可以帶來極大地性能提升盔几。