零拷貝(Zero-Copy)用于在數(shù)據(jù)讀寫過程中減少不需要的CPU拷貝,CPU就那么幾個(gè),減少它的負(fù)擔(dān)自然可以提高處理效率。數(shù)據(jù)傳輸有本地的文件拷貝和通過socket進(jìn)行文件傳輸兩種,兩者區(qū)別不大,只是文件數(shù)據(jù)最終的去向仍然是本地磁盤還是網(wǎng)卡的區(qū)別,這里以socket文件為例介紹傳統(tǒng)IO演化至零拷貝的過程。
介紹零拷貝之前,可以先看一下傳統(tǒng)IO,借此熟悉一些相關(guān)概念,先上圖:
首先要知道操作系統(tǒng)已經(jīng)隔離了兩塊運(yùn)行空間,即用戶空間和內(nèi)核空間。可以理解為用戶程序是跑在用戶空間的,而操作系統(tǒng)的內(nèi)核代碼是跑在內(nèi)核空間的,把這兩個(gè)隔離是為了用戶程序的故障不影響操作系統(tǒng)。其實(shí)現(xiàn)代操作系統(tǒng)已經(jīng)對(duì)數(shù)據(jù)的拷貝做了優(yōu)化,之前把數(shù)據(jù)從底層硬件拷貝到內(nèi)核空間也是CPU來的,現(xiàn)在CPU只需要通知一下DMA(Direct Memory Access,直接內(nèi)存存取),拷貝工作就交給DMA了,這樣CPU就解放出來做其他事去了,所以現(xiàn)代操作系統(tǒng)底層硬件和內(nèi)核空間之間的數(shù)據(jù)拷貝CPU參與的很少可以不予考慮,都是DMA來的,但是內(nèi)核空間和用戶空間之間的活都是CPU親自上的。
從上圖可以看出,傳統(tǒng)IO是這么幾個(gè)步驟:
1.線程在用戶空間發(fā)起read()讀文件,線程從用戶態(tài)切換為內(nèi)核態(tài)
2.DMA將磁盤數(shù)據(jù)拷貝到內(nèi)核緩存后,CPU又將數(shù)據(jù)從內(nèi)核緩存拷貝至用戶緩存,這時(shí)線程又從內(nèi)核態(tài)切換為用戶態(tài)
3.這時(shí)候知道了數(shù)據(jù)應(yīng)該往哪里寫,CPU將數(shù)據(jù)從用戶緩存拷貝至socket緩存,線程又從用戶態(tài)切換到內(nèi)核態(tài)
4.最后DMA將數(shù)據(jù)從內(nèi)核緩存拷貝到網(wǎng)卡,read()調(diào)用結(jié)束返回,線程又從內(nèi)核態(tài)切換到用戶態(tài)
整個(gè)過程線程上下文切換了四次,一共有四次拷貝,2次CPU來的,2次DMA來的。觀察圖不經(jīng)會(huì)想,為啥數(shù)據(jù)要在用戶空間走一趟呢,能不能在內(nèi)核空間直接從內(nèi)核緩存到socket緩存呢,答案是可以的,這就是第一種零拷貝技術(shù)的原理,即mmap+write,先上圖:
mmap即內(nèi)存映射,mmap()是由unix/linux操作系統(tǒng)來調(diào)用的,它可以將內(nèi)核緩存中的一塊區(qū)域與用戶緩存中的一塊區(qū)域形成映射關(guān)系,即共享內(nèi)存,不過在用戶緩存中的這塊映射區(qū)域是堆外內(nèi)存。建立映射關(guān)系后,理解起來就是往其中任意一頭寫另外一頭也寫進(jìn)去了,這樣是為了省掉一次CPU拷貝,傳統(tǒng)IO要把數(shù)據(jù)從內(nèi)核緩存拷貝到用戶緩存才能寫,現(xiàn)在直接在用戶緩存寫,有了映射關(guān)系,對(duì)應(yīng)的那塊內(nèi)核緩存也有了。mmap+write實(shí)現(xiàn)的零拷貝流程是這樣的:
1.用戶進(jìn)程要讀一個(gè)磁盤文件,告訴內(nèi)核進(jìn)程發(fā)起mmap()函數(shù)調(diào)用,來來來把你的內(nèi)核緩存和我的一塊用戶緩存建立下映射關(guān)系,我要讀這個(gè)磁盤文件了。
2.內(nèi)核進(jìn)程乖乖調(diào)用了mmap()函數(shù),將一塊內(nèi)核緩存和用戶緩存中的一塊堆外內(nèi)存建立的映射關(guān)系。并且告訴DMA將這個(gè)文件中的數(shù)據(jù)拷貝到了這塊內(nèi)核緩存中。到這里mmap()函數(shù)就調(diào)用結(jié)束了,任務(wù)完成。嚴(yán)格的說到這里為止都不算IO過程,因此也沒有統(tǒng)計(jì)線程的上下文切換次數(shù)。
3.這才開始IO,因?yàn)榇疟P文件已經(jīng)被DMA拷貝到內(nèi)核緩存中去了,又被映射到了這塊堆外內(nèi)存,所以就直接在用戶緩存里就讀到了,線程沒有上下文切換,然后準(zhǔn)備寫進(jìn)一塊socket緩存里去了,線程發(fā)起了write()調(diào)用,狀態(tài)由用戶態(tài)切換為內(nèi)核態(tài),這時(shí)候內(nèi)核基于CPU拷貝將數(shù)據(jù)從那塊映射著的內(nèi)核緩存拷貝到socket緩存,CPU也就拷貝了這一次。
4.然后又是DMA將數(shù)據(jù)從socket緩存拷貝到網(wǎng)卡,最后write()函數(shù)調(diào)用返回,線程從內(nèi)核態(tài)切換到用戶態(tài)。
整個(gè)過程線程切換了兩次,一共有三次拷貝,其中2次DMA拷貝,1次CPU拷貝。到這里CPU已經(jīng)輕松不少了,就拷貝了一次嘛,可以不是說好的零拷貝的嘛,怎么還有一次拷貝,然后sendfile()函數(shù)就登場(chǎng)了,它是實(shí)實(shí)在在的實(shí)現(xiàn)了零拷貝,先上圖:
sendfile()也是操作系統(tǒng)來調(diào)用的,用戶線程只能通過特定的方法發(fā)起調(diào)用,比如java.nio包下的FileChannel,它的transferTo()方法可以發(fā)起sendfile()函數(shù)的調(diào)用。sendfile()函數(shù)實(shí)現(xiàn)零拷貝的過程是這樣的:
1.用戶線程發(fā)起sendfile()函數(shù)調(diào)用,與mmap()函數(shù)不同的是,不單單告訴內(nèi)核去哪里讀數(shù)據(jù),往哪里寫數(shù)據(jù)也一起告訴內(nèi)核了。這時(shí)候就已經(jīng)開始算IO了,線程從用戶態(tài)切換到了內(nèi)核態(tài)。
2.知道了從哪里讀數(shù)據(jù),依然是DMA去磁盤里把數(shù)據(jù)拷貝到內(nèi)核緩存中去,由于同時(shí)也知道了應(yīng)該往哪里寫數(shù)據(jù),那就接著干活唄。
3.先把數(shù)據(jù)描述信息從內(nèi)核緩存復(fù)制到指定的socket緩存,然后DMA又來了,這個(gè)時(shí)候socket緩存中的數(shù)據(jù)描述信息就起作用了,這些描述信息主要是數(shù)據(jù)的位置信息等。DMA Gather通過這些數(shù)據(jù)描述信息將數(shù)據(jù)從內(nèi)核緩存拷貝到網(wǎng)卡。
4.sendfile()函數(shù)調(diào)用結(jié)束,線程從內(nèi)核態(tài)切換到了用戶態(tài),CPU一次拷貝都沒有!零!
這就是真正的零拷貝,整個(gè)過程用戶線程切換了兩次,只有兩次拷貝,但都是DMA來的。
關(guān)于第三種零拷貝方式,這是Linux2.4對(duì)sendfile做了改進(jìn)之后的零拷貝。其實(shí)linux 2.1 內(nèi)核開始就引入了sendfile()函數(shù),當(dāng)時(shí)的零拷貝是這樣的。
可以看出整個(gè)過程用戶線程切換了兩次,有三次拷貝,兩次DMA來的,還是有一次CPU拷貝。這種零拷貝方式和mmap+write方式有點(diǎn)類似,但是這也算零拷貝演進(jìn)過程中的一環(huán)。
sendfile()函數(shù)的man page里面有這句話: In Linux kernels before 2.6.33, out_fd must refer to a socket. Since Linux 2.6.33 it can be any file. 也就是說Linux2.6.33之前sendfile()只能用于文件到socket的傳輸。而Linux2.6.33之后可以用于兩個(gè)文件描述符之間和文件到socket之間的傳輸。
-
IO
+關(guān)注
關(guān)注
0文章
448瀏覽量
39134 -
cpu
+關(guān)注
關(guān)注
68文章
10855瀏覽量
211595 -
數(shù)據(jù)
+關(guān)注
關(guān)注
8文章
7006瀏覽量
88944
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論