包教包會(huì)的零拷貝
這一篇的主題是零拷貝這個(gè)技術(shù)點(diǎn)!
我們接下來從下面這幾個(gè)問題的角度來給全方面分析零拷貝這個(gè)技術(shù)點(diǎn),一邊讀不懂的同學(xué),趕緊收藏,讀多幾遍就懂了
還有還有,收藏起來,等以后忘記了或者快要面試的時(shí)候,可以逃出來熟悉熟悉
畢竟,好記性不如爛筆頭的嘞

為什么要有 DMA 技術(shù)?
我們先來看一下在沒有DMA技術(shù)之前的IO過程:
?
1、CPU發(fā)出對(duì)應(yīng)的指令到磁盤系統(tǒng),然后返回
?
2、磁盤系統(tǒng)收到指令,把數(shù)據(jù)放入到磁盤系統(tǒng)的內(nèi)部緩沖區(qū)中,然后產(chǎn)生一個(gè)中斷指令
?
3、CPU收到中斷信號(hào),停止當(dāng)前工作,緊接著把磁盤系統(tǒng)緩沖區(qū)中的數(shù)據(jù)讀到自己的寄存器內(nèi),然后把寄存器的數(shù)據(jù)寫入到內(nèi)存,在此數(shù)據(jù)傳輸期間CPU無法執(zhí)行其它工作
?
畫了一個(gè)圖幫助大家理解

聰明的小伙伴已經(jīng)發(fā)現(xiàn)其中的弊端了,就是數(shù)據(jù)傳輸期間,CPU無法執(zhí)行其它命令
?
我們知道CPU是中央處理器,這個(gè)東西的性能能省就省,能扣著點(diǎn)用就扣著點(diǎn)用,畢竟整個(gè)機(jī)器都要用這家伙
?
簡單的搬運(yùn)幾個(gè)字符數(shù)據(jù)肯定沒啥問題,但是如果傳輸大量數(shù)據(jù)的時(shí)候都需要CPU來搬運(yùn),那就很糟糕了
?
于是,DMA技術(shù)就誕生了,就是直接內(nèi)存訪問技術(shù)Direct?Memory Access
?
DMA技術(shù),就是在進(jìn)行IO設(shè)備和內(nèi)存之間數(shù)據(jù)傳輸?shù)臅r(shí)候,數(shù)據(jù)搬運(yùn)的工作全部交給DMA控制器,而CPU不再參與任何和數(shù)據(jù)搬運(yùn)相關(guān)的事情了,這樣就把CPU空出來了
?
具體來看一下使用DMA控制器的流程

1、用戶調(diào)用read,先操作系統(tǒng)發(fā)起IO請(qǐng)求,請(qǐng)求讀取數(shù)據(jù)到自己的內(nèi)存緩沖區(qū),然后進(jìn)入阻塞
?
2、操作系統(tǒng)收到請(qǐng)求,把IO請(qǐng)求發(fā)給了DMA,然后CPU執(zhí)行其它任務(wù),DMA發(fā)送給磁盤
?
3、磁盤收到IO請(qǐng)求,把數(shù)據(jù)放入到自己的緩沖區(qū),磁盤系統(tǒng)緩沖區(qū)滿的時(shí)候,向DMA發(fā)起中斷指令
?
4、DMA收到中斷指令,將磁盤緩沖區(qū)數(shù)據(jù)拷貝到內(nèi)核緩沖區(qū),不占用CPU
?
5、DMA讀取了足夠多數(shù)據(jù),發(fā)送中斷信號(hào)給CPU
?
6、CPU收到DMA信號(hào),知道數(shù)據(jù)準(zhǔn)備好了,將數(shù)據(jù)從內(nèi)核拷貝到用戶空間
?
看整個(gè)過程,發(fā)現(xiàn)CPU不再參與數(shù)據(jù)搬運(yùn)的工作,而是由DMA完成的,但是呢,CPU在這個(gè)過程也是必不可少,因?yàn)閭鬏斒裁?,從哪里傳輸?shù)侥睦镄枰?span style="font-family: Calibri;">CPU來告訴DMA控制器
?
這就像創(chuàng)業(yè)公司,老板自己干活忙不過來了,就招了一個(gè)秘書,但是,這個(gè)秘書操作什么,如何操作,還是得聽老板的指揮
?
早期 DMA 只存在在主板上,如今由于 I/O 設(shè)備越來越多,數(shù)據(jù)傳輸?shù)男枨笠膊槐M相同,所以每個(gè) I/O 設(shè)備里面都有自己的 DMA 控制器。
? ? ? ??
?
傳統(tǒng)的傳輸文件
?
先來給大家簡單說一下用戶空間和內(nèi)核空間,比如我們部署一個(gè)Java程序到一臺(tái)Linux服務(wù)器上,我們可以認(rèn)為JVM的區(qū)域就是用戶空間,其余的空間就是內(nèi)核空間,用戶空間和內(nèi)核空間對(duì)于系統(tǒng)文件的操作權(quán)限是不一樣的
?
傳統(tǒng)的文件傳輸?shù)墓ぷ鞣绞剑簲?shù)據(jù)讀取和寫入是從用戶空間和內(nèi)核空間來回復(fù)制,而內(nèi)核空間的數(shù)據(jù)是通過操作系統(tǒng)層面的IO接口從磁盤讀取或者寫入
?
代碼通常如下,一般會(huì)需要兩個(gè)系統(tǒng)調(diào)用:
read(file,?tmp_buf,?len);write(socket, tmp_buf, len);
看這兩行代碼做了啥

兩次系統(tǒng)調(diào)用,發(fā)生了4次用戶態(tài)和內(nèi)核態(tài)的上下文切換,每次系統(tǒng)調(diào)用都得先從用戶態(tài)切換到內(nèi)核態(tài),然后等內(nèi)核態(tài)完成任務(wù),再切換回到用戶態(tài)
?
一次上下文的切換耗時(shí)幾十納秒到幾微秒,時(shí)間看上去很短,但是在高并發(fā)的場景下,這類時(shí)間就會(huì)變得不可忽視,從而影響系統(tǒng)的性能
?
中間還發(fā)生了4次數(shù)據(jù)拷貝,其中兩次是DMA的拷貝,DMA技術(shù)是優(yōu)化IO設(shè)備到內(nèi)核區(qū)的,另外兩次是通過CPU拷貝用戶緩沖區(qū)的
?
1、第一次拷貝,磁盤上的數(shù)據(jù)通過DMA技術(shù)拷貝到操作系統(tǒng)的內(nèi)核區(qū)中
?
2、第二次拷貝,CPU把內(nèi)核緩沖區(qū)數(shù)據(jù)拷貝到用戶緩沖區(qū)中
?
3、第三次拷貝,CPU將用戶緩沖區(qū)的數(shù)據(jù)搬運(yùn)到內(nèi)核緩沖區(qū)中
?
4、第四次拷貝,通過DMA技術(shù)把內(nèi)核數(shù)據(jù)搬運(yùn)到網(wǎng)卡的緩沖區(qū)中
?
問題:我們搬運(yùn)一次數(shù)據(jù),中間卻復(fù)制了4次,過多的上下文切換和過多的數(shù)據(jù)拷貝都會(huì)降低系統(tǒng)性能,所以,如果想提高文件傳輸?shù)男阅埽托枰獪p少用戶態(tài)和內(nèi)核態(tài)的上下文切換和內(nèi)容拷貝的次數(shù)
?
優(yōu)化思路
?
減少用戶態(tài)和內(nèi)核態(tài)之間的上下文切換
?
之所以發(fā)生上下文的切換,是因?yàn)橛脩艨臻g沒有權(quán)限操作磁盤或者網(wǎng)卡,內(nèi)核的權(quán)限最高,這些操作設(shè)備的過程都需要交給操作系統(tǒng)的內(nèi)核來完成,一次系統(tǒng)調(diào)用也就意味著必然發(fā)生2次上下文的切換,首先從用戶態(tài)切換到內(nèi)核態(tài),內(nèi)核態(tài)執(zhí)行完任務(wù)之后再切換到用戶態(tài)執(zhí)行相應(yīng)進(jìn)程的代碼指令
?
所以,要減少上下文切換的次數(shù),就需要減少系統(tǒng)調(diào)用的次數(shù)
?
減少數(shù)據(jù)拷貝的次數(shù)
?
數(shù)據(jù)傳輸?shù)?/span>4次拷貝,其中內(nèi)核拷貝到用戶緩沖區(qū),再從用戶緩沖區(qū)拷貝到內(nèi)核緩沖區(qū),這兩個(gè)過程是沒必要的,因?yàn)樵谖募鬏數(shù)膽?yīng)用場景中,在用戶空間我們并不會(huì)對(duì)數(shù)據(jù)再加工,所以這個(gè)數(shù)據(jù)沒必要搬運(yùn)到用戶空間
? ? ? ? ? ??
如何實(shí)現(xiàn)零拷貝?
零拷貝技術(shù)實(shí)現(xiàn)的方式通常有 2 種:
mmap + write(三次拷貝+兩次系統(tǒng)調(diào)用)Sendfile(三次拷貝+一次系統(tǒng)調(diào)用)
下面就談一談,它們是如何減少「上下文切換」和「數(shù)據(jù)拷貝」的次數(shù)。
mmap + write
在前面我們知道,read() 系統(tǒng)調(diào)用的過程中會(huì)把內(nèi)核緩沖區(qū)的數(shù)據(jù)拷貝到用戶的緩沖區(qū)里,于是為了減少這一步開銷,我們可以用 mmap() 替換 read() 系統(tǒng)調(diào)用函數(shù)。
buf?=?mmap(file,?len);write(sockfd, buf, len);
mmap() 系統(tǒng)調(diào)用函數(shù)會(huì)直接把內(nèi)核緩沖區(qū)里的數(shù)據(jù)「映射」到用戶空間,這樣,操作系統(tǒng)內(nèi)核與用戶空間就不需要再進(jìn)行任何的數(shù)據(jù)拷貝操作。

具體過程如下:
?
1、應(yīng)用調(diào)用了mmap(),DMA把磁盤數(shù)據(jù)拷貝到內(nèi)核緩沖區(qū),此時(shí),應(yīng)用進(jìn)程和內(nèi)核會(huì)共享這個(gè)內(nèi)核緩沖區(qū)
?
2、應(yīng)用系統(tǒng)調(diào)用write(),操作系統(tǒng)直接把內(nèi)核緩沖區(qū)數(shù)據(jù)拷貝到網(wǎng)絡(luò)緩沖區(qū)中,這個(gè)也是屬于內(nèi)核態(tài),內(nèi)核中的拷貝,由CPU來操作
?
3、第三次拷貝,通過DMA技術(shù)把網(wǎng)絡(luò)緩沖區(qū)數(shù)據(jù)拷貝到網(wǎng)卡的緩沖區(qū)中
?
我們可以得知,通過使用mmap()來代替 read(),可以減少一次數(shù)據(jù)拷貝的過程。
?
但這還不是最理想的零拷貝,因?yàn)槿匀恍枰ㄟ^ CPU 把內(nèi)核緩沖區(qū)的數(shù)據(jù)拷貝到 socket 緩沖區(qū)里,而且仍然需要 4 次上下文切換,因?yàn)橄到y(tǒng)調(diào)用還是 2 次。
?
Sendfile
?
在 Linux 內(nèi)核版本 2.1 中,提供了一個(gè)專門發(fā)送文件的系統(tǒng)調(diào)用函數(shù) sendfile(),函數(shù)形式如下:
#include?ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
它的前兩個(gè)參數(shù)分別是目的端和源端的文件描述符,后面兩個(gè)參數(shù)是源端的偏移量和復(fù)制數(shù)據(jù)的長度,返回值是實(shí)際復(fù)制數(shù)據(jù)的長度。
?
首先,它可以替代前面的 read() 和 write() 這兩個(gè)系統(tǒng)調(diào)用,這樣就可以減少一次系統(tǒng)調(diào)用,也就減少了 2 次上下文切換的開銷。
?
其次,該系統(tǒng)調(diào)用,可以直接把內(nèi)核緩沖區(qū)里的數(shù)據(jù)拷貝到 socket 緩沖區(qū)里,不再拷貝到用戶態(tài),這樣就只有 2 次上下文切換,和 3 次數(shù)據(jù)拷貝。
如下圖

但是這還不是真正的零拷貝技術(shù),如果網(wǎng)卡支持 SG-DMA(The Scatter-Gather Direct Memory Access)技術(shù)(和普通的 DMA 有所不同),我們可以進(jìn)一步減少通過 CPU 把內(nèi)核緩沖區(qū)里的數(shù)據(jù)拷貝到 socket 緩沖區(qū)的過程。
?
你可以在你的 Linux 系統(tǒng)通過下面這個(gè)命令,查看網(wǎng)卡是否支持 scatter-gather 特性:
$?ethtool?-k?eth0?|?grep?scatter-gatherscatter-gather: on
于是,從 Linux 內(nèi)核 2.4 版本開始起,對(duì)于支持網(wǎng)卡支持 SG-DMA 技術(shù)的情況下, sendfile() 系統(tǒng)調(diào)用的過程發(fā)生了點(diǎn)變化,具體過程如下:
?
1、DMA直接將磁盤上的數(shù)據(jù)拷貝到內(nèi)核緩沖區(qū)中
?
2、緩沖區(qū)描述符和數(shù)據(jù)長度傳到?socket 緩沖區(qū),這樣網(wǎng)卡的 SG-DMA 控制器就可以直接將內(nèi)核緩存中的數(shù)據(jù)拷貝到網(wǎng)卡的緩沖區(qū)里,此過程不需要將數(shù)據(jù)從操作系統(tǒng)內(nèi)核緩沖區(qū)拷貝到 socket 緩沖區(qū)中,這樣就減少了一次數(shù)據(jù)拷貝
?
所以,這個(gè)過程之中,只進(jìn)行了 2 次數(shù)據(jù)拷貝,如下圖:

這就是所謂的零拷貝(Zero-copy)技術(shù),因?yàn)槲覀儧]有在內(nèi)存層面去拷貝數(shù)據(jù),也就是說全程沒有通過 CPU 來搬運(yùn)數(shù)據(jù),所有的數(shù)據(jù)都是通過 DMA 來進(jìn)行傳輸?shù)摹?/span>
?
CPU屬于參與了,但沒完全參與,DMA操作需要CPU指揮,描述符和數(shù)據(jù)長度需要CPU發(fā)送
?
零拷貝技術(shù)的文件傳輸方式相比傳統(tǒng)文件傳輸?shù)姆绞剑瑴p少了 2 次上下文切換和數(shù)據(jù)拷貝次數(shù),只需要 2 次上下文切換和數(shù)據(jù)拷貝次數(shù),就可以完成文件的傳輸,而且 2 次的數(shù)據(jù)拷貝過程,都不需要通過 CPU,2 次都是由 DMA 來搬運(yùn)。
?
我們通常說的這個(gè)零拷貝技術(shù)中的這個(gè)零,指的是內(nèi)核態(tài)和用戶態(tài)之間的拷貝次數(shù),變成了0
所以,總體來看,零拷貝技術(shù)可以把文件傳輸?shù)男阅芴岣咧辽僖槐兑陨稀?/span>
?
PageCache
?
上面說的第一步是先把磁盤文件數(shù)據(jù)拷貝到內(nèi)核緩沖區(qū)中,這個(gè)內(nèi)核緩沖區(qū)就是磁盤高速緩沖區(qū)PageCache,內(nèi)存速度比磁盤速度快,但是內(nèi)存空間比磁盤要小
?
我們需要把此時(shí)的熱點(diǎn)數(shù)據(jù)放入到緩存中,因?yàn)檫@是最近需要頻繁訪問的,空間不足的時(shí)候淘汰掉那些訪問頻率低的數(shù)據(jù)
?
緩存這些道理大家應(yīng)該都懂,零拷貝也使用了緩存技術(shù),讀取數(shù)據(jù)的時(shí)候,優(yōu)先在PageCache中找,找到直接返回,找不到去磁盤中讀取,然后緩存到PageCache中
?
還有一點(diǎn),讀取磁盤數(shù)據(jù)的時(shí)候,需要找到數(shù)據(jù)所在的位置,但是對(duì)于機(jī)械磁盤來說,就是通過磁頭旋轉(zhuǎn)到數(shù)據(jù)所在的扇區(qū),再開始「順序」讀取數(shù)據(jù),但是旋轉(zhuǎn)磁頭這個(gè)物理動(dòng)作是非常耗時(shí)的,為了降低它的影響,PageCache 使用了「預(yù)讀功能」。
?
比如,假設(shè) read 方法每次只會(huì)讀 32 KB 的字節(jié),雖然 read 剛開始只會(huì)讀 0 ~ 32 KB 的字節(jié),但內(nèi)核會(huì)把其后面的 32~64 KB 也讀取到 PageCache,這樣后面讀取 32~64 KB 的成本就很低,如果在 32~64 KB 淘汰出 PageCache 前,進(jìn)程讀取到它了,收益就非常大。
?
所以,PageCache 的優(yōu)點(diǎn)主要是兩個(gè):
緩存最近被訪問的數(shù)據(jù);預(yù)讀功能;
這兩個(gè)做法,將大大提高讀寫磁盤的性能。
?
但是,在傳輸大文件(GB 級(jí)別的文件)的時(shí)候,PageCache 會(huì)不起作用,那就白白浪費(fèi) DMA 多做的一次數(shù)據(jù)拷貝,造成性能的降低,即使使用了 PageCache 的零拷貝也會(huì)損失性能,一個(gè)大文件直接占滿,導(dǎo)致某些熱點(diǎn)小文件無法使用,性能就降低了
?
所以,針對(duì)大文件的傳輸,不應(yīng)該使用 PageCache,也就是說不應(yīng)該使用零拷貝技術(shù),因?yàn)榭赡苡捎?PageCache 被大文件占據(jù),而導(dǎo)致「熱點(diǎn)」小文件無法利用到 PageCache,這樣在高并發(fā)的環(huán)境下,會(huì)帶來嚴(yán)重的性能問題。
?
對(duì)于大文件傳輸,可以通過異步IO和繞開PageCache的IO來代替零拷貝技術(shù)
?
在 nginx 中,我們可以用如下配置,來根據(jù)文件的大小來使用不同的方式:
location?/video/?{????sendfile?on;????aio?on;????directio?1024m;}
當(dāng)文件大小大于 directio 值后,使用「異步 I/O + 直接 I/O」,否則使用「零拷貝技術(shù)」。
? ? ? ??
?
總結(jié)
?
1、早期IO,內(nèi)核數(shù)據(jù)需要IO進(jìn)行復(fù)制,2次系統(tǒng)調(diào)用,4次上下文切換,4次數(shù)據(jù)的拷貝,CPU拷貝數(shù)據(jù)期間不能執(zhí)行其它命令
?
2、引入DMA技術(shù),DMA可以代替CPU進(jìn)行磁盤到內(nèi)核區(qū)域數(shù)據(jù)的復(fù)制,這個(gè)期間CPU可執(zhí)行其它命令,改善了性能
?
3、零拷貝技術(shù):mmap+write,2次系統(tǒng)調(diào)用,4次上下文切換,3次數(shù)據(jù)的拷貝,減少了讀取期間內(nèi)核區(qū)域到用戶區(qū)域的數(shù)據(jù)復(fù)制,原因是兩者共享了內(nèi)核區(qū)域的緩沖區(qū)
?
4、零拷貝技術(shù):Sendfile,1次系統(tǒng)調(diào)用,2次上下文切換,3次數(shù)據(jù)的拷貝,直接指定了原文件和目標(biāo)文件,替代了原來的兩次系統(tǒng)調(diào)用,直接一次完成
?
5、真正的零拷貝技術(shù):網(wǎng)卡支持 SG-DMA技術(shù),數(shù)據(jù)從磁盤系統(tǒng)讀取到內(nèi)核緩沖區(qū)之后,不需要復(fù)制到相應(yīng)的socket緩沖區(qū)即可,只需要發(fā)送描述符和數(shù)據(jù)長度即可,這個(gè)期間經(jīng)歷了1次系統(tǒng)調(diào)用,2次上下文切換,2次數(shù)據(jù)拷貝,沒有在內(nèi)核層面去進(jìn)行數(shù)據(jù)的拷貝
?
6、零拷貝技術(shù)引用PageCache緩存技術(shù),緩存技術(shù)用于加速熱點(diǎn)文件的查詢速度,但是不適用于大文件,大文件可以通過異步IO和繞開PageCache的IO來代替零拷貝技術(shù)
?
參考文獻(xiàn):https://zhuanlan.zhihu.com/p/258513662
結(jié)束語
感謝大家能夠做我最初的讀者和傳播者,請(qǐng)大家相信,只要你給我一份愛,我終究會(huì)還你們一頁情的。
左耳君會(huì)持續(xù)更新技術(shù)文章,和生活中的暴躁文章,歡迎大家關(guān)注,我們一起乘千里風(fēng)、破萬里浪
哦對(duì)了,后續(xù)所有的文章都會(huì)更新到這里
https://github.com/DayuMM2021/Java

