這篇文章翻譯自Efficient data transfer through zero copy。由于譯者水平有限,有的地方翻譯的可能不正確,所以讀者應(yīng)當(dāng)先查看原文。
Web應(yīng)用程序?yàn)榱颂峁╈o態(tài)內(nèi)容,需要先從磁盤中讀出數(shù)據(jù),然后將這些數(shù)據(jù)寫入到Socket中。雖然這個(gè)過程不會(huì)占用太多CPU資源,但是它確實(shí)效率不高。內(nèi)核從磁盤讀取數(shù)據(jù),需要在內(nèi)核態(tài)和用戶態(tài)之間切換,然后將它寫入到Socket時(shí),又需要從用戶態(tài)切換到內(nèi)核態(tài)。
當(dāng)數(shù)據(jù)在用戶態(tài)和內(nèi)核態(tài)之間切換時(shí),會(huì)消耗CPU資源以及內(nèi)存帶寬。我們可以通過一種叫做Zero-copy的技術(shù)來消除這些開銷。zero-copy會(huì)將數(shù)據(jù)直接從磁盤拷貝到Socket,這就避免在內(nèi)核態(tài)和用戶態(tài)之間的切換。在Java中,java.nio.channels.FileChannel的transferTo()函數(shù)就是通過zero-copy來實(shí)現(xiàn)的。你可以通過transferTo()方法,直接將數(shù)據(jù)從一個(gè)Channel傳輸?shù)搅硪粋€(gè)Channel。
在這篇文章中,首先通過一個(gè)使用傳統(tǒng)方式實(shí)現(xiàn)的文件傳輸?shù)睦樱瑏聿榭雌溟_銷。然后通過一個(gè)用zero-copy實(shí)現(xiàn)的例子,來驗(yàn)證其性能優(yōu)勢。
使用傳統(tǒng)的方式實(shí)現(xiàn)的文件傳輸例子
假設(shè)我們要開發(fā)一個(gè)用于從特定文件中讀取數(shù)據(jù),然后通過網(wǎng)絡(luò)將它傳輸給其他的程序的例子。這個(gè)程序的核心,就是這么兩個(gè)調(diào)用:
File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);
這很容易理解。但是,在其內(nèi)部,卻有著四次用戶態(tài)和內(nèi)核態(tài)之間的切換,并且,數(shù)據(jù)被拷貝了四次。下面這張圖,展示了數(shù)據(jù)傳輸?shù)倪^程:
而下面這張圖,則展示了內(nèi)核態(tài)和用戶態(tài)之間切換的過程:
這四次用戶態(tài)和內(nèi)核態(tài)之間切換分別是:
- 當(dāng)調(diào)用read()時(shí),會(huì)導(dǎo)致一次從用戶態(tài)到內(nèi)核態(tài)的切換。read()的內(nèi)部是通過sys_read()來實(shí)現(xiàn)的。第一次數(shù)據(jù)復(fù)制,是數(shù)據(jù)通過DMA被從文件讀出并且放在了內(nèi)核的buffer中。
- 數(shù)據(jù)從內(nèi)核的讀緩沖區(qū)被復(fù)制到應(yīng)用程序的緩沖區(qū),然后read()調(diào)用返回。這就會(huì)導(dǎo)致一次從內(nèi)核態(tài)到用戶態(tài)的切換。現(xiàn)在,數(shù)據(jù)被存儲(chǔ)到了應(yīng)用程序的緩沖區(qū)中。
- send()調(diào)用又會(huì)導(dǎo)致一次從用戶態(tài)到內(nèi)核態(tài)的切換。這里發(fā)生了第三次數(shù)據(jù)復(fù)制,數(shù)據(jù)從應(yīng)用程序的緩沖區(qū)被復(fù)制到內(nèi)核的寫緩沖區(qū)。
- send()調(diào)用返回,這導(dǎo)致了第四次上下文切換。并且數(shù)據(jù)通過DMA從內(nèi)核寫緩沖區(qū)被發(fā)送到了協(xié)議棧。
即使中間的內(nèi)核緩沖區(qū)(而不是直接將數(shù)據(jù)傳輸?shù)綉?yīng)用程序的緩沖區(qū)中)似乎看起來效率很低。但是實(shí)際上,中間的內(nèi)核緩沖區(qū)是為了提升性能而存在的。對(duì)于內(nèi)核讀緩沖區(qū)來說,它可以實(shí)現(xiàn)預(yù)先讀,即,緩沖區(qū)中預(yù)先加載一些磁盤中的數(shù)據(jù),下次應(yīng)用程序在讀數(shù)據(jù)的時(shí)候,就不需要再去磁盤讀取,而是直接從內(nèi)核的讀緩沖區(qū)讀取就好了,這是因?yàn)椋醒芯勘砻鳎瑧?yīng)用程序總是傾向于讀取連續(xù)的數(shù)據(jù),即如果磁盤上的一塊數(shù)據(jù)被訪問到了,那么,在磁盤上,與這塊數(shù)據(jù)相鄰的其他數(shù)據(jù),也很快就會(huì)被訪問到。這就提高了讀性能。而對(duì)于寫緩沖區(qū)來說,它可以實(shí)現(xiàn)異步的寫操作。即,對(duì)于寫操作,將數(shù)據(jù)復(fù)制到內(nèi)核寫緩沖區(qū),基本上就可以認(rèn)為寫入成功,而不需要一直阻塞到TCP協(xié)議棧真的發(fā)送了數(shù)據(jù)并收到了響應(yīng)。
然而,內(nèi)核緩沖區(qū)也有一些缺點(diǎn)。想象這樣一種場景,應(yīng)用程序請(qǐng)求10KB的數(shù)據(jù),而內(nèi)核緩沖區(qū)僅有4KB,那么,就需要分三次來讀取磁盤數(shù)據(jù),并復(fù)制到應(yīng)用程序的緩沖區(qū)。對(duì)于內(nèi)核的緩沖區(qū)也是這樣。
通過zero-copy,就可以規(guī)避這些問題。
使用zero-copy實(shí)現(xiàn)的文件傳輸?shù)睦?/h2>
我們可以發(fā)現(xiàn),在上面的例子中,實(shí)際上,第二次和第三次數(shù)據(jù)復(fù)制并不是必須的。應(yīng)用程序僅僅是作為一個(gè)橋梁,將數(shù)據(jù)從內(nèi)核讀緩沖區(qū)傳輸?shù)絻?nèi)核寫緩沖區(qū)。實(shí)際上,數(shù)據(jù)可以直接從內(nèi)核的讀緩沖區(qū)寫入到socket的緩沖區(qū)。Java中的transferTo()就實(shí)現(xiàn)了這個(gè)操作。
transferTo()方法的聲明如下:
public void transferTo(long position, long count, WritableByteChannel target);
transferTo方法會(huì)將數(shù)據(jù)從FileChannel傳輸?shù)絎ritableByteChannel。但是,這個(gè)方法依賴于操作系統(tǒng)的底層實(shí)現(xiàn)。只有當(dāng)操作系統(tǒng)支持zero-copy時(shí),這個(gè)方法才有用。在UNIX以及Linux的不同發(fā)行版中,這個(gè)方法最終會(huì)調(diào)用sendfile()系統(tǒng)調(diào)用。
sendfile()的聲明如下:
#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
下圖展示了transferTo()方法的流程:
下圖展示了使用transferTo()方法時(shí),上下文切換的情況:
兩次上下文切換分別發(fā)生在:
- 在調(diào)用transferTo()方法時(shí),需要從用戶態(tài)轉(zhuǎn)換到內(nèi)核態(tài)。然后數(shù)據(jù)通過DMA被從文件讀到內(nèi)核讀緩沖區(qū)中。然后數(shù)據(jù)被從內(nèi)核讀緩沖區(qū)復(fù)制到Socket的緩沖區(qū)中。最后,數(shù)據(jù)通過DMA被放到了NIC buffer中.
- tranferTo()方法返回時(shí),需要從內(nèi)核態(tài)切換到用戶態(tài)。
即使這相對(duì)于用傳統(tǒng)方式實(shí)現(xiàn)的例子有了一些提升,比如,上下文切換次數(shù)從四次降到了兩次,數(shù)據(jù)需要被復(fù)制三次,而不是四次了。但是,這還不是zero-copy。要進(jìn)一步降低開銷,需要網(wǎng)絡(luò)接口直接一些特定的操作。從Linux內(nèi)核2.4之后,socket buffer descriptor就被修改了,來適應(yīng)這種需求。這個(gè)修改,不僅降低了上下文切換的次數(shù),而且消除了數(shù)據(jù)在被復(fù)制時(shí),涉及到的CPU消耗:
- transferTo()方法讓數(shù)據(jù)通過DMA被復(fù)制內(nèi)核讀緩沖區(qū)
- 沒有數(shù)據(jù)會(huì)被復(fù)制到socket buffer。socket buffer只會(huì)收到一個(gè)descriptor,它包含了數(shù)據(jù)的位置和長度信息。DMA直接將數(shù)據(jù)從內(nèi)核讀緩沖區(qū)復(fù)制到NIC Buffer中,因此,消除掉了CPU消耗。
性能比較
我們?cè)谝粋€(gè)內(nèi)核為2.6的Linux操作系統(tǒng)上,運(yùn)行了一些測試,最終得出了下面的結(jié)果:
我們可以看到,使用transferTo()實(shí)現(xiàn)的這種方式,相對(duì)于用傳統(tǒng)方式實(shí)現(xiàn),只需要大約65%的時(shí)間。