1 概念說明#
在進行解釋之前,首先要說明幾個概念:
用戶空間和內(nèi)核空間
進程切換
進程的阻塞
文件描述符
緩存 IO
現(xiàn)在操作系統(tǒng)都是采用虛擬存儲器,那么對32位操作系統(tǒng)而言,它的尋址空間(虛擬存儲空間)為4G(2的32次方)崎溃。操作系統(tǒng)的核心是內(nèi)核材义,獨立于普通的應(yīng)用程序蠕蚜,可以訪問受保護的內(nèi)存空間梢灭,也有訪問底層硬件設(shè)備的所有權(quán)限。為了保證用戶進程不能直接操作內(nèi)核(kernel)幕侠,保證內(nèi)核的安全帝美,操作系統(tǒng)將虛擬空間劃分為兩部分,一部分為內(nèi)核空間晤硕,一部分為用戶空間悼潭。針對linux操作系統(tǒng)而言,將最高的1G字節(jié)(從虛擬地址0xC0000000到0xFFFFFFFF)舞箍,供內(nèi)核使用舰褪,稱為內(nèi)核空間,而將較低的3G字節(jié)(從虛擬地址0x00000000到0xBFFFFFFF)疏橄,供各個進程使用占拍,稱為用戶空間。
為了控制進程的執(zhí)行软族,內(nèi)核必須有能力掛起正在CPU上運行的進程刷喜,并恢復以前掛起的某個進程的執(zhí)行残制。這種行為被稱為進程切換立砸。因此可以說,任何進程都是在操作系統(tǒng)內(nèi)核的支持下運行的初茶,是與內(nèi)核緊密相關(guān)的颗祝。
從一個進程的運行轉(zhuǎn)到另一個進程上運行,這個過程中經(jīng)過下面這些變化:
保存處理機上下文恼布,包括程序計數(shù)器和其他寄存器螺戳。
更新PCB信息。
把進程的PCB移入相應(yīng)的隊列折汞,如就緒倔幼、在某事件阻塞等隊列。
選擇另一個進程執(zhí)行爽待,并更新其PCB损同。
更新內(nèi)存管理的數(shù)據(jù)結(jié)構(gòu)。
恢復處理機上下文鸟款。
注:總而言之就是很耗資源膏燃,具體的可以參考這篇文章:進程切換。
正在執(zhí)行的進程何什,由于期待的某些事件未發(fā)生组哩,如請求系統(tǒng)資源失敗、等待某種操作的完成、新數(shù)據(jù)尚未到達或無新工作做等伶贰,則由系統(tǒng)自動執(zhí)行阻塞原語(Block)蛛砰,使自己由運行狀態(tài)變?yōu)樽枞麪顟B(tài)∈蜓茫可見暴备,進程的阻塞是進程自身的一種主動行為,也因此只有處于運行態(tài)的進程(獲得CPU)们豌,才可能將其轉(zhuǎn)為阻塞狀態(tài)涯捻。當進程進入阻塞狀態(tài),是不占用CPU資源的望迎。
文件描述符(File descriptor)是計算機科學中的一個術(shù)語障癌,是一個用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一個非負整數(shù)辩尊。實際上涛浙,它是一個索引值,指向內(nèi)核為每一個進程所維護的該進程打開文件的記錄表摄欲。當程序打開一個現(xiàn)有文件或者創(chuàng)建一個新文件時,內(nèi)核向進程返回一個文件描述符我注。在程序設(shè)計中,一些涉及底層的程序編寫往往會圍繞著文件描述符展開但骨。但是文件描述符這一概念往往只適用于UNIX智袭、Linux這樣的操作系統(tǒng)奔缠。
緩存 IO 又被稱作標準 IO,大多數(shù)文件系統(tǒng)的默認 IO 操作都是緩存 IO吼野。在 Linux 的緩存 IO 機制中校哎,操作系統(tǒng)會將 IO 的數(shù)據(jù)緩存在文件系統(tǒng)的頁緩存( page cache )中,也就是說瞳步,數(shù)據(jù)會先被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中闷哆,然后才會從操作系統(tǒng)內(nèi)核的緩沖區(qū)拷貝到應(yīng)用程序的地址空間。
緩存 IO 的缺點:
數(shù)據(jù)在傳輸過程中需要在應(yīng)用程序地址空間和內(nèi)核進行多次數(shù)據(jù)拷貝操作谚攒,這些數(shù)據(jù)拷貝操作所帶來的 CPU 以及內(nèi)存開銷是非常大的阳准。
網(wǎng)絡(luò)IO的本質(zhì)是socket的讀取,socket在linux系統(tǒng)被抽象為流馏臭,IO可以理解為對流的操作野蝇。剛才說了讼稚,對于一次IO訪問(以read舉例),數(shù)據(jù)會先被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中绕沈,然后才會從操作系統(tǒng)內(nèi)核的緩沖區(qū)拷貝到應(yīng)用程序的地址空間锐想。所以說,當一個read操作發(fā)生時乍狐,它會經(jīng)歷兩個階段:
第一階段:等待數(shù)據(jù)準備 (Waiting for the data to be ready)赠摇。
第二階段:將數(shù)據(jù)從內(nèi)核拷貝到進程中 (Copying the data from the kernel to the process)。
對于socket流而言浅蚪,
第一步:通常涉及等待網(wǎng)絡(luò)上的數(shù)據(jù)分組到達藕帜,然后被復制到內(nèi)核的某個緩沖區(qū)。
第二步:把數(shù)據(jù)從內(nèi)核緩沖區(qū)復制到應(yīng)用進程緩沖區(qū)惜傲。
網(wǎng)絡(luò)應(yīng)用需要處理的無非就是兩大類問題洽故,網(wǎng)絡(luò)IO,數(shù)據(jù)計算盗誊。相對于后者时甚,網(wǎng)絡(luò)IO的延遲荒适,給應(yīng)用帶來的性能瓶頸大于后者刀诬。網(wǎng)絡(luò)IO的模型大致有如下幾種:
同步模型(synchronous IO)
阻塞IO(bloking IO)
非阻塞IO(non-blocking IO)
多路復用IO(multiplexing IO)
信號驅(qū)動式IO(signal-driven IO)
異步IO(asynchronous IO)
注:由于signal driven IO在實際中并不常用摊阀,所以我這只提及剩下的四種IO Model纪隙。
在深入介紹Linux IO各種模型之前蒲肋,讓我們先來探索一下基本 Linux IO 模型的簡單矩陣奋早。如下圖所示:
輸入圖片說明
每個 IO 模型都有自己的使用模式愤炸,它們對于特定的應(yīng)用程序都有自己的優(yōu)點规个。本節(jié)將簡要對其一一進行介紹诞仓。常見的IO模型有阻塞墅拭、非阻塞谍婉、IO多路復用穗熬,異步死陆。以一個生動形象的例子來說明這四個概念别凤。周末我和女友去逛街规哪,中午餓了诉稍,我們準備去吃飯杯巨。周末人多服爷,吃飯需要排隊仍源,我和女友有以下幾種方案笼踩。
我和女友點完餐后嚎于,不知道什么時候能做好匾旭,只好坐在餐廳里面等女蜈,直到做好伪窖,然后吃完才離開覆山。女友本想還和我一起逛街的簇宽,但是不知道飯能什么時候做好魏割,只好和我一起在餐廳等钞它,而不能去逛街遭垛,直到吃完飯才能去逛街锯仪,中間等待做飯的時間浪費掉了。這就是典型的阻塞谤碳。
同步阻塞 IO 模型是最常用的一個模型,也是最簡單的模型漩仙。在linux中,默認情況下所有的socket都是blocking峻村。它符合人們最常見的思考邏輯粘昨。阻塞就是進程 "被" 休息, CPU處理其它進程去了张肾。
在這個IO模型中吞瞪,用戶空間的應(yīng)用程序執(zhí)行一個系統(tǒng)調(diào)用(recvform)芍秆,這會導致應(yīng)用程序阻塞,什么也不干眉菱,直到數(shù)據(jù)準備好克伊,并且將數(shù)據(jù)從內(nèi)核復制到用戶進程华坦,最后進程再處理數(shù)據(jù)犁跪,在等待數(shù)據(jù)到處理數(shù)據(jù)的兩個階段歹袁,整個進程都被阻塞条舔。不能處理別的網(wǎng)絡(luò)IO孟抗。調(diào)用應(yīng)用程序處于一種不再消費 CPU 而只是簡單等待響應(yīng)的狀態(tài),因此從處理的角度來看捷沸,這是非常有效的痒给。在調(diào)用recv()/recvfrom()函數(shù)時侈玄,發(fā)生在內(nèi)核中等待數(shù)據(jù)和復制數(shù)據(jù)的過程序仙,大致如下圖:
輸入圖片說明
當用戶進程調(diào)用了recv()/recvfrom()這個系統(tǒng)調(diào)用潘悼,kernel就開始了IO的第一個階段:準備數(shù)據(jù)(對于網(wǎng)絡(luò)IO來說治唤,很多時候數(shù)據(jù)在一開始還沒有到達。比如柜裸,還沒有收到一個完整的UDP包疙挺。這個時候kernel就要等待足夠的數(shù)據(jù)到來)蔬崩。這個過程需要等待搀暑,也就是說數(shù)據(jù)被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中是需要一個過程的沪袭。而在用戶進程這邊冈绊,整個進程會被阻塞(當然死宣,是進程自己選擇的阻塞)毅该。第二個階段:當kernel一直等到數(shù)據(jù)準備好了眶掌,它就會將數(shù)據(jù)從kernel中拷貝到用戶內(nèi)存朴爬,然后kernel返回結(jié)果召噩,用戶進程才解除block的狀態(tài)具滴,重新運行起來构韵。
所以疲恢,blocking IO的特點就是在IO執(zhí)行的兩個階段都被block了冈闭。
優(yōu)點:
能夠及時返回數(shù)據(jù)萎攒,無延遲;
對內(nèi)核開發(fā)者來說這是省事了羊精;
缺點:
對用戶來說處于等待就要付出性能的代價了喧锦;
2.2 同步非阻塞 IO(nonblocking IO)##
我女友不甘心白白在這等燃少,又想去逛商場阵具,又擔心飯好了怕敬。所以我們逛一會东跪,回來詢問服務(wù)員飯好了沒有越庇,來來回回好多次卤唉,飯都還沒吃都快累死了啦。這就是非阻塞熬的。需要不斷的詢問赊级,是否準備好了橡伞。
同步非阻塞就是 “每隔一會兒瞄一眼進度條” 的輪詢(polling)方式。在這種模型中羡洛,設(shè)備是以非阻塞的形式打開的。這意味著 IO 操作不會立即完成肋联,read 操作可能會返回一個錯誤代碼牺蹄,說明這個命令不能立即滿足(EAGAIN 或 EWOULDBLOCK)氓奈。
在網(wǎng)絡(luò)IO時候舀奶,非阻塞IO也會進行recvform系統(tǒng)調(diào)用但荤,檢查數(shù)據(jù)是否準備好,與阻塞IO不一樣涧至,"非阻塞將大的整片時間的阻塞分成N多的小的阻塞, 所以進程不斷地有機會 '被' CPU光顧"腹躁。
也就是說非阻塞的recvform系統(tǒng)調(diào)用調(diào)用之后,進程并沒有被阻塞南蓬,內(nèi)核馬上返回給進程纺非,如果數(shù)據(jù)還沒準備好,此時會返回一個error赘方。進程在返回之后烧颖,可以干點別的事情,然后再發(fā)起recvform系統(tǒng)調(diào)用。重復上面的過程,循環(huán)往復的進行recvform系統(tǒng)調(diào)用。這個過程通常被稱之為輪詢。輪詢檢查內(nèi)核數(shù)據(jù)反砌,直到數(shù)據(jù)準備好酒贬,再拷貝數(shù)據(jù)到進程零如,進行數(shù)據(jù)處理。需要注意,拷貝數(shù)據(jù)整個過程,進程仍然是屬于阻塞的狀態(tài)。
在linux下敞临,可以通過設(shè)置socket使其變?yōu)閚on-blocking。當對一個non-blocking socket執(zhí)行讀操作時,流程如圖所示:
輸入圖片說明
當用戶進程發(fā)出read操作時忍啸,如果kernel中的數(shù)據(jù)還沒有準備好,那么它并不會block用戶進程,而是立刻返回一個error溪椎。從用戶進程角度講蛾洛,它發(fā)起一個read操作后谎碍,并不需要等待提岔,而是馬上就得到了一個結(jié)果。用戶進程判斷結(jié)果是一個error時季惯,它就知道數(shù)據(jù)還沒有準備好藕筋,于是它可以再次發(fā)送read操作暇藏。一旦kernel中的數(shù)據(jù)準備好了甸各,并且又再次收到了用戶進程的system call诫尽,那么它馬上就將數(shù)據(jù)拷貝到了用戶內(nèi)存送淆,然后返回。
所以惕澎,nonblocking IO的特點是用戶進程需要不斷的主動詢問kernel數(shù)據(jù)好了沒有祟绊。
同步非阻塞方式相比同步阻塞方式:
優(yōu)點:能夠在等待任務(wù)完成的時間里干其他活了(包括提交其他任務(wù)泽腮,也就是 “后臺” 可以有多個任務(wù)在同時執(zhí)行)。
缺點:任務(wù)完成的響應(yīng)延遲增大了,因為每過一段時間才去輪詢一次read操作,而任務(wù)可能在兩次輪詢之間的任意時間完成察署。這會導致整體數(shù)據(jù)吞吐量的降低梅尤。
2.3 IO 多路復用( IO multiplexing)##
與第二個方案差不多号枕,餐廳安裝了電子屏幕用來顯示點餐的狀態(tài)赞厕,這樣我和女友逛街一會蒜茴,回來就不用去詢問服務(wù)員了久信,直接看電子屏幕就可以了裹虫。這樣每個人的餐是否好了筑公,都直接看電子屏幕就可以了雳窟,這就是典型的IO多路復用。
由于同步非阻塞方式需要不斷主動輪詢十酣,輪詢占據(jù)了很大一部分過程涩拙,輪詢會消耗大量的CPU時間,而 “后臺” 可能有多個任務(wù)在同時進行耸采,人們就想到了循環(huán)查詢多個任務(wù)的完成狀態(tài)兴泥,只要有任何一個任務(wù)完成,就去處理它虾宇。如果輪詢不是進程的用戶態(tài)搓彻,而是有人幫忙就好了。那么這就是所謂的 “IO 多路復用”嘱朽。UNIX/Linux 下的 select旭贬、poll、epoll 就是干這個的(epoll 比 poll搪泳、select 效率高稀轨,做的事情是一樣的)。
IO多路復用有兩個特別的系統(tǒng)調(diào)用select岸军、poll奋刽、epoll函數(shù)。select調(diào)用是內(nèi)核級別的艰赞,select輪詢相對非阻塞的輪詢的區(qū)別在于---前者可以等待多個socket佣谐,能實現(xiàn)同時對多個IO端口進行監(jiān)聽,當其中任何一個socket的數(shù)據(jù)準好了方妖,就能返回進行可讀狭魂,然后進程再進行recvform系統(tǒng)調(diào)用,將數(shù)據(jù)由內(nèi)核拷貝到用戶進程,當然這個過程是阻塞的雌澄。select或poll調(diào)用之后斋泄,會阻塞進程,與blocking IO阻塞不同在于掷伙,此時的select不是等到socket數(shù)據(jù)全部到達再處理, 而是有了一部分數(shù)據(jù)就會調(diào)用用戶進程來處理是己。如何知道有一部分數(shù)據(jù)到達了呢?監(jiān)視的事情交給了內(nèi)核任柜,內(nèi)核負責數(shù)據(jù)到達的處理卒废。也可以理解為"非阻塞"吧。
I/O復用模型會用到select宙地、poll摔认、epoll函數(shù),這幾個函數(shù)也會使進程阻塞宅粥,但是和阻塞I/O所不同的的参袱,這兩個函數(shù)可以同時阻塞多個I/O操作。而且可以同時對多個讀操作秽梅,多個寫操作的I/O函數(shù)進行檢測抹蚀,直到有數(shù)據(jù)可讀或可寫時(注意不是全部數(shù)據(jù)可讀或可寫),才真正調(diào)用I/O操作函數(shù)企垦。
對于多路復用环壤,也就是輪詢多個socket。多路復用既然可以處理多個IO钞诡,也就帶來了新的問題郑现,多個IO之間的順序變得不確定了,當然也可以針對不同的編號荧降。具體流程接箫,如下圖所示:
輸入圖片說明
IO multiplexing就是我們說的select,poll朵诫,epoll辛友,有些地方也稱這種IO方式為event driven IO。select/epoll的好處就在于單個process就可以同時處理多個網(wǎng)絡(luò)連接的IO剪返。它的基本原理就是select瞎领,poll,epoll這個function會不斷的輪詢所負責的所有socket随夸,當某個socket有數(shù)據(jù)到達了,就通知用戶進程震放。
當用戶進程調(diào)用了select宾毒,那么整個進程會被block,而同時殿遂,kernel會“監(jiān)視”所有select負責的socket诈铛,當任何一個socket中的數(shù)據(jù)準備好了乙各,select就會返回。這個時候用戶進程再調(diào)用read操作幢竹,將數(shù)據(jù)從kernel拷貝到用戶進程耳峦。
多路復用的特點是通過一種機制一個進程能同時等待IO文件描述符,內(nèi)核監(jiān)視這些文件描述符(套接字描述符)焕毫,其中的任意一個進入讀就緒狀態(tài)蹲坷,select, poll邑飒,epoll函數(shù)就可以返回循签。對于監(jiān)視的方式,又可以分為 select疙咸, poll县匠, epoll三種方式。
上面的圖和blocking IO的圖其實并沒有太大的不同撒轮,事實上乞旦,還更差一些。因為這里需要使用兩個system call (select 和 recvfrom)题山,而blocking IO只調(diào)用了一個system call (recvfrom)兰粉。但是,用select的優(yōu)勢在于它可以同時處理多個connection臀蛛。
所以亲桦,如果處理的連接數(shù)不是很高的話,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好浊仆,可能延遲還更大客峭。(select/epoll的優(yōu)勢并不是對于單個連接能處理得更快,而是在于能處理更多的連接抡柿。)
在IO multiplexing Model中舔琅,實際中,對于每一個socket洲劣,一般都設(shè)置成為non-blocking备蚓,但是,如上圖所示囱稽,整個用戶的process其實是一直被block的郊尝。只不過process是被select這個函數(shù)block,而不是被socket IO給block战惊。所以IO多路復用是阻塞在select流昏,epoll這樣的系統(tǒng)調(diào)用之上,而沒有阻塞在真正的I/O系統(tǒng)調(diào)用如recvfrom之上。
在I/O編程過程中况凉,當需要同時處理多個客戶端接入請求時谚鄙,可以利用多線程或者I/O多路復用技術(shù)進行處理。I/O多路復用技術(shù)通過把多個I/O的阻塞復用到同一個select的阻塞上刁绒,從而使得系統(tǒng)在單線程的情況下可以同時處理多個客戶端請求闷营。與傳統(tǒng)的多線程/多進程模型比,I/O多路復用的最大優(yōu)勢是系統(tǒng)開銷小知市,系統(tǒng)不需要創(chuàng)建新的額外進程或者線程傻盟,也不需要維護這些進程和線程的運行,降底了系統(tǒng)的維護工作量初狰,節(jié)省了系統(tǒng)資源莫杈,I/O多路復用的主要應(yīng)用場景如下:
服務(wù)器需要同時處理多個處于監(jiān)聽狀態(tài)或者多個連接狀態(tài)的套接字。
服務(wù)器需要同時處理多種網(wǎng)絡(luò)協(xié)議的套接字奢入。
了解了前面三種IO模式筝闹,在用戶進程進行系統(tǒng)調(diào)用的時候,他們在等待數(shù)據(jù)到來的時候腥光,處理的方式不一樣关顷,直接等待,輪詢武福,select或poll輪詢议双,兩個階段過程:
第一個階段有的阻塞,有的不阻塞捉片,有的可以阻塞又可以不阻塞平痰。
第二個階段都是阻塞的。
從整個IO過程來看伍纫,他們都是順序執(zhí)行的宗雇,因此可以歸為同步模型(synchronous)。都是進程主動等待且向內(nèi)核檢查狀態(tài)莹规∨馄眩【此句很重要!A际舞虱!】
高并發(fā)的程序一般使用同步非阻塞方式而非多線程 + 同步阻塞方式。要理解這一點母市,首先要扯到并發(fā)和并行的區(qū)別矾兜。比如去某部門辦事需要依次去幾個窗口,辦事大廳里的人數(shù)就是并發(fā)數(shù)患久,而窗口個數(shù)就是并行度椅寺。也就是說并發(fā)數(shù)是指同時進行的任務(wù)數(shù)(如同時服務(wù)的 HTTP 請求)舶沿,而并行數(shù)是可以同時工作的物理資源數(shù)量(如 CPU 核數(shù))。通過合理調(diào)度任務(wù)的不同階段配并,并發(fā)數(shù)可以遠遠大于并行度,這就是區(qū)區(qū)幾個 CPU 可以支持上萬個用戶并發(fā)請求的奧秘高镐。在這種高并發(fā)的情況下溉旋,為每個任務(wù)(用戶請求)創(chuàng)建一個進程或線程的開銷非常大。而同步非阻塞方式可以把多個 IO 請求丟到后臺去嫉髓,這就可以在一個進程里服務(wù)大量的并發(fā) IO 請求观腊。
注意:IO多路復用是同步阻塞模型還是異步阻塞模型,在此給大家分析下:
此處仍然不太清楚的算行,強烈建議大家在細究《聊聊同步梧油、異步、阻塞與非阻塞》中講同步與異步的根本性區(qū)別州邢,同步是需要主動等待消息通知儡陨,而異步則是被動接收消息通知,通過回調(diào)量淌、通知骗村、狀態(tài)等方式來被動獲取消息。IO多路復用在阻塞到select階段時呀枢,用戶進程是主動等待并調(diào)用select函數(shù)獲取數(shù)據(jù)就緒狀態(tài)消息胚股,并且其進程狀態(tài)為阻塞。所以裙秋,把IO多路復用歸為同步阻塞模式琅拌。
2.4 信號驅(qū)動式IO(signal-driven IO)##
信號驅(qū)動式I/O:首先我們允許Socket進行信號驅(qū)動IO,并安裝一個信號處理函數(shù),進程繼續(xù)運行并不阻塞摘刑。當數(shù)據(jù)準備好時进宝,進程會收到一個SIGIO信號,可以在信號處理函數(shù)中調(diào)用I/O操作函數(shù)處理數(shù)據(jù)泣侮。過程如下圖所示:
輸入圖片說明
2.5 異步非阻塞 IO(asynchronous IO)##
女友不想逛街即彪,又餐廳太吵了,回家好好休息一下活尊。于是我們叫外賣隶校,打個電話點餐,然后我和女友可以在家好好休息一下蛹锰,飯好了送貨員送到家里來深胳。這就是典型的異步,只需要打個電話說一下铜犬,然后可以做自己的事情舞终,飯好了就送來了轻庆。
相對于同步IO,異步IO不是順序執(zhí)行敛劝。用戶進程進行aio_read系統(tǒng)調(diào)用之后余爆,無論內(nèi)核數(shù)據(jù)是否準備好,都會直接返回給用戶進程夸盟,然后用戶態(tài)進程可以去做別的事情蛾方。等到socket數(shù)據(jù)準備好了,內(nèi)核直接復制數(shù)據(jù)給進程上陕,然后從內(nèi)核向進程發(fā)送通知桩砰。IO兩個階段,進程都是非阻塞的释簿。
Linux提供了AIO庫函數(shù)實現(xiàn)異步亚隅,但是用的很少。目前有很多開源的異步IO庫庶溶,例如libevent煮纵、libev、libuv渐尿。異步過程如下圖所示:
輸入圖片說明
用戶進程發(fā)起aio_read操作之后醉途,立刻就可以開始去做其它的事。而另一方面砖茸,從kernel的角度隘擎,當它受到一個asynchronous read之后,首先它會立刻返回凉夯,所以不會對用戶進程產(chǎn)生任何block货葬。然后,kernel會等待數(shù)據(jù)準備完成劲够,然后將數(shù)據(jù)拷貝到用戶內(nèi)存震桶,當這一切都完成之后,kernel會給用戶進程發(fā)送一個signal或執(zhí)行一個基于線程的回調(diào)函數(shù)來完成這次 IO 處理過程征绎,告訴它read操作完成了蹲姐。
在 Linux 中,通知的方式是 “信號”:
如果這個進程正在用戶態(tài)忙著做別的事(例如在計算兩個矩陣的乘積)人柿,那就強行打斷之柴墩,調(diào)用事先注冊的信號處理函數(shù),這個函數(shù)可以決定何時以及如何處理這個異步任務(wù)凫岖。由于信號處理函數(shù)是突然闖進來的江咳,因此跟中斷處理程序一樣,有很多事情是不能做的哥放,因此保險起見歼指,一般是把事件 “登記” 一下放進隊列爹土,然后返回該進程原來在做的事。
如果這個進程正在內(nèi)核態(tài)忙著做別的事踩身,例如以同步阻塞方式讀寫磁盤胀茵,那就只好把這個通知掛起來了,等到內(nèi)核態(tài)的事情忙完了挟阻,快要回到用戶態(tài)的時候宰掉,再觸發(fā)信號通知。
如果這個進程現(xiàn)在被掛起了赁濒,例如無事可做 sleep 了,那就把這個進程喚醒孟害,下次有 CPU 空閑的時候拒炎,就會調(diào)度到這個進程,觸發(fā)信號通知挨务。
異步 API 說來輕巧击你,做來難,這主要是對 API 的實現(xiàn)者而言的谎柄。Linux 的異步 IO(AIO)支持是 2.6.22 才引入的丁侄,還有很多系統(tǒng)調(diào)用不支持異步 IO。Linux 的異步 IO 最初是為數(shù)據(jù)庫設(shè)計的朝巫,因此通過異步 IO 的讀寫操作不會被緩存或緩沖鸿摇,這就無法利用操作系統(tǒng)的緩存與緩沖機制。
很多人把 Linux 的 O_NONBLOCK 認為是異步方式劈猿,但事實上這是前面講的同步非阻塞方式拙吉。需要指出的是,雖然 Linux 上的 IO API 略顯粗糙揪荣,但每種編程框架都有封裝好的異步 IO 實現(xiàn)筷黔。操作系統(tǒng)少做事,把更多的自由留給用戶仗颈,正是 UNIX 的設(shè)計哲學佛舱,也是 Linux 上編程框架百花齊放的一個原因。
從前面 IO 模型的分類中挨决,我們可以看出 AIO 的動機:
同步阻塞模型需要在 IO 操作開始時阻塞應(yīng)用程序请祖。這意味著不可能同時重疊進行處理和 IO 操作。
同步非阻塞模型允許處理和 IO 操作重疊進行凰棉,但是這需要應(yīng)用程序根據(jù)重現(xiàn)的規(guī)則來檢查 IO 操作的狀態(tài)损拢。
這樣就剩下異步非阻塞 IO 了,它允許處理和 IO 操作重疊進行撒犀,包括 IO 操作完成的通知福压。
IO多路復用除了需要阻塞之外掏秩,select 函數(shù)所提供的功能(異步阻塞 IO)與 AIO 類似。不過荆姆,它是對通知事件進行阻塞蒙幻,而不是對 IO 調(diào)用進行阻塞。
有時我們的 API 只提供異步通知方式胆筒,例如在 node.js 里邮破,但業(yè)務(wù)邏輯需要的是做完一件事后做另一件事,例如數(shù)據(jù)庫連接初始化后才能開始接受用戶的 HTTP 請求仆救。這樣的業(yè)務(wù)邏輯就需要調(diào)用者是以阻塞方式來工作抒和。
為了在異步環(huán)境里模擬 “順序執(zhí)行” 的效果,就需要把同步代碼轉(zhuǎn)換成異步形式彤蔽,這稱為 CPS(Continuation Passing Style)變換摧莽。BYVoid 大神的continuation.js庫就是一個 CPS 變換的工具。用戶只需用比較符合人類常理的同步方式書寫代碼顿痪,CPS 變換器會把它轉(zhuǎn)換成層層嵌套的異步回調(diào)形式镊辕。
輸入圖片說明
輸入圖片說明
另外一種使用阻塞方式的理由是降低響應(yīng)延遲。如果采用非阻塞方式蚁袭,一個任務(wù) A 被提交到后臺征懈,就開始做另一件事 B,但 B 還沒做完揩悄,A 就完成了卖哎,這時要想讓 A 的完成事件被盡快處理(比如 A 是個緊急事務(wù)),要么丟棄做到一半的 B删性,要么保存 B 的中間狀態(tài)并切換回 A棉饶,任務(wù)的切換是需要時間的(不管是從磁盤載入到內(nèi)存,還是從內(nèi)存載入到高速緩存)镇匀,這勢必降低 A 的響應(yīng)速度照藻。因此,對實時系統(tǒng)或者延遲敏感的事務(wù)汗侵,有時采用阻塞方式比非阻塞方式更好幸缕。
3.1 blocking和non-blocking區(qū)別##
調(diào)用blocking IO會一直block住對應(yīng)的進程直到操作完成,而non-blocking IO在kernel還準備數(shù)據(jù)的情況下會立刻返回晰韵。
3.2 synchronous IO和asynchronous IO區(qū)別##
在說明synchronous IO和asynchronous IO的區(qū)別之前发乔,需要先給出兩者的定義。POSIX的定義是這樣子的:
A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
An asynchronous I/O operation does not cause the requesting process to be blocked;
兩者的區(qū)別就在于synchronous IO做”IO operation”的時候會將process阻塞雪猪。按照這個定義栏尚,之前所述的blocking IO,non-blocking IO只恨,IO multiplexing都屬于synchronous IO译仗。
有人會說抬虽,non-blocking IO并沒有被block啊。這里有個非匙菥“狡猾”的地方阐污,定義中所指的”IO operation”是指真實的IO操作,就是例子中的recvfrom這個system call咱圆。non-blocking IO在執(zhí)行recvfrom這個system call的時候笛辟,如果kernel的數(shù)據(jù)沒有準備好,這時候不會block進程序苏。但是手幢,當kernel中數(shù)據(jù)準備好的時候,recvfrom會將數(shù)據(jù)從kernel拷貝到用戶內(nèi)存中忱详,這個時候進程是被block了弯菊,在這段時間內(nèi),進程是被block的踱阿。
而asynchronous IO則不一樣,當進程發(fā)起IO 操作之后钦铁,就直接返回再也不理睬了软舌,直到kernel發(fā)送一個信號,告訴進程說IO完成牛曹。在這整個過程中佛点,進程完全沒有被block。
各個IO Model的比較如圖所示:
輸入圖片說明
通過上面的圖片黎比,可以發(fā)現(xiàn)non-blocking IO和asynchronous IO的區(qū)別還是很明顯的超营。在non-blocking IO中,雖然進程大部分時間都不會被block阅虫,但是它仍然要求進程去主動的check演闭,并且當數(shù)據(jù)準備完成以后,也需要進程主動的再次調(diào)用recvfrom來將數(shù)據(jù)拷貝到用戶內(nèi)存颓帝。而asynchronous IO則完全不同米碰。它就像是用戶進程將整個IO操作交給了他人(kernel)完成,然后他人做完后發(fā)信號通知购城。在此期間吕座,用戶進程不需要去檢查IO操作的狀態(tài),也不需要主動的去拷貝數(shù)據(jù)瘪板。
簡單總結(jié)一下:
同步:主動猜是否準備好(光等或輪詢)
異步:服務(wù)員在菜準備好的時候通知你
阻塞:沒準備好之前什么事都不干
非阻塞:沒準備好之前干其他事
樓主舉的例子所說的同步跟阻塞一般是針對數(shù)據(jù)準備階段的吴趴。
所以最后的總結(jié)圖對應(yīng)的是:
阻塞I/O:同步阻塞
非阻塞I/O:同步(輪詢)非阻塞
I/O多路復用:同步阻塞(不過可以同時監(jiān)聽多個socket狀態(tài),效率高了)
信號驅(qū)動I/O:異步非阻塞
異步I/O:真正意義上的異步非阻塞(上面的都只是數(shù)據(jù)準備階段侮攀,這個是數(shù)據(jù)準備和數(shù)據(jù)處理階段)
轉(zhuǎn)自:http://www.reibang.com/p/486b0965c296