好多年沒(méi)記錄技術(shù)筆記童擎,接下來(lái)已經(jīng)安下心深入學(xué)習(xí)一些底層的知識(shí)弯菊。最近在學(xué)習(xí)php swoole的擴(kuò)展中疮丛,看到了不可在I/O中進(jìn)行死循環(huán)焕数,其中它使用的是I/O的Reactor模型」蚶悖看到這里缀去,就想了解下常掛嘴邊的I/O的模型。以下內(nèi)容均為度娘甸祭,谷哥提供缕碎。
I/O模型共有四類
(1)同步阻塞IO(Blocking IO):即傳統(tǒng)的IO模型。
(2)同步非阻塞IO(Non-blocking IO):默認(rèn)創(chuàng)建的socket都是阻塞的池户,非阻塞IO要求socket被設(shè)置為NONBLOCK咏雌。注意這里所說(shuō)的NIO并非Java的NIO(New IO)庫(kù)。
(3)IO多路復(fù)用(IO Multiplexing):即經(jīng)典的Reactor設(shè)計(jì)模式校焦,有時(shí)也稱為異步阻塞IO赊抖,Java中的Selector和Linux中的epoll都是這種模型。
(4)異步IO(Asynchronous IO):即經(jīng)典的Proactor設(shè)計(jì)模式寨典,也稱為異步非阻塞IO氛雪。
一、同步阻塞I/O
同步阻塞IO模型是最簡(jiǎn)單的IO模型耸成,用戶線程在內(nèi)核進(jìn)行IO操作時(shí)被阻塞报亩。
圖1?同步阻塞IO
如圖1所示,用戶線程通過(guò)系統(tǒng)調(diào)用read發(fā)起IO讀操作井氢,由用戶空間轉(zhuǎn)到內(nèi)核空間弦追。內(nèi)核等到數(shù)據(jù)包到達(dá)后,然后將接收的數(shù)據(jù)拷貝到用戶空間毙沾,完成read操作骗卜。
用戶線程使用同步阻塞IO模型的偽代碼描述為:
{
while(read(socket, buffer)!= SUCCESS)
;
process(buffer);
}
即用戶需要等待read將socket中的數(shù)據(jù)讀取到buffer后,才繼續(xù)處理接收的數(shù)據(jù)。整個(gè)IO請(qǐng)求的過(guò)程中流码,用戶線程是被阻塞的三妈,這導(dǎo)致用戶在發(fā)起IO請(qǐng)求時(shí),不能做任何事情箭昵,對(duì)CPU的資源利用率不夠。
二、同步非阻塞
同步非阻塞IO是在同步阻塞IO的基礎(chǔ)上躺枕,將socket設(shè)置為NONBLOCK。這樣做用戶線程可以在發(fā)起IO請(qǐng)求后可以立即返回。
圖2?同步非阻塞IO
如圖2所示拐云,由于socket是非阻塞的方式罢猪,因此用戶線程發(fā)起IO請(qǐng)求時(shí)立即返回。但并未讀取到任何數(shù)據(jù)叉瘩,用戶線程需要不斷地發(fā)起IO請(qǐng)求膳帕,直到數(shù)據(jù)到達(dá)后,才真正讀取到數(shù)據(jù)薇缅,繼續(xù)執(zhí)行危彩。
用戶線程使用同步非阻塞IO模型的偽代碼描述為:
{
while(read(socket,?buffer)!=?SUCCESS)
;
process(buffer);
}
即用戶需要不斷地調(diào)用read泳桦,嘗試讀取socket中的數(shù)據(jù)汤徽,直到讀取成功后,才繼續(xù)處理接收的數(shù)據(jù)灸撰。整個(gè)IO請(qǐng)求的過(guò)程中谒府,雖然用戶線程每次發(fā)起IO請(qǐng)求后可以立即返回,但是為了等到數(shù)據(jù)浮毯,仍需要不斷地輪詢狱掂、重復(fù)請(qǐng)求,消耗了大量的CPU的資源亲轨。一般很少直接使用這種模型趋惨,而是在其他IO模型中使用非阻塞IO這一特性。
三惦蚊、異步阻塞
IO多路復(fù)用模型是建立在內(nèi)核提供的多路分離函數(shù)select基礎(chǔ)之上的器虾,使用select函數(shù)可以避免同步非阻塞IO模型中輪詢等待的問(wèn)題。
圖3?多路分離函數(shù)select
如圖3所示蹦锋,用戶首先將需要進(jìn)行IO操作的socket添加到select中兆沙,然后阻塞等待select系統(tǒng)調(diào)用返回。當(dāng)數(shù)據(jù)到達(dá)時(shí)莉掂,socket被激活葛圃,select函數(shù)返回。用戶線程正式發(fā)起read請(qǐng)求憎妙,讀取數(shù)據(jù)并繼續(xù)執(zhí)行库正。
從流程上來(lái)看,使用select函數(shù)進(jìn)行IO請(qǐng)求和同步阻塞模型沒(méi)有太大的區(qū)別厘唾,甚至還多了添加監(jiān)視socket褥符,以及調(diào)用select函數(shù)的額外操作,效率更差抚垃。但是喷楣,使用select以后最大的優(yōu)勢(shì)是用戶可以在一個(gè)線程內(nèi)同時(shí)處理多個(gè)socket的IO請(qǐng)求趟大。用戶可以注冊(cè)多個(gè)socket,然后不斷地調(diào)用select讀取被激活的socket铣焊,即可達(dá)到在同一個(gè)線程內(nèi)同時(shí)處理多個(gè)IO請(qǐng)求的目的逊朽。而在同步阻塞模型中,必須通過(guò)多線程的方式才能達(dá)到這個(gè)目的曲伊。
用戶線程使用select函數(shù)的偽代碼描述為:
{
select(socket);
while(1){
sockets?=?select();
for(socket?in?sockets)?{
if(can_read(socket))?{
read(socket,?buffer);
process(buffer);
}
}
}
}
其中while循環(huán)前將socket添加到select監(jiān)視中惋耙,然后在while內(nèi)一直調(diào)用select獲取被激活的socket,一旦socket可讀熊昌,便調(diào)用read函數(shù)將socket中的數(shù)據(jù)讀取出來(lái)绽榛。
然而,使用select函數(shù)的優(yōu)點(diǎn)并不僅限于此婿屹。雖然上述方式允許單線程內(nèi)處理多個(gè)IO請(qǐng)求灭美,但是每個(gè)IO請(qǐng)求的過(guò)程還是阻塞的(在select函數(shù)上阻塞),平均時(shí)間甚至比同步阻塞IO模型還要長(zhǎng)昂利。如果用戶線程只注冊(cè)自己感興趣的socket或者IO請(qǐng)求届腐,然后去做自己的事情,等到數(shù)據(jù)到來(lái)時(shí)再進(jìn)行處理蜂奸,則可以提高CPU的利用率犁苏。
IO多路復(fù)用模型使用了Reactor設(shè)計(jì)模式實(shí)現(xiàn)了這一機(jī)制。
圖4?Reactor設(shè)計(jì)模式
如圖4所示扩所,EventHandler抽象類表示IO事件處理器围详,它擁有IO文件句柄Handle(通過(guò)get_handle獲取)祖屏,以及對(duì)Handle的操作handle_event(讀/寫等)助赞。繼承于EventHandler的子類可以對(duì)事件處理器的行為進(jìn)行定制。Reactor類用于管理EventHandler(注冊(cè)袁勺、刪除等)雹食,并使用handle_events實(shí)現(xiàn)事件循環(huán),不斷調(diào)用同步事件多路分離器(一般是內(nèi)核)的多路分離函數(shù)select期丰,只要某個(gè)文件句柄被激活(可讀/寫等)群叶,select就返回(阻塞),handle_events就會(huì)調(diào)用與文件句柄關(guān)聯(lián)的事件處理器的handle_event進(jìn)行相關(guān)操作钝荡。
圖5?IO多路復(fù)用
如圖5所示街立,通過(guò)Reactor的方式,可以將用戶線程輪詢IO操作狀態(tài)的工作統(tǒng)一交給handle_events事件循環(huán)進(jìn)行處理化撕。用戶線程注冊(cè)事件處理器之后可以繼續(xù)執(zhí)行做其他的工作(異步)几晤,而Reactor線程負(fù)責(zé)調(diào)用內(nèi)核的select函數(shù)檢查socket狀態(tài)约炎。當(dāng)有socket被激活時(shí)植阴,則通知相應(yīng)的用戶線程(或執(zhí)行用戶線程的回調(diào)函數(shù))蟹瘾,執(zhí)行handle_event進(jìn)行數(shù)據(jù)讀取、處理的工作掠手。由于select函數(shù)是阻塞的憾朴,因此多路IO復(fù)用模型也被稱為異步阻塞IO模型。注意喷鸽,這里的所說(shuō)的阻塞是指select函數(shù)執(zhí)行時(shí)線程被阻塞众雷,而不是指socket。一般在使用IO多路復(fù)用模型時(shí)做祝,socket都是設(shè)置為NONBLOCK的砾省,不過(guò)這并不會(huì)產(chǎn)生影響,因?yàn)橛脩舭l(fā)起IO請(qǐng)求時(shí)混槐,數(shù)據(jù)已經(jīng)到達(dá)了编兄,用戶線程一定不會(huì)被阻塞。
用戶線程使用IO多路復(fù)用模型的偽代碼描述為:
void?UserEventHandler::handle_event()?{
if(can_read(socket))?{
read(socket,?buffer);
process(buffer);
}
}
{
Reactor.register(new?UserEventHandler(socket));
}
用戶需要重寫EventHandler的handle_event函數(shù)進(jìn)行讀取數(shù)據(jù)声登、處理數(shù)據(jù)的工作狠鸳,用戶線程只需要將自己的EventHandler注冊(cè)到Reactor即可。Reactor中handle_events事件循環(huán)的偽代碼大致如下悯嗓。
Reactor::handle_events()?{
while(1)?{
sockets?=?select();
for(socket?in?sockets)?{
get_event_handler(socket).handle_event();
}
}
}
事件循環(huán)不斷地調(diào)用select獲取被激活的socket件舵,然后根據(jù)獲取socket對(duì)應(yīng)的EventHandler,執(zhí)行器handle_event函數(shù)即可脯厨。
IO多路復(fù)用是最常使用的IO模型铅祸,但是其異步程度還不夠“徹底”,因?yàn)樗褂昧藭?huì)阻塞線程的select系統(tǒng)調(diào)用合武。因此IO多路復(fù)用只能稱為異步阻塞IO个少,而非真正的異步IO。
四眯杏、異步非阻塞
“真正”的異步IO需要操作系統(tǒng)更強(qiáng)的支持夜焦。在IO多路復(fù)用模型中,事件循環(huán)將文件句柄的狀態(tài)事件通知給用戶線程岂贩,由用戶線程自行讀取數(shù)據(jù)茫经、處理數(shù)據(jù)。而在異步IO模型中萎津,當(dāng)用戶線程收到通知時(shí)卸伞,數(shù)據(jù)已經(jīng)被內(nèi)核讀取完畢,并放在了用戶線程指定的緩沖區(qū)內(nèi)锉屈,內(nèi)核在IO完成后通知用戶線程直接使用即可荤傲。
異步IO模型使用了Proactor設(shè)計(jì)模式實(shí)現(xiàn)了這一機(jī)制。
圖6?Proactor設(shè)計(jì)模式
如圖6颈渊,Proactor模式和Reactor模式在結(jié)構(gòu)上比較相似遂黍,不過(guò)在用戶(Client)使用方式上差別較大终佛。Reactor模式中,用戶線程通過(guò)向Reactor對(duì)象注冊(cè)感興趣的事件監(jiān)聽雾家,然后事件觸發(fā)時(shí)調(diào)用事件處理函數(shù)铃彰。而Proactor模式中,用戶線程將AsynchronousOperation(讀/寫等)芯咧、Proactor以及操作完成時(shí)的CompletionHandler注冊(cè)到AsynchronousOperationProcessor牙捉。AsynchronousOperationProcessor使用Facade模式提供了一組異步操作API(讀/寫等)供用戶使用,當(dāng)用戶線程調(diào)用異步API后敬飒,便繼續(xù)執(zhí)行自己的任務(wù)邪铲。AsynchronousOperationProcessor?會(huì)開啟獨(dú)立的內(nèi)核線程執(zhí)行異步操作,實(shí)現(xiàn)真正的異步无拗。當(dāng)異步IO操作完成時(shí)霜浴,AsynchronousOperationProcessor將用戶線程與AsynchronousOperation一起注冊(cè)的Proactor和CompletionHandler取出,然后將CompletionHandler與IO操作的結(jié)果數(shù)據(jù)一起轉(zhuǎn)發(fā)給Proactor蓝纲,Proactor負(fù)責(zé)回調(diào)每一個(gè)異步操作的事件完成處理函數(shù)handle_event阴孟。雖然Proactor模式中每個(gè)異步操作都可以綁定一個(gè)Proactor對(duì)象,但是一般在操作系統(tǒng)中税迷,Proactor被實(shí)現(xiàn)為Singleton模式永丝,以便于集中化分發(fā)操作完成事件。
圖7?異步IO
如圖7所示箭养,異步IO模型中慕嚷,用戶線程直接使用內(nèi)核提供的異步IO?API發(fā)起read請(qǐng)求,且發(fā)起后立即返回毕泌,繼續(xù)執(zhí)行用戶線程代碼喝检。不過(guò)此時(shí)用戶線程已經(jīng)將調(diào)用的AsynchronousOperation和CompletionHandler注冊(cè)到內(nèi)核,然后操作系統(tǒng)開啟獨(dú)立的內(nèi)核線程去處理IO操作撼泛。當(dāng)read請(qǐng)求的數(shù)據(jù)到達(dá)時(shí)挠说,由內(nèi)核負(fù)責(zé)讀取socket中的數(shù)據(jù),并寫入用戶指定的緩沖區(qū)中愿题。最后內(nèi)核將read的數(shù)據(jù)和用戶線程注冊(cè)的CompletionHandler分發(fā)給內(nèi)部Proactor损俭,Proactor將IO完成的信息通知給用戶線程(一般通過(guò)調(diào)用用戶線程注冊(cè)的完成事件處理函數(shù)),完成異步IO潘酗。
用戶線程使用異步IO模型的偽代碼描述為:
void?UserCompletionHandler::handle_event(buffer)?{
process(buffer);
}
{
aio_read(socket,?new?UserCompletionHandler);
}
用戶需要重寫CompletionHandler的handle_event函數(shù)進(jìn)行處理數(shù)據(jù)的工作杆兵,參數(shù)buffer表示Proactor已經(jīng)準(zhǔn)備好的數(shù)據(jù),用戶線程直接調(diào)用內(nèi)核提供的異步IO?API仔夺,并將重寫的CompletionHandler注冊(cè)即可琐脏。
相比于IO多路復(fù)用模型,異步IO并不十分常用,不少高性能并發(fā)服務(wù)程序使用IO多路復(fù)用模型+多線程任務(wù)處理的架構(gòu)基本可以滿足需求日裙。況且目前操作系統(tǒng)對(duì)異步IO的支持并非特別完善吹艇,更多的是采用IO多路復(fù)用模型模擬異步IO的方式(IO事件觸發(fā)時(shí)不直接通知用戶線程,而是將數(shù)據(jù)讀寫完畢后放到用戶指定的緩沖區(qū)中)
基本復(fù)制于: 高性能IO模型淺析