零拷貝,零開銷
本文僅是中文版本砌庄,原文由 Sathish Palaniappan, Pramod Nagaraja 發(fā)布于 2008年09月2號(hào)。文章適合初次接觸零拷貝技術(shù)并想進(jìn)一步學(xué)習(xí)的讀者,零拷貝本身是一種思想盹舞,不與任何編程語言綁定产镐,不懂Java的讀者可以跳過零拷貝技術(shù)在Java中實(shí)現(xiàn)的具體細(xì)節(jié)。
許多Web應(yīng)用提供大量的靜態(tài)內(nèi)容踢步,主要就是從磁盤讀取數(shù)據(jù)然后將數(shù)據(jù)寫回套接字癣亚,中間不涉及數(shù)據(jù)的變換。這種操作對(duì)CPU的使用相對(duì)較少获印,但是效率很低:首先述雾,內(nèi)核從文件讀取數(shù)據(jù),然后將數(shù)據(jù)從內(nèi)核空間拷貝到用戶進(jìn)程空間兼丰,最后應(yīng)用程序?qū)?shù)據(jù)拷貝回內(nèi)核空間并通過套接字發(fā)送玻孟。實(shí)際上,在整個(gè)流程中應(yīng)用程序僅充當(dāng)一個(gè)將數(shù)據(jù)從磁盤拷貝到套接字的低效中間層地粪。
每次數(shù)據(jù)跨越用戶態(tài)和內(nèi)核態(tài)的邊界取募,數(shù)據(jù)都需要拷貝,拷貝操作消耗CPU和內(nèi)存帶寬蟆技。幸運(yùn)的是通過一種稱為“零拷貝”的技術(shù)可消除這些不必要的拷貝玩敏。使用零拷貝的應(yīng)用要求內(nèi)核將磁盤數(shù)據(jù)直接拷貝到套接字而不再經(jīng)過應(yīng)用。零拷貝可以極大的提高應(yīng)用的性能并減少上下文在內(nèi)核態(tài)和用戶態(tài)之間的切換次數(shù)质礼。
在 Linux 和 Unix 系統(tǒng)中 Java 類庫通過java.nio.channels.FileChannel
的transgerTo
方法支持零拷貝旺聚。可以使用transgerTo
方法在兩個(gè)通道之間直接傳遞數(shù)據(jù)眶蕉,而不要求數(shù)據(jù)經(jīng)過應(yīng)用程序砰粹。為了更好的理解零拷貝技術(shù)對(duì)性能的提升,首先通過傳統(tǒng)復(fù)制語義實(shí)現(xiàn)一個(gè)簡單文件傳輸功能造挽,然后通過零拷貝技術(shù)實(shí)現(xiàn)同樣功能碱璃,并比較兩種實(shí)現(xiàn)在性能上的差異。
數(shù)據(jù)傳輸: 傳統(tǒng)語義
考慮這樣的場景:從文件讀取數(shù)據(jù)并通過網(wǎng)絡(luò)將數(shù)據(jù)傳遞給其他程序(這是很多應(yīng)用的行為饭入,包括提供靜態(tài)內(nèi)容的Web應(yīng)用嵌器,F(xiàn)TP 服務(wù)器,郵件服務(wù)器等)谐丢。兩個(gè)核心的操作如代碼1所示:
代碼 1. 從文件拷貝數(shù)據(jù)到套接字
File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);
雖然代碼1非常的簡單爽航,但是在代碼內(nèi)部實(shí)現(xiàn),拷貝操作需要上下文在用戶態(tài)和內(nèi)核態(tài)切換四次乾忱,在操作完成前數(shù)據(jù)需要拷貝四次讥珍。圖1展示了數(shù)據(jù)如何從文件轉(zhuǎn)移到套接字:
圖 1. 傳統(tǒng)數(shù)據(jù)拷貝方法
圖 2 展示了上下文切換:
圖 2. 傳統(tǒng)方法下的上下文切換
涉及的步驟包括:
read()
調(diào)用導(dǎo)致上下文從用戶態(tài)切換到內(nèi)核態(tài)。內(nèi)核通過sys_read()
(或等價(jià)的方法)從文件讀取數(shù)據(jù)窄瘟。DMA引擎執(zhí)行第一次拷貝:從文件讀取數(shù)據(jù)并存儲(chǔ)到內(nèi)核空間的緩沖區(qū)衷佃。請(qǐng)求的數(shù)據(jù)從內(nèi)核的讀緩沖區(qū)拷貝到用戶緩沖區(qū),然后
read()
方法返回蹄葱。read()
方法返回導(dǎo)致上下文從內(nèi)核態(tài)切換到用戶態(tài)∈弦澹現(xiàn)在待讀取的數(shù)據(jù)已經(jīng)存儲(chǔ)在用戶空間內(nèi)的緩沖區(qū)衰腌。send()
調(diào)用導(dǎo)致上下文從用戶態(tài)切換到內(nèi)核態(tài)。第三次拷貝數(shù)據(jù)從用戶空間重新拷貝到內(nèi)核空間緩沖區(qū)觅赊。但是右蕊,這一次,數(shù)據(jù)被寫入一個(gè)不同的緩沖區(qū)吮螺,一個(gè)與目標(biāo)套接字相關(guān)聯(lián)的緩沖區(qū)饶囚。send()
系統(tǒng)調(diào)用返回導(dǎo)致第四次上下文切換。當(dāng)DMA引擎將數(shù)據(jù)從內(nèi)核緩沖區(qū)傳輸?shù)絽f(xié)議引擎緩沖區(qū)時(shí)鸠补,第四次拷貝是獨(dú)立且異步的萝风。
使用中間內(nèi)核緩沖區(qū)(而不是將數(shù)據(jù)直接發(fā)送到用戶緩沖區(qū))似乎非常低效。但是紫岩,進(jìn)程引入中間內(nèi)核緩沖區(qū)可以提高性能规惰。在讀取端使用中間內(nèi)核緩沖區(qū),在應(yīng)用請(qǐng)求的數(shù)據(jù)沒有超出內(nèi)核緩沖區(qū)的數(shù)據(jù)時(shí)泉蝌,內(nèi)核緩沖區(qū)可以擔(dān)當(dāng)“預(yù)讀緩存”的角色歇万。在寫端,中間內(nèi)核緩沖區(qū)使寫操作完全異步化勋陪。
不幸的是贪磺,當(dāng)請(qǐng)求的數(shù)據(jù)大于內(nèi)核緩沖區(qū)大小時(shí)這種方法往往會(huì)成為性能瓶頸。數(shù)據(jù)在最終被發(fā)送之前诅愚,在磁盤寒锚,內(nèi)核緩沖區(qū)和用戶緩沖區(qū)之間發(fā)生多次拷貝。零拷貝通過減少不必要的數(shù)據(jù)拷貝以提供性能违孝。
數(shù)據(jù)傳輸: 零拷貝方式
如果你回想使用傳統(tǒng)語義傳遞數(shù)據(jù)的場景刹前,你會(huì)發(fā)現(xiàn)第二次和第三次數(shù)據(jù)拷貝并不是真的需要。應(yīng)用程序除了緩存數(shù)據(jù)然后將數(shù)據(jù)傳回套接字緩沖區(qū)外沒有做任何事情雌桑。數(shù)據(jù)可以直接從內(nèi)核的讀緩沖區(qū)傳輸?shù)教捉幼志彌_區(qū)喇喉。transferTo
方法允許你實(shí)現(xiàn)這樣的流程。transferTo
方法的簽名如代碼 2所示:
代碼 2. The transferTo() method
public void transferTo(long position, long count, WritableByteChannel target);
transferTo
方法將數(shù)據(jù)從文件通道傳輸?shù)浇o定的可寫字節(jié)通道筹燕。transferTo
內(nèi)部實(shí)現(xiàn)依賴底層操作系統(tǒng)對(duì)零拷貝的支持:在UNIX和各 Linux 版本中轧飞,transgerTo
方法調(diào)用最終會(huì)調(diào)用sendfile()方法
衅鹿,代碼如 List 3 所示撒踪,sendfile
將數(shù)據(jù)從一個(gè)文件描述符傳輸?shù)搅硪粋€(gè):
代碼 3. The sendfile() system call
#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
代碼 1 中的file.read()
和socket.send()
兩個(gè)方法調(diào)用可以替換為一個(gè)transferTo()
方法調(diào)用,如代碼 4所示:
代碼 4. 使用 transferTo() 從磁盤拷貝數(shù)據(jù)到套接字
transferTo(position, count, writableChannel);
圖 3 展示了使用 transferTo()
方法時(shí)大渤,數(shù)據(jù)的流向:
圖 3. 使用transferTo()時(shí)數(shù)據(jù)拷貝
圖 4 展示了使用 transferTo()
方法時(shí)制妄,上下文的切換:
圖 4. 使用 transferTo() 時(shí)上下文切換
使用transgerTo()
方法時(shí)涉及的步驟包括以下兩步:
transgerTo
方法調(diào)用觸發(fā)DMA引擎將文件上下文信息拷貝到內(nèi)核讀緩沖區(qū),接著內(nèi)核將數(shù)據(jù)從內(nèi)核緩沖區(qū)拷貝到與外出套接字相關(guān)聯(lián)的緩沖區(qū)泵三。DMA引擎將數(shù)據(jù)從內(nèi)核套接字緩沖區(qū)傳輸?shù)絽f(xié)議引擎(第三次數(shù)據(jù)拷貝)耕捞。
這是一個(gè)改進(jìn):上下文切換的次數(shù)從4次減少到2次衔掸,數(shù)據(jù)拷貝的次數(shù)從4次減少到3次(僅有一次數(shù)據(jù)拷貝消耗CPU資源)。然而俺抽,這并沒有實(shí)現(xiàn)零拷貝的目標(biāo)敞映,如果底層網(wǎng)卡支持gather operations,可以進(jìn)一步減少內(nèi)核拷貝數(shù)據(jù)的次數(shù)磷斧。Linux 內(nèi)核 從2.4 版本開始修改了套接字緩沖區(qū)描述符以滿足這個(gè)要求振愿。這種方法不僅減少了多個(gè)上下文切換,還消除了消耗CPU的重復(fù)數(shù)據(jù)拷貝弛饭。用戶使用的方法沒有任何變化冕末,依然通過transferTo
方法,但是方法的內(nèi)部實(shí)現(xiàn)
發(fā)生了變化:
transferTo
方法調(diào)用觸發(fā) DMA 引擎將文件上下文信息拷貝到內(nèi)核緩沖區(qū)侣颂。數(shù)據(jù)不會(huì)被拷貝到套接字緩沖區(qū)档桃,只有數(shù)據(jù)的描述符(包括數(shù)據(jù)位置和長度)被拷貝到套接字緩沖區(qū)。DMA 引擎直接將數(shù)據(jù)從內(nèi)核緩沖區(qū)拷貝到協(xié)議引擎憔晒,這樣減少了最后一次需要消耗CPU的拷貝操作藻肄。
圖 5 展示了在有gather option
條件下使用transferTo
時(shí)數(shù)據(jù)拷貝情況:
圖 5. 使用 transferTo() and gather operations 時(shí)的數(shù)據(jù)拷貝
性能測試
現(xiàn)在讓我們在一個(gè)需要在客戶端和服務(wù)器之間傳輸文件的程序中應(yīng)用零拷貝。 TraditionalClient.java
和 TraditionalServer.java
基于傳統(tǒng)復(fù)制語義拒担,使用 File.read()
和 Socket.send()
方法讀取和發(fā)送數(shù)據(jù). TraditionalServer.java
是一個(gè)監(jiān)聽在5230端口等待客戶端連接的服務(wù)器應(yīng)用仅炊,每次從套接字中讀取4kb的數(shù)據(jù)。 TraditionalClient.java
連接到服務(wù)器, 使用File.read()
方法每次從文件讀取4kb數(shù)據(jù)澎蛛,然后調(diào)用方法socket.send()
) 將數(shù)據(jù)通過套接字發(fā)送給服務(wù)器.
類似的, TransferToServer.java
和 TransferToClient.java
通過使用transferTo()
(使用sendfile()
系統(tǒng)調(diào)用發(fā)送數(shù)據(jù))實(shí)現(xiàn)一樣的將數(shù)據(jù)從客戶端發(fā)送到服務(wù)器的功能抚垄。
性能比較
在Linux 內(nèi)核2.6版本上,以毫秒統(tǒng)計(jì)使用傳統(tǒng)方法和使用transferTo
方法傳輸不同大小的文件的耗時(shí)谋逻。表1展示了測試結(jié)果:
表 1. 性能標(biāo)膠: 傳統(tǒng)方法 vs. 零拷貝
File size | Normal file transfer (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é)果來看使用transgerTo
的API和傳統(tǒng)方法相比可以降低65%的傳輸時(shí)間呆馁。這可以有效的提高在不同I/O通道之間大量拷貝數(shù)據(jù)應(yīng)用的性能。
總結(jié)
我們已經(jīng)證明了在從一個(gè)通道讀取數(shù)據(jù)并將相同的數(shù)據(jù)寫入另一個(gè)通道的場景下使用transferTo
帶來的巨大性能優(yōu)勢毁兆。內(nèi)部緩沖區(qū)的拷貝浙滤,盡管這些拷貝隱藏在內(nèi)核里,但是也是可觀的消耗气堕。對(duì)于需要處理在通道之間拷貝大量數(shù)據(jù)的應(yīng)用纺腊,零拷貝技術(shù)可以顯著的提升性能。性能測試使用的用例可以從Github免費(fèi)下載茎芭。
擴(kuò)展閱讀
在Java編程領(lǐng)域揖膜,Netty是一個(gè)非常流行的基于事件驅(qū)動(dòng)的異步網(wǎng)絡(luò)應(yīng)用框架,Netty的核心框架之一就是擁有豐富的支持零拷貝的字節(jié)緩沖區(qū)梅桩,想進(jìn)一步了解零拷貝技術(shù)的朋友可以深入研究Netty中零拷貝技術(shù)的實(shí)現(xiàn)壹粟。