談?wù)劚尘?/h2>
第一次接觸零拷貝,噼里啪啦各種雜談概念內(nèi)核摆碉、上下文切換巷帝、DMA扫夜、MMAP....看了不少文章,不知道你是否也覺(jué)得是云里霧里棍厂,纏繞不清。也許把一件事情說(shuō)清楚勋桶,首先要貼近程序員能夠感觸到的“0距離”的場(chǎng)景侥猬。
也許你覺(jué)得零拷貝是面試大綱中常見(jiàn)的一綱退唠,毫無(wú)用處瞧预。但你每天確實(shí)都在接觸它,你卻并未發(fā)現(xiàn)它盆驹。比如:rocketMQ一死、Kafka的消費(fèi)者。
你細(xì)品梧兼,為什么這兩者會(huì)涉及到零拷貝呢?拷貝——Ctrl+C
再熟悉不過(guò)的騷操作焦履。
消費(fèi)者發(fā)起消費(fèi)的過(guò)程是這樣的:將數(shù)據(jù)從磁盤(pán)讀取出來(lái)裁良,通過(guò)網(wǎng)絡(luò)傳輸傳遞給消費(fèi)者校套。而這其中將數(shù)據(jù)從磁盤(pán)到網(wǎng)卡的過(guò)程笛匙,就是數(shù)據(jù)拷貝。數(shù)據(jù)移動(dòng)肯定需要資源消耗秋柄,比如CPU骇笔、上下文切換等。然而簡(jiǎn)簡(jiǎn)單單的數(shù)據(jù)拷貝的過(guò)程懦傍,內(nèi)部的數(shù)據(jù)流動(dòng)并不簡(jiǎn)單粗俱。因此看了下面的介紹寸认,你一定會(huì)明白為什么要零拷貝串慰?
傳統(tǒng)的IO拷貝
舉例
以下是消費(fèi)者消費(fèi)數(shù)據(jù)為例邦鲫,為了模擬數(shù)據(jù)從磁盤(pán)到網(wǎng)卡的過(guò)程,我借用一段代碼,讓Java同學(xué)能感受到我們?cè)谧鍪裁刺墼铮_切的說(shuō)醉者,我們是在解釋為什么傳統(tǒng)IO不是很理想:
// 模擬讀取topic_data.db這個(gè)數(shù)據(jù)文件
File file = new File("D://topic_data.db");
RandomAccessFile raf = new RandomAccessFile(file, "rw");
byte[] arr = new byte[(int) file.length()];
raf.read(arr);
// 將讀取的字節(jié)碼通過(guò)socket傳輸出去
Socket socket = new ServerSocket(8080).accept();
socket.getOutputStream().write(arr);
圖文說(shuō)明
以下是我結(jié)合網(wǎng)上資料撬即,手繪了一張傳統(tǒng)IO讀寫(xiě)的流程圖呈队,來(lái)解釋上述代碼的執(zhí)行流程:
內(nèi)核空間與用戶(hù)空間
:為了保證內(nèi)核的安全宪摧,現(xiàn)在的操作系統(tǒng)一般都強(qiáng)制用戶(hù)進(jìn)程不能直接操作內(nèi)核。具體的實(shí)現(xiàn)方式基本都是由操作系統(tǒng)將虛擬地址空間劃分為兩部分沿后,一部分為內(nèi)核空間尖滚,另一部分為用戶(hù)空間瞧柔。
讀流程和寫(xiě)流程大致是這樣的非剃,如果不是特別好理解,建議強(qiáng)迫記憶券坞,因?yàn)檫@是以下能夠繼續(xù)探究的基礎(chǔ):
- 應(yīng)用程序調(diào)用內(nèi)核指令讀取文件恨锚。
- 文件通過(guò)DMA控制器拷貝到內(nèi)核緩沖區(qū)(
ReadBuffer
) - cpu將內(nèi)核緩存沖區(qū)的數(shù)據(jù)拷貝到應(yīng)用程序緩沖區(qū)猴伶。
- cpu將應(yīng)用程序緩沖區(qū)的數(shù)據(jù)拷貝到內(nèi)核緩沖區(qū)(
SocketBuffer
) - 通過(guò)DMA控制器將數(shù)據(jù)拷貝至網(wǎng)卡
- 拷貝完成后他挎,通知應(yīng)用程序捡需。
那么站辉,DMA又是個(gè)啥?DMA這東東翻譯過(guò)來(lái)叫直接內(nèi)存訪(fǎng)問(wèn)
,顧名思義殊霞,直接訪(fǎng)問(wèn)到內(nèi)存绷蹲。你想想瘸右,將數(shù)據(jù)從一塊區(qū)域拷貝到另外一塊區(qū)域,cpu肯定得負(fù)責(zé)搬運(yùn)苞俘。而這個(gè)DMA的誕生讓cpu盡量不參與搬運(yùn)吃谣,更多的時(shí)間去處理其他的事情做裙。你可以參考正規(guī)的解釋?zhuān)?/p>
Direct Memory Access(存儲(chǔ)器直接訪(fǎng)問(wèn))锚贱。這是指一種高速的數(shù)據(jù)傳輸操作拧廊,允許在外部設(shè)備和存儲(chǔ)器之間直接讀寫(xiě)數(shù)據(jù)。整個(gè)數(shù)據(jù)傳輸操作在一個(gè)稱(chēng)為"DMA控制器"的控制下進(jìn)行的凰盔。CPU除了在數(shù)據(jù)傳輸開(kāi)始和結(jié)束時(shí)做一點(diǎn)處理外(開(kāi)始和結(jié)束時(shí)候要做中斷處理)户敬,在傳輸過(guò)程中CPU可以進(jìn)行其他的工作(前提是未設(shè)置停止CPU訪(fǎng)問(wèn))尿庐。這樣屁倔,在大部分時(shí)間里暮胧,CPU和輸入輸出都處于并行操作往衷。因此席舍,使整個(gè)計(jì)算機(jī)系統(tǒng)的效率大大提高哮笆。
探究細(xì)節(jié)
簡(jiǎn)單的流程梳理,我們對(duì)一些細(xì)節(jié)做下統(tǒng)計(jì):
- 上下文切換次數(shù)(圖1中的粉色圓圈):
4次
- 數(shù)據(jù)拷貝次數(shù)(圖1中的綠色圓圈):
4次
- cpu參與次數(shù):
2次
很明顯萝毛,每次操作都需要內(nèi)核及硬件的成本付出笆包,如何減少對(duì)應(yīng)的次數(shù)就是零拷貝
真正要解決的問(wèn)題略荡。
上下文切換
為什么用戶(hù)空間切換到內(nèi)核空間開(kāi)銷(xiāo)比較大汛兜?甚至有人叫這破玩意叫上下文切換?我摘抄一段文字粥谬。你可以磨一磨帝嗡、品一品:
當(dāng)程序中有系統(tǒng)調(diào)用語(yǔ)句,程序執(zhí)行到系統(tǒng)調(diào)用時(shí)狮辽,首先使用類(lèi)似int 80H的軟中斷指令喉脖,保存現(xiàn)場(chǎng)树叽,去系統(tǒng)調(diào)用谦絮,在內(nèi)核態(tài)執(zhí)行层皱,然后恢復(fù)現(xiàn)場(chǎng)叫胖,每個(gè)進(jìn)程都會(huì)有兩個(gè)棧,一個(gè)內(nèi)核態(tài)棧和一個(gè)用戶(hù)態(tài)棧怎棱。當(dāng)int中斷執(zhí)行時(shí)就會(huì)由用戶(hù)態(tài)棧轉(zhuǎn)向內(nèi)核態(tài)棧拳恋。系統(tǒng)調(diào)用時(shí)需要進(jìn)行棧的切換诅岩。而且內(nèi)核代碼對(duì)用戶(hù)不信任吩谦,需要進(jìn)行額外的檢查。系統(tǒng)調(diào)用的返回過(guò)程有很多額外工作咐扭,比如檢查是否需要調(diào)度等蝗肪。
系統(tǒng)調(diào)用一般都需要保存用戶(hù)程序的上下文(context), 在進(jìn)入內(nèi)核的時(shí)候需要保存用戶(hù)態(tài)的寄存器薛闪,在內(nèi)核態(tài)返回用戶(hù)態(tài)的時(shí)候會(huì)恢復(fù)這些寄存器的內(nèi)容豁延。這是一個(gè)開(kāi)銷(xiāo)的地方诱咏。 如果需要在不同用戶(hù)程序間切換的話(huà)缴挖,那么還要更新cr3寄存器映屋,這樣會(huì)更換每個(gè)程序的虛擬內(nèi)存到物理內(nèi)存映射表的地址棚点,也是一個(gè)比較高負(fù)擔(dān)的操作乙濒。
再談零拷貝
講了這么多傳統(tǒng)IO,目的是為了理解零拷貝做鋪墊么库,零拷貝是基于傳統(tǒng)IO的改進(jìn)版诉儒。
在開(kāi)始之前忱反,我們先看看什么是虛擬內(nèi)存地址:
虛擬內(nèi)存地址
所有現(xiàn)代操作系統(tǒng)都使用虛擬內(nèi)存温算,使用虛擬地址取代物理地址注竿,這樣做的好處就是:
1魂贬、多個(gè)虛擬內(nèi)存可以指向同一個(gè)物理地址
2付燥、虛擬內(nèi)存空間可以遠(yuǎn)遠(yuǎn)大于物理內(nèi)存空間
如果把圖1
中內(nèi)核空間
和用戶(hù)空間
的虛擬地址映射到同一個(gè)物理地址键科,就不需要cpu將數(shù)據(jù)在內(nèi)核空間
和用戶(hù)空間
來(lái)回拷貝萝嘁。
mmap+write與sendfile
mmap+write就是利用虛擬內(nèi)存地址的方式牙言,減少內(nèi)核空間和用戶(hù)空間的數(shù)據(jù)拷貝咱枉,從而減少數(shù)據(jù)拷貝次數(shù)蚕断。我們看下mmap+write的讀寫(xiě)流程:
從上圖可以看出亿乳,mmap與傳統(tǒng)IO讀流程的區(qū)別只是在內(nèi)核空間與用戶(hù)空間數(shù)據(jù)采用的虛擬內(nèi)存地址的方式共享內(nèi)存,減少了一次Cpu的數(shù)據(jù)拷貝滋恬,然而恢氯,上下文切換次數(shù)并未減少勋拟。
write()流程如下:
由于應(yīng)用程序緩沖區(qū)與內(nèi)核緩沖區(qū)共享內(nèi)存敢靡,cpu
只需要將ReadBuffer
數(shù)據(jù)拷貝到SocketBuffer
醋安。
那么吓揪,來(lái)綜合看下mmap+write的方式成本消耗如何柠辞?
- 上下文切換次數(shù):
4次
- 數(shù)據(jù)拷貝次數(shù):
3次
- cpu參與次數(shù):
1次
mmap+write
相對(duì)傳統(tǒng)Io叭首,減少了一次cpu的數(shù)據(jù)拷貝焙格,然而上下文切換次數(shù)并沒(méi)有減少夷都,你試想一下囤官,如果應(yīng)用程序與內(nèi)核只做一次交互不就可以減少2次上下文切換党饮,因此刑顺,sendfile()
相對(duì)mmap()+write()
就是做了這一點(diǎn)的結(jié)合性改善。參考下圖(盜圖一張不皆,不留名,嘿嘿):
寫(xiě)在最后
說(shuō)了這么多傳統(tǒng)IO鲫骗、mmap以及sendfile执泰,我們來(lái)做下比對(duì):
傳統(tǒng) IO 執(zhí)行的話(huà)需要 4 次上下文切換(用戶(hù)態(tài) -> 內(nèi)核態(tài) -> 用戶(hù)態(tài) -> 內(nèi)核態(tài) -> 用戶(hù)態(tài))和 4 次拷貝(磁盤(pán)文件 DMA 拷貝到內(nèi)核緩沖區(qū)术吝,內(nèi)核緩沖區(qū) CPU 拷貝到用戶(hù)緩沖區(qū)排苍,用戶(hù)緩沖區(qū) CPU 拷貝到 Socket 緩沖區(qū)淘衙,Socket 緩沖區(qū) DMA 拷貝到協(xié)議引擎)彤守。
mmap 將磁盤(pán)文件映射到內(nèi)存具垫,支持讀和寫(xiě)筝蚕,對(duì)內(nèi)存的操作會(huì)反映在磁盤(pán)文件上饰及,適合小數(shù)據(jù)量讀寫(xiě)康震,需要 4 次上下文切換(用戶(hù)態(tài) -> 內(nèi)核態(tài) -> 用戶(hù)態(tài) -> 內(nèi)核態(tài) -> 用戶(hù)態(tài))和3 次拷貝(磁盤(pán)文件DMA拷貝到內(nèi)核緩沖區(qū),內(nèi)核緩沖區(qū) CPU 拷貝到 Socket 緩沖區(qū)腿短,Socket 緩沖區(qū) DMA 拷貝到協(xié)議引擎)屏箍。
sendfile 是將讀到內(nèi)核空間的數(shù)據(jù)绘梦,轉(zhuǎn)到 socket buffer,進(jìn)行網(wǎng)絡(luò)發(fā)送赴魁,適合大文件傳輸卸奉,只需要 2 次上下文切換(用戶(hù)態(tài) -> 內(nèi)核態(tài) -> 用戶(hù)態(tài))和 2 次拷貝(磁盤(pán)文件 DMA 拷貝到內(nèi)核緩沖區(qū),內(nèi)核緩沖區(qū) DMA 拷貝到協(xié)議引擎)颖御。
此外,零拷貝其實(shí)也沒(méi)有真正意義上的清零潘拱,只是相對(duì)傳統(tǒng)IO進(jìn)行了性能優(yōu)化:
- 1.采用虛擬內(nèi)存地址的方式共享內(nèi)存疹鳄,減少內(nèi)核與用戶(hù)空間數(shù)據(jù)拷貝的次數(shù)。
- 2.拷貝次數(shù)的減少芦岂,間接減少了cpu的參與次數(shù)瘪弓。
- 3.sendfile這種方式減少了上下文切換的次數(shù)。
- 4.同時(shí)禽最,DMA控制也是一種減少cpu參與數(shù)據(jù)拷貝的方式腺怯。
因此,減少數(shù)據(jù)拷貝
川无、CPU參與
呛占、上下文切換
才是零拷貝最具靈魂、最絕的一筆懦趋!
作者介紹
keaizhuzhu栓票,公眾號(hào)面試怪圈
小編,網(wǎng)站面試怪圈
站長(zhǎng)愕够,曾就職于阿里巴巴本地生活走贪,目前就職于京東做后端開(kāi)發(fā)。
編寫(xiě)過(guò)《Java面試怪圈內(nèi)卷手冊(cè)》
面試秘籍惑芭,全網(wǎng)閱讀量過(guò)萬(wàn)次坠狡。
官網(wǎng):http://www.msgqer.com
。旨在分享前端遂跟、后端逃沿、大數(shù)據(jù)、各種中間件技術(shù)的面試資料幻锁,總訪(fǎng)問(wèn)量數(shù)萬(wàn)次凯亮。點(diǎn)擊【閱讀原文】可直達(dá)。
Java后端在線(xiàn)面試題
地址:http://www.msgqer.com/case/fwCase