讀懂零拷貝是什么

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ù)拷貝方法

Figure 1. Traditional data copying approach

傳統(tǒng)的上下文切換

Figure 2. Traditional context switches

上面的圖中涉及到的步驟有:

  • 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ù)路徑

Figure 3. Data copy with transferTo()

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

Figure 4. Context switching with transferTo()

當(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í)使用

Figure 5. Data copies when transferTo() and gather operations are used

創(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ù)可以帶來極大地性能提升盔几。

參考文獻(xiàn)

Efficient data transfer through zero copy

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市掩幢,隨后出現(xiàn)的幾起案子逊拍,更是在濱河造成了極大的恐慌,老刑警劉巖际邻,帶你破解...
    沈念sama閱讀 218,525評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件芯丧,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡世曾,警方通過查閱死者的電腦和手機(jī)缨恒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來轮听,“玉大人骗露,你說我怎么就攤上這事⊙。” “怎么了萧锉?”我有些...
    開封第一講書人閱讀 164,862評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長述寡。 經(jīng)常有香客問我柿隙,道長,這世上最難降的妖魔是什么鲫凶? 我笑而不...
    開封第一講書人閱讀 58,728評(píng)論 1 294
  • 正文 為了忘掉前任禀崖,我火速辦了婚禮,結(jié)果婚禮上螟炫,老公的妹妹穿的比我還像新娘波附。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,743評(píng)論 6 392
  • 文/花漫 我一把揭開白布叶雹。 她就那樣靜靜地躺著财饥,像睡著了一般换吧。 火紅的嫁衣襯著肌膚如雪折晦。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,590評(píng)論 1 305
  • 那天沾瓦,我揣著相機(jī)與錄音满着,去河邊找鬼。 笑死贯莺,一個(gè)胖子當(dāng)著我的面吹牛风喇,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播缕探,決...
    沈念sama閱讀 40,330評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼魂莫,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了爹耗?” 一聲冷哼從身側(cè)響起耙考,我...
    開封第一講書人閱讀 39,244評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎潭兽,沒想到半個(gè)月后倦始,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,693評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡山卦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,885評(píng)論 3 336
  • 正文 我和宋清朗相戀三年鞋邑,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片账蓉。...
    茶點(diǎn)故事閱讀 40,001評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡枚碗,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出铸本,到底是詐尸還是另有隱情肮雨,我是刑警寧澤,帶...
    沈念sama閱讀 35,723評(píng)論 5 346
  • 正文 年R本政府宣布归敬,位于F島的核電站酷含,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏汪茧。R本人自食惡果不足惜椅亚,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,343評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望舱污。 院中可真熱鬧呀舔,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至惧磺,卻和暖如春颖对,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背磨隘。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評(píng)論 1 270
  • 我被黑心中介騙來泰國打工缤底, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人番捂。 一個(gè)月前我還...
    沈念sama閱讀 48,191評(píng)論 3 370
  • 正文 我出身青樓个唧,卻偏偏與公主長得像,于是被迫代替她去往敵國和親设预。 傳聞我的和親對(duì)象是個(gè)殘疾皇子徙歼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,955評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容