由于Netty湃密,了解了一些異步IO的知識(shí)巫员,JAVA里面NIO就是原來(lái)的IO的一個(gè)補(bǔ)充庶香,本文主要記錄下在JAVA中IO的底層實(shí)現(xiàn)原理,以及對(duì)Zerocopy技術(shù)介紹简识。
IO赶掖,其實(shí)意味著:數(shù)據(jù)不停地搬入搬出緩沖區(qū)而已(使用了緩沖區(qū))救军。比如,用戶程序發(fā)起讀操作倘零,導(dǎo)致“ syscall read ”系統(tǒng)調(diào)用,就會(huì)把數(shù)據(jù)搬入到 一個(gè)buffer中戳寸;用戶發(fā)起寫(xiě)操作呈驶,導(dǎo)致 “syscall write ”系統(tǒng)調(diào)用,將會(huì)把一個(gè) buffer 中的數(shù)據(jù) 搬出去(發(fā)送到網(wǎng)絡(luò)中 or 寫(xiě)入到磁盤(pán)文件)
上面的過(guò)程看似簡(jiǎn)單疫鹊,但是底層操作系統(tǒng)具體如何實(shí)現(xiàn)以及實(shí)現(xiàn)的細(xì)節(jié)就非常復(fù)雜了袖瞻。正是因?yàn)閷?shí)現(xiàn)方式不同,有針對(duì)普通情況下的文件傳輸(暫且稱普通IO吧)拆吆,也有針對(duì)大文件傳輸或者批量大數(shù)據(jù)傳輸?shù)膶?shí)現(xiàn)方式聋迎,比如zerocopy技術(shù)。
先來(lái)看一張普通的IO處理的流程圖:
整個(gè)IO過(guò)程的流程如下:
1)程序員寫(xiě)代碼創(chuàng)建一個(gè)緩沖區(qū)(這個(gè)緩沖區(qū)是用戶緩沖區(qū)):哈哈枣耀。然后在一個(gè)while循環(huán)里面調(diào)用read()方法讀數(shù)據(jù)(觸發(fā)"syscall read"系統(tǒng)調(diào)用)
byte[] b = new byte[4096];
while((read = inputStream.read(b))>=0) {
total = total + read;
// other code....
}
2)當(dāng)執(zhí)行到read()方法時(shí)霉晕,其實(shí)底層是發(fā)生了很多操作的:
①內(nèi)核給磁盤(pán)控制器發(fā)命令說(shuō):我要從讀磁盤(pán)上的某某塊磁盤(pán)塊上的數(shù)據(jù)。--kernel issuing a command to the disk controller hardware to fetch the data from disk.
②在DMA的控制下捞奕,把磁盤(pán)上的數(shù)據(jù)讀入到內(nèi)核緩沖區(qū)牺堰。--The disk controller writes the data directly into a kernel memory buffer by DMA
③內(nèi)核把數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到用戶緩沖區(qū)。--kernel copies the data from the temporary buffer in kernel space
這里的用戶緩沖區(qū)應(yīng)該就是我們寫(xiě)的代碼中 new 的 byte[] 數(shù)組颅围。
從上面的步驟中可以分析出什么伟葫?
?對(duì)于操作系統(tǒng)而言,JVM只是一個(gè)用戶進(jìn)程院促,處于用戶態(tài)空間中筏养。而處于用戶態(tài)空間的進(jìn)程是不能直接操作底層的硬件的。而IO操作就需要操作底層的硬件常拓,比如磁盤(pán)渐溶。因此,IO操作必須得借助內(nèi)核的幫助才能完成(中斷墩邀,trap)掌猛,即:會(huì)有用戶態(tài)到內(nèi)核態(tài)的切換。
?我們寫(xiě)代碼 new byte[] 數(shù)組時(shí)眉睹,一般是都是“隨意” 創(chuàng)建一個(gè)“任意大小”的數(shù)組荔茬。比如,new byte[128]竹海、new byte[1024]慕蔚、new byte[4096]....
但是,對(duì)于磁盤(pán)塊的讀取而言斋配,每次訪問(wèn)磁盤(pán)讀數(shù)據(jù)時(shí)孔飒,并不是讀任意大小的數(shù)據(jù)的灌闺,而是:每次讀一個(gè)磁盤(pán)塊或者若干個(gè)磁盤(pán)塊(這是因?yàn)樵L問(wèn)磁盤(pán)操作代價(jià)是很大的,而且我們也相信局部性原理) 因此坏瞄,就需要有一個(gè)“中間緩沖區(qū)”--即內(nèi)核緩沖區(qū)桂对。先把數(shù)據(jù)從磁盤(pán)讀到內(nèi)核緩沖區(qū)中,然后再把數(shù)據(jù)從內(nèi)核緩沖區(qū)搬到用戶緩沖區(qū)鸠匀。
這也是為什么我們總感覺(jué)到第一次read操作很慢蕉斜,而后續(xù)的read操作卻很快的原因吧。因?yàn)樽汗鳎瑢?duì)于后續(xù)的read操作而言宅此,它所需要讀的數(shù)據(jù)很可能已經(jīng)在內(nèi)核緩沖區(qū)了,此時(shí)只需將內(nèi)核緩沖區(qū)中的數(shù)據(jù)拷貝到用戶緩沖區(qū)即可爬范,并未涉及到底層的讀取磁盤(pán)操作父腕,當(dāng)然就快了。
The kernel tries to cache and/or prefetch data, so the data being requested by the process may already be available in kernel space.
If so, the data requested by the process is copied out.
If the data isn't available, the process is suspended while the kernel goes about bringing the data into memory.
如果數(shù)據(jù)不可用青瀑,process將會(huì)被掛起璧亮,并需要等待內(nèi)核從磁盤(pán)上把數(shù)據(jù)取到內(nèi)核緩沖區(qū)中。
那我們可能會(huì)說(shuō):DMA為什么不直接將磁盤(pán)上的數(shù)據(jù)讀入到用戶緩沖區(qū)呢斥难?一方面是 ?中提到的內(nèi)核緩沖區(qū)作為一個(gè)中間緩沖區(qū)杜顺。用來(lái)“適配”用戶緩沖區(qū)的“任意大小”和每次讀磁盤(pán)塊的固定大小。另一方面則是蘸炸,用戶緩沖區(qū)位于用戶態(tài)空間躬络,而DMA讀取數(shù)據(jù)這種操作涉及到底層的硬件,硬件一般是不能直接訪問(wèn)用戶態(tài)空間的(OS的原因吧)
綜上搭儒,由于DMA不能直接訪問(wèn)用戶空間(用戶緩沖區(qū))穷当,普通IO操作需要將數(shù)據(jù)來(lái)回地在 用戶緩沖區(qū) 和 內(nèi)核緩沖區(qū)移動(dòng),這在一定程序上影響了IO的速度淹禾。那有沒(méi)有相應(yīng)的解決方案呢馁菜?
那就是直接內(nèi)存映射IO,也即JAVA IO中提到的內(nèi)存映射文件铃岔,或者說(shuō) 直接內(nèi)存....總之汪疮,它們表達(dá)的意思都差不多。示例圖如下:
從上圖可以看出:內(nèi)核空間的 buffer 與 用戶空間的 buffer 都映射到同一塊 物理內(nèi)存區(qū)域毁习。
它的主要特點(diǎn)如下:
①對(duì)文件的操作不需要再發(fā)read 或者 write 系統(tǒng)調(diào)用了---The user process sees the file data asmemory, so there is no need to issue read() or write() system calls.
②當(dāng)用戶進(jìn)程訪問(wèn)“內(nèi)存映射文件”地址時(shí)智嚷,自動(dòng)產(chǎn)生缺頁(yè)錯(cuò)誤,然后由底層的OS負(fù)責(zé)將磁盤(pán)上的數(shù)據(jù)送到內(nèi)存纺且。關(guān)于頁(yè)式存儲(chǔ)管理盏道,可參考:內(nèi)存分配與內(nèi)存管理的一些理解
As the user process touches the mapped memory space, page faults will be generated automatically to bring in the file data from disk.
If the user modifies the mapped memory space, the affected page is automatically marked as dirty and will be subsequently
flushed to disk to update the file.
這就是是JAVA NIO中提到的內(nèi)存映射緩沖區(qū)(Memory-Mapped-Buffer)它類似于JAVA NIO中的直接緩沖區(qū)(Directed Buffer)。MemoryMappedBuffer可以通過(guò)java.nio.channels.FileChannel.java(通道)的 map方法創(chuàng)建载碌。
使用內(nèi)存映射緩沖區(qū)來(lái)操作文件猜嘱,它比普通的IO操作讀文件要快得多衅枫。甚至比使用文件通道(FileChannel)操作文件 還要快。因?yàn)槔柿妫褂脙?nèi)存映射緩沖區(qū)操作文件時(shí)弦撩,沒(méi)有顯示的系統(tǒng)調(diào)用(read,write),而且OS還會(huì)自動(dòng)緩存一些文件頁(yè)(memory page)
zerocopy技術(shù)介紹
看完了上面的IO操作的底層實(shí)現(xiàn)過(guò)程论皆,再來(lái)了解zerocopy技術(shù)就很easy了孤钦。IBM有一篇名為《Efficient data transfer through zero copy》的論文對(duì)zerocopy做了完整的介紹。感覺(jué)非常好纯丸,下面就基于這篇文來(lái)記錄下自己的一些理解。
zerocopy技術(shù)的目標(biāo)就是提高IO密集弄JAVA應(yīng)用程序的性能静袖。在本文的前面部分介紹了:IO操作需要數(shù)據(jù)頻繁地在內(nèi)核緩沖區(qū)和用戶緩沖區(qū)之間拷貝觉鼻,而zerocopy技術(shù)可以減少這種拷貝次數(shù),同時(shí)也降低了上下文切換(用戶態(tài)與內(nèi)核態(tài)之間的切換)的次數(shù)队橙。
比如坠陈,大多數(shù)WEB應(yīng)用程序執(zhí)行的一項(xiàng)操作就是:接受用戶請(qǐng)求--->從本地磁盤(pán)讀數(shù)據(jù)--->數(shù)據(jù)進(jìn)入內(nèi)核緩沖區(qū)--->用戶緩沖區(qū)--->內(nèi)核緩沖區(qū)--->用戶緩沖區(qū)--->socket發(fā)送
數(shù)據(jù)每次在內(nèi)核緩沖區(qū)與用戶緩沖區(qū)之間的拷貝會(huì)消耗CPU以及內(nèi)存的帶寬。而zerocopy有效減少了這種拷貝次數(shù)捐康。
Each time data traverses the user-kernel boundary, it must be copied, which consumes CPU cycles and memory bandwidth.
Fortunately, you can eliminate these copies through a technique called—appropriately enough —zero copy
那它是怎么做到的呢仇矾?
我們知道,JVM(JAVA虛擬機(jī))為JAVA語(yǔ)言提供了跨平臺(tái)的一致性解总,屏蔽了底層操作系統(tǒng)的具體實(shí)現(xiàn)細(xì)節(jié)贮匕,因此,JAVA語(yǔ)言也很難直接使用底層操作系統(tǒng)提供的一些“奇技淫巧”花枫。
而要實(shí)現(xiàn)zerocopy刻盐,首先得有操作系統(tǒng)的支持。其次劳翰,JDK類庫(kù)也要提供相應(yīng)的接口支持敦锌。幸運(yùn)的是,自JDK1.4以來(lái)佳簸,JDK提供了對(duì)NIO的支持乙墙,通過(guò)java.nio.channels.FileChannel類的transferTo()方法可以直接將字節(jié)傳送到可寫(xiě)的通道中(Writable Channel),并不需要將字節(jié)送入用戶程序空間(用戶緩沖區(qū))
You can use the transferTo()method to transfer bytes directly from the channel on which it is invoked to
another writable byte channel, without requiring data to flow through the application
下面就來(lái)詳細(xì)分析一下經(jīng)典的web服務(wù)器(比如文件服務(wù)器)干的活:從磁盤(pán)中中讀文件生均,并把文件通過(guò)網(wǎng)絡(luò)(socket)發(fā)送給Client听想。
File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);
從代碼上看,就是兩步操作马胧。第一步:將文件讀入buf哗魂;第二步:將 buf 中的數(shù)據(jù)通過(guò)socket發(fā)送出去。但是漓雅,這兩步操作需要四次上下文切換(用戶態(tài)與內(nèi)核態(tài)之間的切換) 和 四次拷貝操作才能完成录别。
①第一次上下文切換發(fā)生在 read()方法執(zhí)行朽色,表示服務(wù)器要去磁盤(pán)上讀文件了,這會(huì)導(dǎo)致一個(gè) sys_read()的系統(tǒng)調(diào)用组题。此時(shí)由用戶態(tài)切換到內(nèi)核態(tài)葫男,完成的動(dòng)作是:DMA把磁盤(pán)上的數(shù)據(jù)讀入到內(nèi)核緩沖區(qū)中(這也是第一次拷貝)。
②第二次上下文切換發(fā)生在read()方法的返回(這也說(shuō)明read()是一個(gè)阻塞調(diào)用)崔列,表示數(shù)據(jù)已經(jīng)成功從磁盤(pán)上讀到內(nèi)核緩沖區(qū)了梢褐。此時(shí),由內(nèi)核態(tài)返回到用戶態(tài)赵讯,完成的動(dòng)作是:將內(nèi)核緩沖區(qū)中的數(shù)據(jù)拷貝到用戶緩沖區(qū)(這是第二次拷貝)盈咳。
③第三次上下文切換發(fā)生在 send()方法執(zhí)行,表示服務(wù)器準(zhǔn)備把數(shù)據(jù)發(fā)送出去了边翼。此時(shí)鱼响,由用戶態(tài)切換到內(nèi)核態(tài),完成的動(dòng)作是:將用戶緩沖區(qū)中的數(shù)據(jù)拷貝到內(nèi)核緩沖區(qū)(這是第三次拷貝)
④第四次上下文切換發(fā)生在 send()方法的返回【這里的send()方法可以異步返回组底,所謂異步返回就是:線程執(zhí)行了send()之后立即從send()返回丈积,剩下的數(shù)據(jù)拷貝及發(fā)送就交給底層操作系統(tǒng)實(shí)現(xiàn)了】。此時(shí)债鸡,由內(nèi)核態(tài)返回到用戶態(tài)江滨,完成的動(dòng)作是:將內(nèi)核緩沖區(qū)中的數(shù)據(jù)送到 protocol engine.(這是第四次拷貝)
這里對(duì) protocol engine不是太了解,但是從上面的示例圖來(lái)看:它是NIC(NetWork Interface Card) buffer厌均。網(wǎng)卡的buffer???
下面這段話唬滑,非常值得一讀:這里再一次提到了為什么需要內(nèi)核緩沖區(qū)。
Use of the intermediate kernel buffer (rather than a direct transfer of the data
into the user buffer)might seem inefficient. But intermediate kernel buffers were
introduced into the process to improve performance. Using the intermediate
buffer on the read side allows the kernel buffer to act as a "readahead cache"
when the application hasn't asked for as much data as the kernel buffer holds.
This significantly improves performance when the requested data amount is less
than the kernel buffer size. The intermediate buffer on the write side allows the write to complete asynchronously.
一個(gè)核心觀點(diǎn)就是:內(nèi)核緩沖區(qū)提高了性能棺弊。咦间雀?是不是很奇怪?因?yàn)榍懊嬉恢闭f(shuō)正是因?yàn)橐肓藘?nèi)核緩沖區(qū)(中間緩沖區(qū))镊屎,使得數(shù)據(jù)來(lái)回地拷貝惹挟,降低了效率。
那先來(lái)看看缝驳,它為什么說(shuō)內(nèi)核緩沖區(qū)提高了性能连锯。
對(duì)于讀操作而言,內(nèi)核緩沖區(qū)就相當(dāng)于一個(gè)“readahead cache”用狱,當(dāng)用戶程序一次只需要讀一小部分?jǐn)?shù)據(jù)時(shí)运怖,首先操作系統(tǒng)從磁盤(pán)上讀一大塊數(shù)據(jù)到內(nèi)核緩沖區(qū),用戶程序只取走了一小部分( 我可以只 new 了一個(gè) 128B的byte數(shù)組啊! new byte[128])夏伊。當(dāng)用戶程序下一次再讀數(shù)據(jù)摇展,就可以直接從內(nèi)核緩沖區(qū)中取了,操作系統(tǒng)就不需要再次訪問(wèn)磁盤(pán)啦溺忧!因?yàn)橛脩粢x的數(shù)據(jù)已經(jīng)在內(nèi)核緩沖區(qū)啦咏连!這也是前面提到的:為什么后續(xù)的讀操作(read()方法調(diào)用)要明顯地比第一次快的原因盯孙。從這個(gè)角度而言,內(nèi)核緩沖區(qū)確實(shí)提高了讀操作的性能祟滴。
再來(lái)看寫(xiě)操作:可以做到 “異步寫(xiě)”(write asynchronously)振惰。也即:wirte(dest[]) 時(shí),用戶程序告訴操作系統(tǒng)垄懂,把dest[]數(shù)組中的內(nèi)容寫(xiě)到XX文件中去骑晶,于是write方法就返回了。操作系統(tǒng)則在后臺(tái)默默地把用戶緩沖區(qū)中的內(nèi)容(dest[])拷貝到內(nèi)核緩沖區(qū)草慧,再把內(nèi)核緩沖區(qū)中的數(shù)據(jù)寫(xiě)入磁盤(pán)桶蛔。那么,只要內(nèi)核緩沖區(qū)未滿漫谷,用戶的write操作就可以很快地返回仔雷。這應(yīng)該就是異步刷盤(pán)策略吧。
(其實(shí)抖剿,到這里。以前一個(gè)糾結(jié)的問(wèn)題就是同步IO识窿,異步IO斩郎,阻塞IO,非阻塞IO之間的區(qū)別已經(jīng)沒(méi)有太大的意義了喻频。這些概念缩宜,只是針對(duì)的看問(wèn)題的角度不一樣而已。阻塞甥温、非阻塞是針對(duì)線程自身而言锻煌;同步、異步是針對(duì)線程以及影響它的外部事件而言....)
Unfortunately, this approach itself can become a performance bottleneck if the size of the data requested
is considerably larger than the kernel buffer size. The data gets copied multiple times among the disk, kernel buffer,
and user buffer before it is finally delivered to the application.
Zero copy improves performance by eliminating these redundant data copies.
終于輪到zerocopy粉墨登場(chǎng)了姻蚓。當(dāng)需要傳輸?shù)臄?shù)據(jù)遠(yuǎn)遠(yuǎn)大于內(nèi)核緩沖區(qū)的大小時(shí)宋梧,內(nèi)核緩沖區(qū)就會(huì)成為瓶頸。這也是為什么zerocopy技術(shù)合適大文件傳輸?shù)脑蛘病?nèi)核緩沖區(qū)為啥成為了瓶頸捂龄?---我想,很大的一個(gè)原因是它已經(jīng)起不到“緩沖”的功能了加叁,畢竟傳輸?shù)臄?shù)據(jù)量太大了倦沧。
下面來(lái)看看zerocopy技術(shù)是如何來(lái)處理文件傳輸?shù)摹?/strong>
當(dāng) transferTo()方法 被調(diào)用時(shí),由用戶態(tài)切換到內(nèi)核態(tài)它匕。完成的動(dòng)作是:DMA將數(shù)據(jù)從磁盤(pán)讀入 Read buffer中(第一次數(shù)據(jù)拷貝)展融。然后,還是在內(nèi)核空間中豫柬,將數(shù)據(jù)從Read buffer 拷貝到 Socket buffer(第二次數(shù)據(jù)拷貝)告希,最終再將數(shù)據(jù)從 Socket buffer 拷貝到 NIC buffer(第三次數(shù)據(jù)拷貝)扑浸。然后,再?gòu)膬?nèi)核態(tài)返回到用戶態(tài)暂雹。
上面整個(gè)過(guò)程就只涉及到了:三次數(shù)據(jù)拷貝和二次上下文切換首装。感覺(jué)也才減少了一次數(shù)據(jù)拷貝嘛。但這里已經(jīng)不涉及用戶空間的緩沖區(qū)了杭跪。
如果說(shuō)zerocopy技術(shù)只能完成到這步仙逻,那也就 just so so 了。
This is an improvement: we've reduced the number of context switches from four to two and reduced the number of data copies
from four to three (only one of which involves the CPU)
三次數(shù)據(jù)拷貝中涧尿,也只有一次拷貝需要到CPU的干預(yù)系奉。(第2次拷貝),而前面的傳統(tǒng)數(shù)據(jù)拷貝需要四次且有三次拷貝需要CPU的干預(yù)姑廉。
We can further reduce the data duplication done by the kernel if the underlying network interface card supports
gather operations. In Linux kernels 2.4 and later, the socket buffer descriptor was modified to accommodate this requirement.
This approach not only reduces multiple context switches but also eliminates the duplicated data copies that
require CPU involvement.
也就是說(shuō)缺亮,如果底層的網(wǎng)絡(luò)硬件以及操作系統(tǒng)支持,還可以進(jìn)一步減少數(shù)據(jù)拷貝次數(shù) 以及 CPU干預(yù)次數(shù)桥言。
從上圖看出:這里一共只有兩次拷貝 和 兩次上下文切換萌踱。而且這兩次拷貝都是DMA copy,并不需要CPU干預(yù)(嚴(yán)謹(jǐn)一點(diǎn)的話就是不完全需要吧.)号阿。
整個(gè)過(guò)程如下:
用戶程序執(zhí)行 transferTo()方法并鸵,導(dǎo)致一次系統(tǒng)調(diào)用,從用戶態(tài)切換到內(nèi)核態(tài)扔涧。完成的動(dòng)作是:DMA將數(shù)據(jù)從磁盤(pán)中拷貝到Read buffer
用一個(gè)描述符標(biāo)記此次待傳輸數(shù)據(jù)的地址以及長(zhǎng)度园担,DMA直接把數(shù)據(jù)從Read buffer 傳輸?shù)?NIC buffer。數(shù)據(jù)拷貝過(guò)程都不用CPU干預(yù)了枯夜。
總結(jié):
這篇文章從IO底層實(shí)現(xiàn)原理開(kāi)始講解弯汰,分析了IO底層實(shí)現(xiàn)細(xì)節(jié)的一些優(yōu)缺點(diǎn),以及為什么引入zerocopy技術(shù)和zerocopy技術(shù)的實(shí)現(xiàn)原理湖雹。個(gè)人的學(xué)習(xí)記錄咏闪,轉(zhuǎn)載請(qǐng)注明出處。