術(shù)語概念描述:
IO有內(nèi)存IO、網(wǎng)絡(luò)IO和磁盤IO三種,通常我們說的IO指的是后兩者区拳。
阻塞和非阻塞,是函數(shù)/方法的實(shí)現(xiàn)方式意乓,即在數(shù)據(jù)就緒之前是立刻返回還是等待樱调。
以文件IO為例,一個(gè)IO讀過程是文件數(shù)據(jù)從磁盤→內(nèi)核緩沖區(qū)→用戶內(nèi)存的過程。同步與異步的區(qū)別主要在于數(shù)據(jù)從內(nèi)核緩沖區(qū)→用戶內(nèi)存這個(gè)過程需不需要用戶進(jìn)程等待。有個(gè)數(shù)據(jù)拷貝的過程笆凌,是拷貝完再通知還是在內(nèi)核緩沖區(qū)就通知圣猎。(網(wǎng)絡(luò)IO把磁盤換做網(wǎng)卡即可)
Linux IO模型
????????同步阻塞
????????同步非阻塞
????????IO復(fù)用
????????信號(hào)驅(qū)動(dòng)
????????異步非阻塞
同步阻塞
????????去餐館吃飯,點(diǎn)一個(gè)自己最愛吃的蓋澆飯乞而,然后在原地等著一直到蓋澆飯做好送悔,自己端到餐桌就餐。這就是典型的同步阻塞爪模。當(dāng)廚師給你做飯的時(shí)候放祟,你需要一直在那里等著。
????????網(wǎng)絡(luò)編程中呻右,讀取客戶端的數(shù)據(jù)需要調(diào)用recvfrom跪妥。在默認(rèn)情況下,這個(gè)調(diào)用會(huì)一直阻塞直到數(shù)據(jù)接收完畢声滥,就是一個(gè)同步阻塞的IO方式眉撵。這也是最簡(jiǎn)單的IO模型,在通常fd(文件描述句柄)較少落塑、就緒很快的情況下使用是沒有問題的纽疟。
同步非阻塞
????????你每次點(diǎn)完飯就在那里等著,突然有一天你發(fā)現(xiàn)自己真傻憾赁。于是污朽,你點(diǎn)完之后,就回桌子那里坐著龙考,然后估計(jì)差不多了蟆肆,就問老板飯好了沒,如果好了就去端晦款,沒好的話就等一會(huì)再去問炎功,依次循環(huán)直到飯做好。這就是同步非阻塞缓溅。
????????這種方式在編程中對(duì)socket設(shè)置O_NONBLOCK即可蛇损。但此方式僅僅針對(duì)網(wǎng)絡(luò)IO有效,對(duì)磁盤IO并沒有作用坛怪。因?yàn)楸镜匚募蘒O就沒有被認(rèn)為是阻塞淤齐,我們所說的網(wǎng)絡(luò)IO的阻塞是因?yàn)榫W(wǎng)路IO有無限阻塞的可能,而本地文件除非是被鎖住袜匿,否則是不可能無限阻塞的更啄,因此只有鎖這種情況下,O_NONBLOCK才會(huì)有作用沉帮。而且锈死,磁盤IO時(shí)要么數(shù)據(jù)在內(nèi)核緩沖區(qū)中直接可以返回,要么需要調(diào)用物理設(shè)備去讀取穆壕,這時(shí)候進(jìn)程的其他工作都需要等待待牵。因此,后續(xù)的IO復(fù)用和信號(hào)驅(qū)動(dòng)IO對(duì)文件IO也是沒有意義的喇勋。
IO復(fù)用
????????你點(diǎn)一份飯然后循環(huán)的去問好沒好顯然有點(diǎn)得不償失缨该,還不如就等在那里直到準(zhǔn)備好,但是當(dāng)你點(diǎn)了好幾樣飯菜的時(shí)候川背,你每次都去問一下所有飯菜的狀態(tài)(未做好/已做好)肯定比你每次阻塞在那里等著好多了贰拿。當(dāng)然,你問的時(shí)候是需要阻塞的熄云,一直到有準(zhǔn)備好的飯菜或者你等的不耐煩(超時(shí))膨更。這就引出了IO復(fù)用,也叫多路IO就緒通知缴允。這是一種進(jìn)程預(yù)先告知內(nèi)核的能力荚守,讓內(nèi)核發(fā)現(xiàn)進(jìn)程指定的一個(gè)或多個(gè)IO條件就緒了奔誓,就通知進(jìn)程晋涣。使得一個(gè)進(jìn)程能在一連串的事件上等待绪抛。
????????IO復(fù)用的實(shí)現(xiàn)方式目前主要有select柔滔、poll和epoll贫母。select和poll的原理基本相同:注冊(cè)待偵聽的fd(這里的fd創(chuàng)建時(shí)最好使用非阻塞)熬苍,每次調(diào)用都去檢查這些fd的狀態(tài)换淆,當(dāng)有一個(gè)或者多個(gè)fd就緒的時(shí)候返回心褐,返回結(jié)果中包括已就緒和未就緒的fd摄职。
????????相比select誊役,poll解決了單個(gè)進(jìn)程能夠打開的文件描述符數(shù)量有限制這個(gè)問題:select受限于FD_SIZE的限制,如果修改則需要修改這個(gè)宏重新編譯內(nèi)核谷市;而poll通過一個(gè)pollfd數(shù)組向內(nèi)核傳遞需要關(guān)注的事件势木,避開了文件描述符數(shù)量限制。此外歌懒,select和poll共同具有的一個(gè)很大的缺點(diǎn)就是包含大量fd的數(shù)組被整體復(fù)制于用戶態(tài)和內(nèi)核態(tài)地址空間之間啦桌,開銷會(huì)隨著fd數(shù)量增多而線性增大。
????????select和poll就類似于上面說的就餐方式及皂。但當(dāng)你每次都去詢問時(shí)甫男,老板會(huì)把所有你點(diǎn)的飯菜都輪詢一遍再告訴你情況,當(dāng)大量飯菜很長(zhǎng)時(shí)間都不能準(zhǔn)備好的情況下是很低效的验烧。于是板驳,老板有些不耐煩了,就讓廚師每做好一個(gè)菜就記下來他碍拆。這樣每次你再去問的時(shí)候若治,他會(huì)直接把已經(jīng)準(zhǔn)備好的菜告訴你慨蓝,你再去端。這就是事件驅(qū)動(dòng)IO就緒通知的方式epoll端幼。
????????epoll的出現(xiàn)礼烈,解決了select、poll的缺點(diǎn):基于事件驅(qū)動(dòng)的方式婆跑,避免了每次都要把所有fd都掃描一遍此熬。epoll_wait只返回就緒的fd。epoll使用nmap內(nèi)存映射技術(shù)避免了內(nèi)存復(fù)制的開銷滑进。epoll的fd數(shù)量上限是操作系統(tǒng)的最大文件句柄數(shù)目,這個(gè)數(shù)目一般和內(nèi)存有關(guān)犀忱,通常遠(yuǎn)大于1024。
總結(jié):
????????select:支持注冊(cè) FD_SETSIZE(1024)?個(gè) socket扶关。
????????poll:poll?作為 select?的替代者阴汇,最大的區(qū)別就是,poll?不再限制 socket?數(shù)量节槐。
????????epoll:epoll?能直接返回具體的準(zhǔn)備好的通道鲫寄,時(shí)間復(fù)雜度 O(1)。
????????ps:select 和poll 都有一個(gè)共同的問題疯淫,那就是它們都只會(huì)返回所有通道(channel)地来,但是不會(huì)告訴你具體是哪幾個(gè)通道已經(jīng)就緒。一旦知道有通道準(zhǔn)備好以后熙掺,需要進(jìn)行一次掃描未斑,通道少的時(shí)候還行,一旦通道的數(shù)量是幾十萬個(gè)以上的時(shí)候币绩,掃描一次的時(shí)間復(fù)雜度O(n)蜡秽。后來才催生了epoll實(shí)現(xiàn)。
????????此外缆镣,對(duì)于IO復(fù)用還有一個(gè)水平觸發(fā)和邊緣觸發(fā)的概念:
????????水平觸發(fā)(可以返回多次):當(dāng)就緒的fd未被用戶進(jìn)程處理后芽突,下一次查詢依舊會(huì)返回,這是select和poll的觸發(fā)方式董瞻。
????????邊緣觸發(fā)(只返回一次):無論就緒的fd是否被處理寞蚌,下一次不再返回。理論上性能更高钠糊,但是實(shí)現(xiàn)相當(dāng)復(fù)雜挟秤,并且任何意外的丟失事件都會(huì)造成請(qǐng)求處理錯(cuò)誤。epoll默認(rèn)使用水平觸發(fā)抄伍,通過相應(yīng)選項(xiàng)可以使用邊緣觸發(fā)艘刚。
信號(hào)驅(qū)動(dòng)
????????上文的就餐方式還是需要你每次都去問一下飯菜狀況。于是截珍,你再次不耐煩了攀甚,就跟老板說箩朴,哪個(gè)飯菜好了就通知我一聲吧。然后就自己坐在桌子那里干自己的事情秋度。更甚者炸庞,你可以把手機(jī)號(hào)留給老板,自己出門静陈,等飯菜好了直接發(fā)條短信給你燕雁。這就類似信號(hào)驅(qū)動(dòng)的IO模型诞丽。
????????流程如下:
????????開啟套接字信號(hào)驅(qū)動(dòng)IO功能鲸拥;
????????系統(tǒng)調(diào)用sigaction執(zhí)行信號(hào)處理函數(shù)(非阻塞,立刻返回)僧免;
????????數(shù)據(jù)就緒(在內(nèi)核緩沖區(qū))刑赶,生成sigio信號(hào),通過信號(hào)回調(diào)通知應(yīng)用來讀取數(shù)據(jù)懂衩。
異步非阻塞
????????之前的就餐方式撞叨,到最后總是需要你自己去把飯菜端到餐桌。這下你也不耐煩了浊洞,于是就告訴老板牵敷,能不能飯好了直接端到你的面前或者送到你的家里(數(shù)據(jù)在用戶內(nèi)存就緒)。這就是異步非阻塞IO了法希。
????????對(duì)比信號(hào)驅(qū)動(dòng)IO枷餐,異步IO的主要區(qū)別在于:信號(hào)驅(qū)動(dòng)由內(nèi)核告訴我們何時(shí)可以開始一個(gè)IO操作(數(shù)據(jù)在內(nèi)核緩沖區(qū)中),而異步IO則由內(nèi)核通知IO操作何時(shí)已經(jīng)完成(數(shù)據(jù)已經(jīng)在用戶空間中)苫亦。異步IO又叫做事件驅(qū)動(dòng)IO毛肋,在Unix中,POSIX1003.1標(biāo)準(zhǔn)為異步方式訪問文件定義了一套庫函數(shù)屋剑,定義了AIO的一系列接口润匙。使用aio_read或者aio_write發(fā)起異步IO操作。使用aio_error檢查正在運(yùn)行的IO操作的狀態(tài)唉匾。
網(wǎng)絡(luò)編程模型
????????Java的I/O發(fā)展簡(jiǎn)史:
????????從JDK1.0到JDK1.3孕讳,Java的I/O類庫都非常原始,很多UNIX網(wǎng)絡(luò)編程中的概念或者接口在I/O類庫中都沒有體現(xiàn)巍膘,例如Pipe卫病、Channel、Buffer和Selector等典徘。2002年發(fā)布JDK1.4時(shí)蟀苛,NIO以JSR-51的身份正式隨JDK發(fā)布。它新增了個(gè)java.nio包逮诲,提供了很多進(jìn)行異步I/O開發(fā)的API和類庫帜平,主要的類和接口如下:
????????進(jìn)行異步I/O操作的緩沖區(qū)ByteBuffer等幽告;
????????進(jìn)行異步I/O操作的管道Pipe;
????????進(jìn)行各種I/O操作(異步或者同步)的Channel裆甩,包括ServerSocketChannel和SocketChannel冗锁;
????????多種字符集的編碼能力和解碼能力;
????????實(shí)現(xiàn)非阻塞I/O操作的多路復(fù)用器selector嗤栓;
????????基于流行的Perl實(shí)現(xiàn)的正則表達(dá)式類庫冻河;
????????文件通道FileChannel。
????????新的NIO類庫的提供茉帅,極大地促進(jìn)了基于Java的異步非阻塞編程的發(fā)展和應(yīng)用叨叙,但是,它依然有不完善的地方堪澎,特別是對(duì)文件系統(tǒng)的處理能力仍顯不足擂错,主要問題如下:
????????沒有統(tǒng)一的文件屬性(例如讀寫權(quán)限);
????????API能力比較弱樱蛤,例如目錄的級(jí)聯(lián)創(chuàng)建和遞歸遍歷钮呀,往往需要自己實(shí)現(xiàn);
????????底層存儲(chǔ)系統(tǒng)的一些高級(jí)API無法使用昨凡;
????????所有的文件操作都是同步阻塞調(diào)用爽醋,不支持異步文件讀寫操作。
????????2011年7月28日便脊,JDK1.7正式發(fā)布蚂四。它的一個(gè)比較大的亮點(diǎn)就是將原來的NIO類庫進(jìn)行了升級(jí),被稱為NIO2.0就轧。NIO2.0由JSR-203演進(jìn)而來证杭,它主要提供了如下三個(gè)方面的改進(jìn):
????????提供能夠批量獲取文件屬性的API,這些API具有平臺(tái)無關(guān)性妒御,不與特性的文件系統(tǒng)相耦合解愤,另外它還提供了標(biāo)準(zhǔn)文件系統(tǒng)的SPI,供各個(gè)服務(wù)提供商擴(kuò)展實(shí)現(xiàn)乎莉;
????????提供AIO功能送讲,支持基于文件的異步I/O操作和針對(duì)網(wǎng)絡(luò)套接字的異步操作;
????????完成JSR-51定義的通道功能惋啃,包括對(duì)配置和多播數(shù)據(jù)報(bào)的支持等哼鬓。
????????上文講述了UNIX環(huán)境的五種IO模型”呙穑基于這五種模型异希,在Java中,隨著NIO和NIO2.0(AIO)的引入绒瘦,一般具有BIO称簿、NIO和AIO網(wǎng)絡(luò)編程模型扣癣。
BIO
????????BIO是一個(gè)典型的網(wǎng)絡(luò)編程模型,是通常我們實(shí)現(xiàn)一個(gè)服務(wù)端程序的過程憨降,步驟如下:
????????主線程accept請(qǐng)求阻塞父虑;
????????請(qǐng)求到達(dá),創(chuàng)建新的線程來處理這個(gè)套接字授药,完成對(duì)客戶端的響應(yīng)士嚎;
????????主線程繼續(xù)accept下一個(gè)請(qǐng)求。
????????這種模型有一個(gè)很大的問題是:當(dāng)客戶端連接增多時(shí)悔叽,服務(wù)端創(chuàng)建的線程也會(huì)暴漲莱衩,系統(tǒng)性能會(huì)急劇下降。因此骄蝇,在此模型的基礎(chǔ)上膳殷,類似于 tomcat的bio connector操骡,采用的是線程池來避免對(duì)于每一個(gè)客戶端都創(chuàng)建一個(gè)線程九火。有些地方把這種方式叫做偽異步IO(把請(qǐng)求拋到線程池中異步等待處理)。
NIO
????????JDK1.4開始引入了NIO類庫册招,這里的NIO指的是Non-blcok IO岔激,主要是使用Selector多路復(fù)用器來實(shí)現(xiàn)。Selector在Linux等主流操作系統(tǒng)上是通過epoll實(shí)現(xiàn)的是掰。
????????NIO的實(shí)現(xiàn)流程虑鼎,類似于select:
????????創(chuàng)建ServerSocketChannel監(jiān)聽客戶端連接并綁定監(jiān)聽端口,設(shè)置為非阻塞模式键痛;
????????創(chuàng)建Reactor線程炫彩,創(chuàng)建多路復(fù)用器(Selector)并啟動(dòng)線程;
????????將ServerSocketChannel注冊(cè)到Reactor線程的Selector上絮短。監(jiān)聽accept事件江兢;
????????Selector在線程run方法中無線循環(huán)輪詢準(zhǔn)備就緒的Key;
????????Selector監(jiān)聽到新的客戶端接入丁频,處理新的請(qǐng)求杉允,完成tcp三次握手,建立物理連接席里;
????????將新的客戶端連接注冊(cè)到Selector上叔磷,監(jiān)聽讀操作。讀取客戶端發(fā)送的網(wǎng)絡(luò)消息奖磁;
????????客戶端發(fā)送的數(shù)據(jù)就緒則讀取客戶端請(qǐng)求改基,進(jìn)行處理。
????????相比BIO咖为,NIO的編程非常復(fù)雜秕狰。
AIO
????????JDK1.7引入NIO2.0嵌洼,提供了異步文件通道和異步套接字通道的實(shí)現(xiàn),是真正的異步非阻塞IO, 對(duì)應(yīng)于Unix中的異步IO封恰。
????????通常會(huì)有一個(gè)線程池用于執(zhí)行異步任務(wù)麻养,提交任務(wù)的線程將任務(wù)提交到線程池就可以立馬返回,不必等到任務(wù)真正完成诺舔。如果想要知道任務(wù)的執(zhí)行結(jié)果鳖昌,通常是通過傳遞一個(gè)回調(diào)函數(shù),任務(wù)結(jié)束后去調(diào)用這個(gè)函數(shù)或者Future get(需要用時(shí)編碼阻塞獲取)的方式低飒。同樣的原理许昨,Java 中的異步?IO 也是一樣的,都是由一個(gè)線程池來負(fù)責(zé)執(zhí)行任務(wù)褥赊,然后使用回調(diào)或自己去查詢結(jié)果糕档。異步?IO 主要是為了控制線程數(shù)量,減少過多的線程帶來的內(nèi)存消耗和?CPU 在線程調(diào)度上的開銷拌喉。
????????NIO的實(shí)現(xiàn)流程如下:
????????創(chuàng)建AsynchronousServerSocketChannel速那,綁定監(jiān)聽端口;
????????調(diào)用AsynchronousServerSocketChannel的accpet方法尿背,傳入自己實(shí)現(xiàn)的CompletionHandler(回調(diào)函數(shù))端仰。包括上一步,都是非阻塞的田藐;
????????連接傳入荔烧,回調(diào)CompletionHandler的completed方法,在里面汽久,調(diào)用AsynchronousSocketChannel的read方法鹤竭,傳入負(fù)責(zé)處理數(shù)據(jù)的CompletionHandler;
????????數(shù)據(jù)就緒景醇,觸發(fā)負(fù)責(zé)處理數(shù)據(jù)的CompletionHandler的completed方法臀稚。繼續(xù)做下一步處理即可。
????????寫入操作類似啡直,也需要傳入CompletionHandler烁涌。
總結(jié):