
周日午后,剛剛放下手里的電話,正在給剛剛的面試者寫評價。剛剛寫到『對Linux的基本IO模型理解不深』這句的時候,女朋友突然出現(xiàn)。



在Java中,主要有三種IO模型,分別是阻塞IO(BIO)、非阻塞IO(NIO)和 異步IO(AIO)。


Java中提供的IO有關(guān)的API,在文件處理的時候,其實依賴操作系統(tǒng)層面的IO操作實現(xiàn)的。比如在Linux 2.6以后,Java中NIO和AIO都是通過epoll來實現(xiàn)的,而在Windows上,AIO是通過IOCP來實現(xiàn)的。
可以把Java中的BIO、NIO和AIO理解為是Java語言對操作系統(tǒng)的各種IO模型的封裝。程序員在使用這些API的時候,不需要關(guān)心操作系統(tǒng)層面的知識,也不需要根據(jù)不同操作系統(tǒng)編寫不同的代碼。只需要使用Java的API就可以了。




在Linux(UNIX)操作系統(tǒng)中,共有五種IO模型,分別是:阻塞IO模型、非阻塞IO模型、IO復(fù)用模型、信號驅(qū)動IO模型以及異步IO模型。
既然提到晚上吃魚,那就通過釣魚的例子來解釋這五種IO模型吧。
到底什么是IO
我們常說的IO,指的是文件的輸入和輸出,但是在操作系統(tǒng)層面是如何定義IO的呢?到底什么樣的過程可以叫做是一次IO呢?
拿一次磁盤文件讀取為例,我們要讀取的文件是存儲在磁盤上的,我們的目的是把它讀取到內(nèi)存中。可以把這個步驟簡化成把數(shù)據(jù)從硬件(硬盤)中讀取到用戶空間中。
其實真正的文件讀取還涉及到緩存等細(xì)節(jié),這里就不展開講述了。關(guān)于用戶空間、內(nèi)核空間以及硬件等的關(guān)系如果讀者不理解的話,可以通過釣魚的例子理解。
釣魚的時候,剛開始魚是在魚塘里面的,我們的釣魚動作的最終結(jié)束標(biāo)志是魚從魚塘中被我們釣上來,放入魚簍中。
這里面的魚塘就可以映射成磁盤,中間過渡的魚鉤可以映射成內(nèi)核空間,最終放魚的魚簍可以映射成用戶空間。一次完整的釣魚(IO)操作,是魚(文件)從魚塘(硬盤)中轉(zhuǎn)移(拷貝)到魚簍(用戶空間)的過程。
阻塞IO模型
我們釣魚的時候,有一種方式比較愜意,比較輕松,那就是我們坐在魚竿面前,這個過程中我們什么也不做,雙手一直把著魚竿,就靜靜的等著魚兒咬鉤。一旦手上感受到魚的力道,就把魚釣起來放入魚簍中。然后再釣下一條魚。
映射到Linux操作系統(tǒng)中,這就是一種最簡單的IO模型,即阻塞IO。 阻塞 I/O 是最簡單的 I/O 模型,一般表現(xiàn)為進(jìn)程或線程等待某個條件,如果條件不滿足,則一直等下去。條件滿足,則進(jìn)行下一步操作。

應(yīng)用進(jìn)程通過系統(tǒng)調(diào)用 recvfrom 接收數(shù)據(jù),但由于內(nèi)核還未準(zhǔn)備好數(shù)據(jù)報,應(yīng)用進(jìn)程就會阻塞住,直到內(nèi)核準(zhǔn)備好數(shù)據(jù)報,recvfrom 完成數(shù)據(jù)報復(fù)制工作,應(yīng)用進(jìn)程才能結(jié)束阻塞狀態(tài)。
這種釣魚方式相對來說比較簡單,對于釣魚的人來說,不需要什么特制的魚竿,拿一根夠長的木棍就可以悠閑的開始釣魚了(實現(xiàn)簡單)。缺點就是比較耗費時間,比較適合那種對魚的需求量小的情況(并發(fā)低,時效性要求低)。


非阻塞IO模型
我們釣魚的時候,在等待魚兒咬鉤的過程中,我們可以做點別的事情,比如玩一把王者榮耀、看一集《延禧攻略》等等。但是,我們要時不時的去看一下魚竿,一旦發(fā)現(xiàn)有魚兒上鉤了,就把魚釣上來。
映射到Linux操作系統(tǒng)中,這就是非阻塞的IO模型。應(yīng)用進(jìn)程與內(nèi)核交互,目的未達(dá)到之前,不再一味的等著,而是直接返回。然后通過輪詢的方式,不停的去問內(nèi)核數(shù)據(jù)準(zhǔn)備有沒有準(zhǔn)備好。如果某一次輪詢發(fā)現(xiàn)數(shù)據(jù)已經(jīng)準(zhǔn)備好了,那就把數(shù)據(jù)拷貝到用戶空間中。

應(yīng)用進(jìn)程通過 recvfrom 調(diào)用不停的去和內(nèi)核交互,直到內(nèi)核準(zhǔn)備好數(shù)據(jù)。如果沒有準(zhǔn)備好,內(nèi)核會返回error,應(yīng)用進(jìn)程在得到error后,過一段時間再發(fā)送recvfrom請求。在兩次發(fā)送請求的時間段,進(jìn)程可以先做別的事情。
這種方式釣魚,和阻塞IO比,所使用的工具沒有什么變化,但是釣魚的時候可以做些其他事情,增加時間的利用率。




信號驅(qū)動IO模型
我們釣魚的時候,為了避免自己一遍一遍的去查看魚竿,我們可以給魚竿安裝一個報警器。當(dāng)有魚兒咬鉤的時候立刻報警。然后我們再收到報警后,去把魚釣起來。
映射到Linux操作系統(tǒng)中,這就是信號驅(qū)動IO。應(yīng)用進(jìn)程在讀取文件時通知內(nèi)核,如果某個 socket 的某個事件發(fā)生時,請向我發(fā)一個信號。在收到信號后,信號對應(yīng)的處理函數(shù)會進(jìn)行后續(xù)處理。

應(yīng)用進(jìn)程預(yù)先向內(nèi)核注冊一個信號處理函數(shù),然后用戶進(jìn)程返回,并且不阻塞,當(dāng)內(nèi)核數(shù)據(jù)準(zhǔn)備就緒時會發(fā)送一個信號給進(jìn)程,用戶進(jìn)程便在信號處理函數(shù)中開始把數(shù)據(jù)拷貝的用戶空間中。
這種方式釣魚,和前幾種相比,所使用的工具有了一些變化,需要有一些定制(實現(xiàn)復(fù)雜)。但是釣魚的人就可以在魚兒咬鉤之前徹底做別的事兒去了。等著報警器響就行了。




IO復(fù)用模型
我們釣魚的時候,為了保證可以最短的時間釣到最多的魚,我們同一時間擺放多個魚竿,同時釣魚。然后哪個魚竿有魚兒咬鉤了,我們就把哪個魚竿上面的魚釣起來。
映射到Linux操作系統(tǒng)中,這就是IO復(fù)用模型。多個進(jìn)程的IO可以注冊到同一個管道上,這個管道會統(tǒng)一和內(nèi)核進(jìn)行交互。當(dāng)管道中的某一個請求需要的數(shù)據(jù)準(zhǔn)備好之后,進(jìn)程再把對應(yīng)的數(shù)據(jù)拷貝到用戶空間中。

IO多路轉(zhuǎn)接是多了一個select函數(shù),多個進(jìn)程的IO可以注冊到同一個select上,當(dāng)用戶進(jìn)程調(diào)用該select,select會監(jiān)聽所有注冊好的IO,如果所有被監(jiān)聽的IO需要的數(shù)據(jù)都沒有準(zhǔn)備好時,select調(diào)用進(jìn)程會阻塞。當(dāng)任意一個IO所需的數(shù)據(jù)準(zhǔn)備好之后,select調(diào)用就會返回,然后進(jìn)程在通過recvfrom來進(jìn)行數(shù)據(jù)拷貝。
這里的IO復(fù)用模型,并沒有向內(nèi)核注冊信號處理函數(shù),所以,他并不是非阻塞的。進(jìn)程在發(fā)出select后,要等到select監(jiān)聽的所有IO操作中至少有一個需要的數(shù)據(jù)準(zhǔn)備好,才會有返回,并且也需要再次發(fā)送請求去進(jìn)行文件的拷貝。
這種方式的釣魚,通過增加魚竿的方式,可以有效的提升效率。




為什么以上四種都是同步的
我們說阻塞IO模型、非阻塞IO模型、IO復(fù)用模型和信號驅(qū)動IO模型都是同步的IO模型。原因是因為,無論以上那種模型,真正的數(shù)據(jù)拷貝過程,都是同步進(jìn)行的。
信號驅(qū)動難道不是異步的么? 信號驅(qū)動,內(nèi)核是在數(shù)據(jù)準(zhǔn)備好之后通知進(jìn)程,然后進(jìn)程再通過recvfrom操作進(jìn)行數(shù)據(jù)拷貝。我們可以認(rèn)為數(shù)據(jù)準(zhǔn)備階段是異步的,但是,數(shù)據(jù)拷貝操作是同步的。所以,整個IO過程也不能認(rèn)為是異步的。


我們把釣魚過程,可以拆分為兩個步驟:1、魚咬鉤(數(shù)據(jù)準(zhǔn)備)。2、把魚釣起來放進(jìn)魚簍里(數(shù)據(jù)拷貝)。無論以上提到的哪種釣魚方式,在第二步,都是需要人主動去做的,并不是魚竿自己完成的。所以,這個釣魚過程其實還是同步進(jìn)行的。


燒水的報警器一響,整個燒水過程就完成了。水已經(jīng)是開水了。
釣魚的報警器一響,只能說明魚兒已經(jīng)咬鉤了,但是還沒有真正的釣上來。
所以 ,使用帶有報警器的水壺?zé)?,燒水過程是異步的。
而使用帶有報警器的魚竿釣魚,釣魚的過程還是同步的。


異步IO模型
我們釣魚的時候,采用一種高科技釣魚竿,即全自動釣魚竿。可以自動感應(yīng)魚上鉤,自動收竿,更厲害的可以自動把魚放進(jìn)魚簍里。然后,通知我們魚已經(jīng)釣到了,他就繼續(xù)去釣下一條魚去了。
映射到Linux操作系統(tǒng)中,這就是異步IO模型。應(yīng)用進(jìn)程把IO請求傳給內(nèi)核后,完全由內(nèi)核去操作文件拷貝。內(nèi)核完成相關(guān)操作后,會發(fā)信號告訴應(yīng)用進(jìn)程本次IO已經(jīng)完成。

用戶進(jìn)程發(fā)起aio_read操作之后,給內(nèi)核傳遞描述符、緩沖區(qū)指針、緩沖區(qū)大小等,告訴內(nèi)核當(dāng)整個操作完成時,如何通知進(jìn)程,然后就立刻去做其他事情了。當(dāng)內(nèi)核收到aio_read后,會立刻返回,然后內(nèi)核開始等待數(shù)據(jù)準(zhǔn)備,數(shù)據(jù)準(zhǔn)備好以后,直接把數(shù)據(jù)拷貝到用戶控件,然后再通知進(jìn)程本次IO已經(jīng)完成。
這種方式的釣魚,無疑是最省事兒的。啥都不需要管,只需要交給魚竿就可以了。


5種IO模型對比





介紹完這些之后,我默默的刪掉了之前寫好的那句面試評價『對Linux的基本IO模型理解不深』,改成了『對IO體系理解的不夠深入,只會使用封裝好的API』。
對 JAVA 開發(fā)有興趣的朋友歡迎加入QQ群:710373545 里面資深架構(gòu)師會分享一些整理好的錄制視頻錄像和BATJ面試題:有Spring,MyBatis,Netty源碼分析,高并發(fā)、高性能、分布式、微服務(wù)架構(gòu)的原理,JVM性能優(yōu)化、分布式架構(gòu)等這些成為架構(gòu)師必備的知識體系。還能領(lǐng)取免費的學(xué)習(xí)資源,目前受益良多。
共同探討!
