轉(zhuǎn)自JAVA IO 以及 NIO 理解
一段話總結(jié):
傳統(tǒng)io中從磁盤中中讀文件稼病,并把文件通過網(wǎng)絡(luò)(socket)發(fā)送給Client
需要四次上下文切換(用戶態(tài)與內(nèi)核態(tài)之間的切換) 和 四次拷貝操作才能完成:read()執(zhí)行椭更,數(shù)據(jù)讀到內(nèi)核緩沖區(qū)--》read()返回贱除,數(shù)據(jù)拷貝到用戶緩沖區(qū)--》send()執(zhí)行嘱兼,數(shù)據(jù)拷貝到內(nèi)核緩沖區(qū)--》send()返回成艘,數(shù)據(jù)拷貝到網(wǎng)卡的緩沖區(qū)爪飘。
zerocopy內(nèi)存映射技術(shù),減少上下文切換和拷貝次數(shù)
兩次拷貝 和 兩次上下文切換:執(zhí)行transferTo()--》數(shù)據(jù)拷貝到內(nèi)核緩存--》數(shù)據(jù)拷貝到網(wǎng)卡緩存
由于Netty砌左,了解了一些異步IO的知識(shí)脖咐,JAVA里面NIO就是原來的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ā)起寫操作匾嘱,導(dǎo)致 “syscall write ”系統(tǒng)調(diào)用,將會(huì)把一個(gè) buffer 中的數(shù)據(jù) 搬出去(發(fā)送到網(wǎng)絡(luò)中 or 寫入到磁盤文件)
上面的過程看似簡(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ù)。
先來看一張普通的IO處理的流程圖:
整個(gè)IO過程的流程如下:
1)程序員寫代碼創(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)核給磁盤控制器發(fā)命令說:我要讀磁盤上的某某塊磁盤塊上的數(shù)據(jù)酱床。--kernel issuing a command to the disk controller hardware to fetch the data from disk.
②在DMA的控制下羊赵,把磁盤上的數(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)該就是我們寫的代碼中 new 的 byte[] 數(shù)組昧捷。
從上面的步驟中可以分析出什么?
?對(duì)于操作系統(tǒng)而言罐寨,JVM只是一個(gè)用戶進(jìn)程靡挥,處于用戶態(tài)空間中。而處于用戶態(tài)空間的進(jìn)程是不能直接操作底層的硬件的鸯绿。而IO操作就需要操作底層的硬件跋破,比如磁盤。因此瓶蝴,IO操作必須得借助內(nèi)核的幫助才能完成(中斷毒返,trap),即:會(huì)有用戶態(tài)到內(nèi)核態(tài)的切換舷手。
?我們寫代碼 new byte[] 數(shù)組時(shí)拧簸,一般是都是“隨意” 創(chuàng)建一個(gè)“任意大小”的數(shù)組。比如男窟,new byte[128]盆赤、new byte[1024]贾富、new byte[4096]....
但是,對(duì)于磁盤塊的讀取而言牺六,每次訪問磁盤讀數(shù)據(jù)時(shí)祷安,并不是讀任意大小的數(shù)據(jù)的,而是:每次讀一個(gè)磁盤塊或者若干個(gè)磁盤塊(這是因?yàn)樵L問磁盤操作代價(jià)是很大的兔乞,而且我們也相信局部性原理) 因此汇鞭,就需要有一個(gè)“中間緩沖區(qū)”--即內(nèi)核緩沖區(qū)。先把數(shù)據(jù)從磁盤讀到內(nèi)核緩沖區(qū)中庸追,然后再把數(shù)據(jù)從內(nèi)核緩沖區(qū)搬到用戶緩沖區(qū)霍骄。
這也是為什么我們總感覺到第一次read操作很慢,而后續(xù)的read操作卻很快的原因吧淡溯。因?yàn)槎琳瑢?duì)于后續(xù)的read操作而言,它所需要讀的數(shù)據(jù)很可能已經(jīng)在內(nèi)核緩沖區(qū)了咱娶,此時(shí)只需將內(nèi)核緩沖區(qū)中的數(shù)據(jù)拷貝到用戶緩沖區(qū)即可米间,并未涉及到底層的讀取磁盤操作,當(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)核從磁盤上把數(shù)據(jù)取到內(nèi)核緩沖區(qū)中琼了。
那我們可能會(huì)說:DMA為什么不直接將磁盤上的數(shù)據(jù)讀入到用戶緩沖區(qū)呢逻锐?一方面是 ?中提到的內(nèi)核緩沖區(qū)作為一個(gè)中間緩沖區(qū)。用來“適配”用戶緩沖區(qū)的“任意大小”和每次讀磁盤塊的固定大小雕薪。另一方面則是昧诱,用戶緩沖區(qū)位于用戶態(tài)空間,而DMA讀取數(shù)據(jù)這種操作涉及到底層的硬件所袁,硬件一般是不能直接訪問用戶態(tài)空間的(OS的原因吧)
綜上盏档,由于DMA不能直接訪問用戶空間(用戶緩沖區(qū)),普通IO操作需要將數(shù)據(jù)來回地在 用戶緩沖區(qū) 和 內(nèi)核緩沖區(qū)移動(dòng)燥爷,這在一定程序上影響了IO的速度蜈亩。那有沒有相應(yīng)的解決方案呢?
那就是直接內(nèi)存映射IO局劲,也即JAVA NIO中提到的內(nèi)存映射文件勺拣,或者說 直接內(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)程訪問“內(nèi)存映射文件”地址時(shí),自動(dòng)產(chǎn)生缺頁錯(cuò)誤,然后由底層的OS負(fù)責(zé)將磁盤上的數(shù)據(jù)送到內(nèi)存愤惰。關(guān)于頁式存儲(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可以通過java.nio.channels.FileChannel.java(通道)的 map方法創(chuàng)建宦言。
使用內(nèi)存映射緩沖區(qū)來操作文件扇单,它比普通的IO操作讀文件要快得多。甚至比使用文件通道(FileChannel)操作文件 還要快奠旺。因?yàn)橹├剑褂脙?nèi)存映射緩沖區(qū)操作文件時(shí),沒有顯示的系統(tǒng)調(diào)用(read,write)响疚,而且OS還會(huì)自動(dòng)緩存一些文件頁(memory page)
zerocopy技術(shù)介紹
看完了上面的IO操作的底層實(shí)現(xiàn)過程鄙信,再來了解zerocopy技術(shù)就很easy了。IBM有一篇名為《Efficient data transfer through zero copy》的論文對(duì)zerocopy做了完整的介紹忿晕。感覺非常好装诡,下面就基于這篇文來記錄下自己的一些理解。
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)求--->從本地磁盤讀數(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語言提供了跨平臺(tái)的一致性绷耍,屏蔽了底層操作系統(tǒng)的具體實(shí)現(xiàn)細(xì)節(jié),因此鲜侥,JAVA語言也很難直接使用底層操作系統(tǒng)提供的一些“奇技淫巧”褂始。
而要實(shí)現(xiàn)zerocopy,首先得有操作系統(tǒng)的支持描函。其次崎苗,JDK類庫也要提供相應(yīng)的接口支持。幸運(yùn)的是舀寓,自JDK1.4以來胆数,JDK提供了對(duì)NIO的支持,通過java.nio.channels.FileChannel類的transferTo()方法可以直接將字節(jié)傳送到可寫的通道中(Writable Channel)互墓,并不需要將字節(jié)送入用戶程序空間(用戶緩沖區(qū))
You can use the transferTo()method to transfer bytes directly from the channel on which it is invoked toanother writable byte channel, without requiring data to flow through the application
下面就來詳細(xì)分析一下經(jīng)典的web服務(wù)器(比如文件服務(wù)器)干的活:從磁盤中中讀文件必尼,并把文件通過網(wǎng)絡(luò)(socket)發(fā)送給Client。
File.read(fileDesc, buf, len);
Socket.send(socket, buf, len);
從代碼上看,就是兩步操作判莉。第一步:將文件讀入buf豆挽;第二步:將 buf 中的數(shù)據(jù)通過socket發(fā)送出去。但是券盅,這兩步操作需要四次上下文切換(用戶態(tài)與內(nèi)核態(tài)之間的切換) 和 四次拷貝操作才能完成帮哈。
①第一次上下文切換發(fā)生在 read()方法執(zhí)行,表示服務(wù)器要去磁盤上讀文件了锰镀,這會(huì)導(dǎo)致一個(gè) sys_read()的系統(tǒng)調(diào)用娘侍。此時(shí)由用戶態(tài)切換到內(nèi)核態(tài),完成的動(dòng)作是:DMA把磁盤上的數(shù)據(jù)讀入到內(nèi)核緩沖區(qū)中(這也是第一次拷貝)泳炉。
②第二次上下文切換發(fā)生在read()方法的返回(這也說明read()是一個(gè)阻塞調(diào)用)憾筏,表示數(shù)據(jù)已經(jīng)成功從磁盤上讀到內(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不是太了解轻纪,但是從上面的示例圖來看:它是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正是因?yàn)橐肓藘?nèi)核緩沖區(qū)(中間緩沖區(qū))掂僵,使得數(shù)據(jù)來回地拷貝,降低了效率顷歌。
那先來看看锰蓬,它為什么說內(nèi)核緩沖區(qū)提高了性能。
對(duì)于讀操作而言眯漩,內(nèi)核緩沖區(qū)就相當(dāng)于一個(gè)“readahead cache”芹扭,當(dāng)用戶程序一次只需要讀一小部分?jǐn)?shù)據(jù)時(shí),首先操作系統(tǒng)從磁盤上讀一大塊數(shù)據(jù)到內(nèi)核緩沖區(qū),用戶程序只取走了一小部分(* 我可以只 new 了一個(gè) 128B的byte數(shù)組啊! new byte[128]*)冯勉。當(dāng)用戶程序下一次再讀數(shù)據(jù)澈蚌,就可以直接從內(nèi)核緩沖區(qū)中取了,操作系統(tǒng)就不需要再次訪問磁盤啦灼狰!因?yàn)橛脩粢x的數(shù)據(jù)已經(jīng)在內(nèi)核緩沖區(qū)啦宛瞄!這也是前面提到的:為什么后續(xù)的讀操作(read()方法調(diào)用)要明顯地比第一次快的原因。從這個(gè)角度而言交胚,內(nèi)核緩沖區(qū)確實(shí)提高了讀操作的性能份汗。
再來看寫操作:可以做到 “異步寫”(write asynchronously)。也即:wirte(dest[]) 時(shí)蝴簇,用戶程序告訴操作系統(tǒng)杯活,把dest[]數(shù)組中的內(nèi)容寫到XX文件中去,于是write方法就返回了熬词。操作系統(tǒng)則在后臺(tái)默默地把用戶緩沖區(qū)中的內(nèi)容(dest[])拷貝到內(nèi)核緩沖區(qū)旁钧,再把內(nèi)核緩沖區(qū)中的數(shù)據(jù)寫入磁盤。那么互拾,只要內(nèi)核緩沖區(qū)未滿歪今,用戶的write操作就可以很快地返回。這應(yīng)該就是異步刷盤策略吧颜矿。
(其實(shí)寄猩,到這里。以前一個(gè)糾結(jié)的問題就是同步IO骑疆,異步IO田篇,阻塞IO,非阻塞IO之間的區(qū)別已經(jīng)沒有太大的意義了箍铭。這些概念泊柬,只是針對(duì)的看問題的角度不一樣而已。阻塞诈火、非阻塞是針對(duì)線程自身而言彬呻;同步、異步是針對(duì)線程以及影響它的外部事件而言....)【更加完美柄瑰、精辟的解釋可以參考這個(gè)系列的文章:系統(tǒng)間通信(3)——IO通信模型和JAVA實(shí)踐 上篇】
既然,你把內(nèi)核緩沖區(qū)說得這么強(qiáng)大和完美剪况,那還要 zerocopy干嘛敖陶础?译断?授翻?
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ù)量太大了。
下面來看看zerocopy技術(shù)是如何來處理文件傳輸?shù)摹?/strong>
當(dāng) transferTo()方法 被調(diào)用時(shí)合陵,由用戶態(tài)切換到內(nèi)核態(tài)枢赔。完成的動(dòng)作是:DMA將數(shù)據(jù)從磁盤讀入 Read buffer中(第一次數(shù)據(jù)拷貝)。然后拥知,還是在內(nèi)核空間中踏拜,將數(shù)據(jù)從Read buffer 拷貝到 Socket buffer(第二次數(shù)據(jù)拷貝),最終再將數(shù)據(jù)從 Socket buffer 拷貝到 NIC buffer(第三次數(shù)據(jù)拷貝)低剔。然后速梗,再從內(nèi)核態(tài)返回到用戶態(tài)。
上面整個(gè)過程就只涉及到了:三次數(shù)據(jù)拷貝和二次上下文切換襟齿。感覺也才減少了一次數(shù)據(jù)拷貝嘛姻锁。但這里已經(jīng)不涉及用戶空間的緩沖區(qū)了。
三次數(shù)據(jù)拷貝中蕊唐,也只有一次拷貝需要到CPU的干預(yù)屋摔。(第2次拷貝),而前面的傳統(tǒng)數(shù)據(jù)拷貝需要四次且有三次拷貝需要CPU的干預(yù)替梨。
This is an improvement: we've reduced the number of context switches from four to two and reduced the number of data copiesfrom four to three (only one of which involves the CPU)
如果說zerocopy技術(shù)只能完成到這步钓试,那也就 just so so 了。
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 thatrequire CPU involvement.
也就是說副瀑,如果底層的網(wǎng)絡(luò)硬件以及操作系統(tǒng)支持弓熏,還可以進(jìn)一步減少數(shù)據(jù)拷貝次數(shù) 以及 CPU干預(yù)次數(shù)。
從上圖看出:這里一共只有兩次拷貝 和 兩次上下文切換糠睡。而且這兩次拷貝都是DMA copy挽鞠,并不需要CPU干預(yù)(嚴(yán)謹(jǐn)一點(diǎn)的話就是不完全需要吧.)。
整個(gè)過程如下:
用戶程序執(zhí)行 transferTo()方法狈孔,導(dǎo)致一次系統(tǒng)調(diào)用信认,從用戶態(tài)切換到內(nèi)核態(tài)。完成的動(dòng)作是:DMA將數(shù)據(jù)從磁盤中拷貝到Read buffer
用一個(gè)描述符標(biāo)記此次待傳輸數(shù)據(jù)的地址以及長(zhǎng)度均抽,DMA直接把數(shù)據(jù)從Read buffer 傳輸?shù)?NIC buffer嫁赏。數(shù)據(jù)拷貝過程都不用CPU干預(yù)了。
總結(jié):
這篇文章從IO底層實(shí)現(xiàn)原理開始講解油挥,分析了IO底層實(shí)現(xiàn)細(xì)節(jié)的一些優(yōu)缺點(diǎn)潦蝇,以及為什么引入zerocopy技術(shù)和zerocopy技術(shù)的實(shí)現(xiàn)原理款熬。個(gè)人的學(xué)習(xí)記錄,轉(zhuǎn)載請(qǐng)注明出處攘乒。
參考文獻(xiàn):
1)《JAVA NIO》O'Reilly出版社
2)《Efficient data transfer through zero copy》IBM出版
3)Zero Copy I: User-Mode Perspective