通過zero-copy進(jìn)行高效的數(shù)據(jù)傳輸(譯)

這篇文章翻譯自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.FileChanneltransferTo()函數(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)之間切換分別是:

  1. 當(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中。
  2. 數(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ū)中。
  3. 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ū)。
  4. 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ā)生在:

  1. 在調(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中.
  2. 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消耗:

  1. transferTo()方法讓數(shù)據(jù)通過DMA被復(fù)制內(nèi)核讀緩沖區(qū)
  2. 沒有數(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í)間。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • 轉(zhuǎn)自JAVA IO 以及 NIO 理解 一段話總結(jié):傳統(tǒng)io中從磁盤中中讀文件,并把文件通過網(wǎng)絡(luò)(socket)發(fā)...
    抓兔子的貓閱讀 1,371評(píng)論 0 4
  • 由于Netty,了解了一些異步IO的知識(shí),JAVA里面NIO就是原來的IO的一個(gè)補(bǔ)充,本文主要記錄下在JAVA中I...
    騷的掉渣閱讀 701評(píng)論 0 8
  • NIO(Non-blocking I/O,在Java領(lǐng)域,也稱為New I/O),是一種同步非阻塞的I/O模型,也...
    閃電是只貓閱讀 3,146評(píng)論 0 7
  • 什么是零拷貝 維基上是這么描述零拷貝的:零拷貝描述的是CPU不執(zhí)行拷貝數(shù)據(jù)從一個(gè)存儲(chǔ)區(qū)域到另一個(gè)存儲(chǔ)區(qū)域的任務(wù),這...
    tomas家的小撥浪鼓閱讀 25,481評(píng)論 11 61
  • 本文探討Linux中主要的幾種零拷貝技術(shù)以及零拷貝技術(shù)適用的場景。為了迅速建立起零拷貝的概念,我們拿一個(gè)常用的場景...
    卡巴拉的樹閱讀 65,533評(píng)論 13 112