轉(zhuǎn)自并發(fā)編程網(wǎng)http://ifeve.com/linux%E9%9B%B6%E6%8B%B7%E8%B4%9D%E5%8E%9F%E7%90%86/
到目前為止骑祟,幾乎所有人都聽說過Linux下所謂的零拷貝功能赊琳,但我經(jīng)常遇到對(duì)這個(gè)主題沒有完全了解的人。 正因?yàn)槿绱巳刑疲覜Q定寫幾篇文章,深入探討這個(gè)問題扫俺,希望能解開這個(gè)有用的特性路鹰。在這篇文章中惨篱,我們從一個(gè)用戶的角度看零拷貝盏筐,血淋淋的內(nèi)核級(jí)細(xì)節(jié)被有意省略。
什么是零拷貝?
為了更好地理解問題的解決方案砸讳,我們首先需要了解問題本身琢融。讓我們來看看網(wǎng)絡(luò)服務(wù)器處理的簡單過程中所涉及到的內(nèi)容,它將存儲(chǔ)在文件中的數(shù)據(jù)存儲(chǔ)到網(wǎng)絡(luò)上的客戶端中簿寂。這里有一些示例代碼:
read(file, tmp_buf, len);
write(socket, tmp_buf, len);
看起來很簡單;你會(huì)認(rèn)為只有這兩個(gè)系統(tǒng)調(diào)用不會(huì)有太多的開銷. 事實(shí)上漾抬,這與事實(shí)并無太大的距離。在這兩個(gè)調(diào)用的后面陶耍,數(shù)據(jù)至少被復(fù)制了4次奋蔚,并且?guī)缀跻呀?jīng)執(zhí)行了許多用戶/內(nèi)核上下文切換(實(shí)際上這個(gè)過程要復(fù)雜得多,但我想讓它保持簡單)。 為了更好地了解所涉及的過程泊碑,請(qǐng)看圖1坤按。頂部顯示了上下文切換,而底部顯示了復(fù)制操作馒过。
步驟一:讀系統(tǒng)調(diào)用會(huì)導(dǎo)致從用戶模式到內(nèi)核模式的上下文切換臭脓。第一個(gè)復(fù)制由DMA引擎執(zhí)行,它讀取磁盤中的文件內(nèi)容并將其存儲(chǔ)到內(nèi)核地址空間緩沖區(qū)中腹忽。
第二步:將數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到用戶緩沖區(qū)来累,read系統(tǒng)調(diào)用返回。調(diào)用的返回導(dǎo)致了從內(nèi)核返回到用戶模式的上下文切換窘奏,現(xiàn)在嘹锁,數(shù)據(jù)存儲(chǔ)在用戶地址空間緩沖區(qū)中,它可以再次開始向下移動(dòng)着裹。
第三步:write系統(tǒng)調(diào)用導(dǎo)致從用戶模式到內(nèi)核模式的上下文切換领猾,執(zhí)行第三個(gè)復(fù)制,將數(shù)據(jù)再次放入內(nèi)核地址空間緩沖區(qū)中骇扇。但是這一次摔竿,數(shù)據(jù)被放入一個(gè)不同的緩沖區(qū),這個(gè)緩沖區(qū)是與套接字相關(guān)聯(lián)的少孝。
第四步:寫系統(tǒng)調(diào)用返回继低,創(chuàng)建第四個(gè)上下文切換。DMA引擎將數(shù)據(jù)從內(nèi)核緩沖區(qū)傳遞到協(xié)議engin時(shí)稍走,第四個(gè)復(fù)制發(fā)生了獨(dú)立和異步的情況袁翁。你可能會(huì)問自己,“你說的獨(dú)立和異步是什么意思婿脸?”在調(diào)用返回之前梦裂,數(shù)據(jù)不是傳輸?shù)膯幔俊? 實(shí)際上盖淡,調(diào)用返回并不能保證傳輸;它甚至不能保證傳輸?shù)拈_始。它只是意味著以太網(wǎng)驅(qū)動(dòng)程序在其隊(duì)列中有空閑的描述符并接受了我們的傳輸數(shù)據(jù) 凿歼,在我們的之前可能會(huì)有很多的數(shù)據(jù)包在排隊(duì)褪迟。除非驅(qū)動(dòng)/硬件實(shí)現(xiàn)了優(yōu)先級(jí)環(huán)或隊(duì)列,否則數(shù)據(jù)將以先入先出的方式傳輸答憔。(圖1中派生的DMA copy表明了最后一個(gè)復(fù)制可以被延遲的事實(shí))味赃。
正如您所看到的,大量的數(shù)據(jù)復(fù)制并不是真正需要的虐拓⌒乃祝可以消除一些重復(fù),以減少開銷并提高性能。 作為一名驅(qū)動(dòng)開發(fā)人員城榛,我使用的硬件具有一些非常高級(jí)的特性揪利。一些硬件可以完全繞過主存,直接將數(shù)據(jù)傳輸?shù)搅硪粋€(gè)設(shè)備上狠持。 該特性消除了系統(tǒng)內(nèi)存中的復(fù)制疟位,這是一件很好的事情,但并不是所有的硬件都支持它喘垂。還有一個(gè)問題是甜刻,磁盤上的數(shù)據(jù)必須重新打包以供網(wǎng)絡(luò)使用,這帶來了一些復(fù)雜的問題正勒。 為了消除開銷得院,我們可以從消除內(nèi)核和用戶緩沖區(qū)之間的一些復(fù)制開始。
消除復(fù)制的一種方法是跳過調(diào)用read和調(diào)用mmap章贞。例如:
tmp_buf = mmap(file, len);
write(socket, tmp_buf, len);
為了更好地了解過程祥绞,請(qǐng)查看圖2。上下文切換保持不變阱驾。
第一步:mmap系統(tǒng)調(diào)用將文件內(nèi)容復(fù)制到DMA引擎的內(nèi)核緩沖區(qū)中就谜。然后在用戶進(jìn)程中共享緩沖區(qū),而不需要在內(nèi)核和用戶內(nèi)存空間之間執(zhí)行任何復(fù)制里覆。
第二步:write系統(tǒng)調(diào)用導(dǎo)致內(nèi)核將數(shù)據(jù)從原始內(nèi)核緩沖區(qū)復(fù)制到與套接字關(guān)聯(lián)的內(nèi)核緩沖區(qū)中丧荐。
第三步:當(dāng)DMA引擎將數(shù)據(jù)從內(nèi)核套接字緩沖區(qū)傳遞到協(xié)議引擎時(shí),第三次復(fù)制發(fā)生喧枷。
通過使用mmap而不是讀取虹统,我們將內(nèi)核必須復(fù)制的數(shù)據(jù)量減少了一半。當(dāng)大量數(shù)據(jù)被傳輸時(shí)隧甚,這將產(chǎn)生相當(dāng)好的結(jié)果车荔。然而,這種改進(jìn)并不是沒有代價(jià)的;使用mmap+write方法時(shí)存在一些隱藏的缺陷戚扳。當(dāng)您的內(nèi)存映射一個(gè)文件忧便,然后調(diào)用write,而另一個(gè)進(jìn)程截?cái)嘞嗤奈募r(shí)帽借,您將陷入其中之一珠增。 您的write系統(tǒng)調(diào)用將被總線錯(cuò)誤信號(hào)SIGBUS中斷,因?yàn)槟鷪?zhí)行了一個(gè)糟糕的內(nèi)存訪問砍艾。該信號(hào)的默認(rèn)行為是殺死進(jìn)程并轉(zhuǎn)儲(chǔ)內(nèi)核——而不是網(wǎng)絡(luò)服務(wù)器最理想的操作蒂教。有兩種方法可以解決這個(gè)問題。
第一種方法是為SIGBUS信號(hào)安裝一個(gè)信號(hào)處理程序脆荷,然后在處理程序中簡單地調(diào)用return凝垛。通過這樣做懊悯,write系統(tǒng)調(diào)用將返回它在被中斷之前所寫的字節(jié)數(shù),以及errno設(shè)置為成功梦皮。我必須指出炭分,這將是一個(gè)糟糕的解決方案,一個(gè)治療癥狀届氢,而不是病根的解決方案欠窒。因?yàn)镾IGBUS信號(hào)表明這個(gè)過程出現(xiàn)了嚴(yán)重的問題,所以我不建議使用這個(gè)作為解決方案退子。
第二個(gè)解決方案涉及文件租賃(在Microsoft Windows中稱為“機(jī)會(huì)鎖定”)岖妄。這是解決這個(gè)問題的正確方法。通過使用文件描述符上的租賃,你將在內(nèi)核上租賃獲取一個(gè)特定的文件寂祥。通過在文件描述符上使用租賃荐虐,可以在特定文件上使用內(nèi)核進(jìn)行租約。然后可以從內(nèi)核請(qǐng)求讀/寫租約丸凭。 當(dāng)另一個(gè)進(jìn)程試圖截?cái)嗾趥鬏數(shù)奈募r(shí)福扬,內(nèi)核會(huì)向您發(fā)送一個(gè)實(shí)時(shí)信號(hào),即RT_SIGNAL_LEASE信號(hào)惜犀。它告訴您內(nèi)核正在破壞您在該文件上的寫或讀租約铛碑。在程序訪問一個(gè)無效的地址并被SIGBUS信號(hào)殺死之前,您的write調(diào)用會(huì)被中斷虽界。write調(diào)用的返回值是在中斷之前寫入的字節(jié)數(shù)汽烦,而errno將被設(shè)置為成功。下面是一些演示如何從內(nèi)核獲得租約的示例代碼:
if(fcntl(fd, F_SETSIG, RT_SIGNAL_LEASE) == -1) {
perror("kernel lease set signal");
return -1;
}
/* l_type can be F_RDLCK F_WRLCK */
if(fcntl(fd, F_SETLEASE, l_type)){
perror("kernel lease set type");
return -1;
}
Sendfile
在內(nèi)核版本2.1中莉御,引入了sendfile系統(tǒng)調(diào)用撇吞,以簡化網(wǎng)絡(luò)和兩個(gè)本地文件之間的數(shù)據(jù)傳輸。sendfile的引入不僅減少了數(shù)據(jù)復(fù)制礁叔,還減少了上下文切換牍颈。使用它是這樣的:
sendfile(socket, file, len);
為了更好地了解過程,請(qǐng)查看圖3琅关。
第一步:sendfile系統(tǒng)調(diào)用將把文件內(nèi)容復(fù)制到DMA引擎的內(nèi)核緩沖區(qū)中煮岁。然后將數(shù)據(jù)復(fù)制到與套接字相關(guān)聯(lián)的內(nèi)核緩沖區(qū)中。
步驟二:當(dāng)DMA引擎將數(shù)據(jù)從內(nèi)核套接字緩沖區(qū)傳遞到協(xié)議引擎時(shí)涣易,第三次復(fù)制發(fā)生人乓。
您可能想知道,如果另一個(gè)進(jìn)程截?cái)嗔宋覀冇胹endfile系統(tǒng)調(diào)用發(fā)送的文件都毒,會(huì)發(fā)生什么。如果我們不注冊(cè)任何信號(hào)處理程序碰缔,sendfile調(diào)用只需返回它在被中斷之前傳輸?shù)淖止?jié)數(shù)账劲,而errno將被設(shè)置為成功。
如果我們?cè)谡{(diào)用sendfile之前從文件的內(nèi)核獲得一個(gè)租約,但是瀑焦,行為和返回狀態(tài)完全相同腌且。在sendfile調(diào)用返回之前,我們還獲得了RT_SIGNAL_LEASE信號(hào)榛瓮。
到目前為止铺董,我們已經(jīng)能夠避免內(nèi)核生成幾個(gè)復(fù)制,但是我們?nèi)匀恢皇O乱粋€(gè)復(fù)制禀晓。這個(gè)可以避免嗎? 當(dāng)然精续,在硬件的幫助下。為了消除內(nèi)核所做的所有數(shù)據(jù)復(fù)制粹懒,我們需要一個(gè)支持收集操作的網(wǎng)絡(luò)接口重付。 這僅僅意味著等待傳輸?shù)臄?shù)據(jù)不需要在連續(xù)的內(nèi)存中它可以分散在不同的內(nèi)存位置。在內(nèi)核版本2.4中凫乖,修改了套接字緩沖區(qū)描述符以適應(yīng)這些需求——在Linux下稱為零拷貝确垫。這種方法不僅減少了多個(gè)上下文切換,還消除了處理器的數(shù)據(jù)復(fù)制帽芽。對(duì)于用戶級(jí)應(yīng)用程序删掀,沒有任何更改,因此代碼仍然是這樣:
sendfile(socket, file, len);
為了更好地了解過程导街,請(qǐng)查看圖4披泪。
第一步:sendfile系統(tǒng)調(diào)用將把文件內(nèi)容復(fù)制到DMA引擎的內(nèi)核緩沖區(qū)中菊匿。
第二步:沒有將數(shù)據(jù)復(fù)制到套接字緩沖區(qū)中付呕。相反,只有帶有關(guān)于數(shù)據(jù)的位置和長度的信息的描述符被追加到套接字緩沖區(qū)跌捆。DMA引擎直接將數(shù)據(jù)從內(nèi)核緩沖區(qū)傳遞到協(xié)議引擎徽职,從而消除剩余的最終復(fù)制。
因?yàn)閿?shù)據(jù)實(shí)際上仍然是從磁盤復(fù)制到內(nèi)存和從存儲(chǔ)器到導(dǎo)線佩厚,有些人可能會(huì)認(rèn)為這不是一個(gè)真正的零拷貝姆钉。但是,這是從操作系統(tǒng)的角度來看是零拷貝抄瓦,因?yàn)閿?shù)據(jù)不是在內(nèi)核緩沖區(qū)之間復(fù)制的潮瓶。當(dāng)使用零拷貝時(shí),除了復(fù)制避免之外钙姊,還可以使用其他性能優(yōu)勢(shì)毯辅,例如更少的上下文切換、更少的CPU數(shù)據(jù)緩存污染和沒有CPU校驗(yàn)和計(jì)算煞额。