一、同步、異步、阻塞與非阻塞
1.同步與異步
1.1 同步與異步概念
同步和異步的概念與消息的通知機制有關。同步與異步主要是從消息通知機制角度來說的。
同步就是一個任務的完成需要依賴另外一個任務時,只有等待被依賴的任務完成后,依賴的任務才能算完成,這是一種可靠的任務序列。要么成功都成功,失敗都失敗,兩個任務的狀態(tài)可以保持一致。
異步是不需要等待被依賴的任務完成,只是通知被依賴的任務要完成什么工作,依賴的任務也立即執(zhí)行,只要自己完成了整個任務就算完成了。至于被依賴的任務最終是否真正完成,依賴它的任務無法確定,所以它是不可靠的任務序列。
1.2 消息通知概念
異步的概念和同步相對。當一個同步調(diào)用發(fā)出后,調(diào)用者要一直等待返回消息(結(jié)果)通知后,才能進行后續(xù)的執(zhí)行;當一個異步過程調(diào)用發(fā)出后,調(diào)用者不能立刻得到返回消息(結(jié)果)。實際處理這個調(diào)用的部件在完成后,通過狀態(tài)、通知和回調(diào)來通知調(diào)用者。
這里提到執(zhí)行部件和調(diào)用者通過三種途徑返回結(jié)果:狀態(tài)、通知和回調(diào)。使用哪一種通知機制,依賴于執(zhí)行部件的實現(xiàn),除非執(zhí)行部件提供多種選擇,否則不受調(diào)用者控制
如果執(zhí)行部件用狀態(tài)來通知,那么調(diào)用者就需要每隔一定時間檢查一次,效率就很低(有些初學多線程編程的人,總喜歡用一個循環(huán)去檢查某個變量的值,這其實是一種很嚴重的錯誤);
如果是使用通知的方式,效率則很高,因為執(zhí)行部件幾乎不需要做額外的操作。至于回調(diào)函數(shù),其實和通知沒太多區(qū)別。
1.3 舉個栗子
比如我去銀行辦理業(yè)務,可能會有兩種方式:
選擇排隊等候;
另種選擇取一個小紙條上面有我的號碼,等到排到我這一號時由柜臺的人通知我輪到我去辦理業(yè)務了;
第一種:前者(排隊等候)就是同步等待消息通知,也就是我要一直在等待銀行辦理業(yè)務情況;
第二種:后者(等待別人通知)就是異步等待消息通知。在異步消息處理中,等待消息通知者(在這個例子中就是等待辦理業(yè)務的人)往往注冊一個回調(diào)機制,在所等待的事件被觸發(fā)時由觸發(fā)機制(在這里是柜臺的人)通過某種機制(在這里是寫在小紙條上的號碼,喊號)找到等待該事件的人。
2. 阻塞和非阻塞
2.1 阻塞和非阻塞概念
阻塞和非阻塞這兩個概念與程序(線程)等待消息通知(無所謂同步或者異步)時的狀態(tài)有關。也就是說阻塞與非阻塞主要是程序(線程)等待消息通知時的狀態(tài)角度來說的。
阻塞調(diào)用是指調(diào)用結(jié)果返回之前,當前線程會被掛起,一直處于等待消息通知,不能夠執(zhí)行其他業(yè)務。函數(shù)只有在得到結(jié)果之后才會返回。
劃重點敲黑板:有人也許會把阻塞調(diào)用和同步調(diào)用等同起來,實際上它們是不同的。
對于同步調(diào)用來說,很多時候當前線程可能還是激活的,只是從邏輯上當前函數(shù)沒有返回而已,此時,這個線程可能也會處理其他的消息。
如果這個線程在等待當前函數(shù)返回時,仍在執(zhí)行其他消息處理,那這種情況就叫做同步非阻塞;
如果這個線程在等待當前函數(shù)返回時,沒有執(zhí)行其他消息處理,而是處于掛起等待狀態(tài),那這種情況就叫做同步阻塞;
所以同步的實現(xiàn)方式會有兩種:同步阻塞、同步非阻塞;同理,異步也會有兩種實現(xiàn):異步阻塞、異步非阻塞;
對于阻塞調(diào)用來說,則當前線程就會被掛起等待當前函數(shù)返回,非阻塞和阻塞的概念相對應,指在不能立刻得到結(jié)果之前,該函數(shù)不會阻塞當前線程,而會立刻返回。雖然表面上看非阻塞的方式可以明顯的提高CPU的利用率,但是也帶了另外一種后果就是系統(tǒng)的線程切換增加。增加的CPU執(zhí)行時間能不能補償系統(tǒng)的切換成本需要好好評估。
2.2 舉個栗子
還是以銀行排隊和叫號碼為例,不論是排隊還是使用號碼等待通知,如果在這個等待的過程中,等待者除了等待消息通知之外不能做其它的事情,那么該機制就是阻塞的,表現(xiàn)在程序中,也就是該程序一直阻塞在該函數(shù)調(diào)用處不能繼續(xù)往下執(zhí)行。
相反,有的人喜歡在銀行辦理這些業(yè)務的時候一邊打打電話發(fā)發(fā)短信一邊等待,這樣的狀態(tài)就是非阻塞的,因為他(等待者)沒有阻塞在這個消息通知上,而是一邊做自己的事情一邊等待。
但是需要注意了,同步非阻塞形式實際上是效率低下的,想象一下你一邊打著電話一邊還需要抬頭看到底隊伍排到你了沒有。如果把打電話和觀察排隊的位置看成是程序的兩個操作的話,這個程序需要在這兩種不同的行為之間來回的切換,效率可想而知是低下的;而異步非阻塞形式卻沒有這樣的問題,因為打電話是你(等待者)的事情,而通知你則是柜臺(消息觸發(fā)機制)的事情,程序沒有在兩種不同的操作中來回切換。
3.同步/異步 與 阻塞/非阻塞
3.1 同步阻塞形式
同步阻塞形式是效率是最低的。
拿上面的例子來說,就是你專心排隊,什么別的事都不做。
實際程序中:就是未對fd 設置O_NONBLOCK標志位的read/write 操作;
3.2 異步阻塞形式
如果在銀行等待辦理業(yè)務的人采用的是異步的方式去等待消息被觸發(fā)(通知),也就是領了一張小紙條,假如在這段時間里他不能離開銀行做其它的事情,那么很顯然,這個人被阻塞在了這個等待的操作上面;
異步操作是可以被阻塞住的,只不過它不是在處理消息時阻塞,而是在等待消息通知時被阻塞。
比如select 函數(shù),假如傳入的最后一個timeout參數(shù)為NULL,那么如果所關注的事件沒有一個被觸發(fā),程序就會一直阻塞在這個select 調(diào)用處
3.3 同步非阻塞形式
實際上是效率低下的,
想象一下你一邊打著電話一邊還需要抬頭看到底隊伍排到你了沒有,如果把打電話和觀察排隊的位置看成是程序的兩個操作的話,這個程序需要在這兩種不同的行為之間來回的切換,效率可想而知是低下的。
很多人會寫阻塞的read/write 操作,但是別忘了可以對fd設置O_NONBLOCK 標志位,這樣就可以將同步操作變成非阻塞的了。
3.4 異步非阻塞形式
效率更高,
因為打電話是你(等待者)的事情,而通知你則是柜臺(消息觸發(fā)機制)的事情,程序沒有在兩種不同的操作中來回切換。
比如說,這個人突然發(fā)覺自己煙癮犯了,需要出去抽根煙,于是他告訴大堂經(jīng)理說,排到我這個號碼的時候麻煩到外面通知我一下(注冊一個回調(diào)函數(shù)),那么他就沒有被阻塞在這個等待的操作上面,自然這個就是異步+非阻塞的方式了。
如果使用異步非阻塞的情況,比如aio_*組的操作,當發(fā)起一個aio_read操作時,函數(shù)會馬上返回不會被阻塞,當所關注的事件被觸發(fā)時會調(diào)用之前注冊的回調(diào)函數(shù)進行處理。
很多人會把同步和阻塞混淆,我想是因為很多時候同步操作會以阻塞的形式表現(xiàn)出來,比如很多人會寫阻塞的read/write操作,但是別忘了可以對fd設置O_NONBLOCK標志位,這樣就可以將同步操作變成非阻塞的了。但最根本是因為沒有區(qū)分這兩個概念,比如阻塞的read/write操作中,其實是把消息通知機制和等待消息通知的狀態(tài)結(jié)合在了一起,在這里所關注的消息就是fd是否可讀/寫,而等待消息通知的狀態(tài)則是對fd可讀/寫等待過程中程序(線程)的狀態(tài)。當我們將這個fd設置為非阻塞的時候,read/write操作就不會在等待消息通知這里阻塞,如果fd不可讀/寫則操作立即返回。
同樣的,很多人也會把異步和非阻塞混淆,因為異步操作一般都不會在真正的IO操作處被阻塞,比如如果用select函數(shù),當select返回可讀時再去read一般都不會被阻塞,而是在select函數(shù)調(diào)用處阻塞。
3.5 再舉個栗子
同步/異步關注的是消息通知的機制,而阻塞/非阻塞關注的是程序(線程)等待消息通知時的狀態(tài)
以小明下載文件為例
同步阻塞:小明一直盯著下載進度條,到 100% 的時候就完成。
同步體現(xiàn)在:等待下載完成通知;
阻塞體現(xiàn)在:等待下載完成通知過程中,不能做其他任務處理;
同步非阻塞:小明提交下載任務后就去干別的,每過一段時間就去瞄一眼進度條,看到 100% 就完成。
同步體現(xiàn)在:等待下載完成通知;
非阻塞體現(xiàn)在:等待下載完成通知過程中,去干別的任務了,只是時不時會瞄一眼進度條;【小明必須要在兩個任務間切換,關注下載進度】
異步阻塞:小明換了個有下載完成通知功能的軟件,這回不用大眼瞪小眼盯著了,下載完成就“?!币宦?。不過小明雖然不用一直盯著看,但是仍然一直等待“叮”的聲音(看起來很傻,不是嗎)。
異步體現(xiàn)在:下載完成“?!币宦曂ㄖ?;
阻塞體現(xiàn)在:等待下載完成“叮”一聲通知過程中,不能做其他任務處理;
異步非阻塞:仍然是那個會“?!币宦暤南螺d軟件,小明提交下載任務后就去干別的,聽到“叮”的一聲就知道完成了。
異步體現(xiàn)在:下載完成“?!币宦曂ㄖ?;
非阻塞體現(xiàn)在:等待下載完成“?!币宦曂ㄖ^程中,去干別的任務了,只需要接收“叮”聲通知即可;【軟件處理下載任務,小明處理其他任務,不需關注進度,只需接收軟件“?!甭曂ㄖ?,即可】
也就是說,同步/異步是“下載完成消息”通知的方式(機制),而阻塞/非阻塞則是在等待“下載完成消息”通知過程中的狀態(tài)(能不能干其他任務),在不同的場景下,同步/異步、阻塞/非阻塞的四種組合都有應用。
所以,綜上所述,同步和異步僅僅是關注的消息如何通知的機制,而阻塞與非阻塞關注的是等待消息通知時的狀態(tài)。也就是說,同步的情況下,是由處理消息者自己去等待消息是否被觸發(fā),而異步的情況下是由觸發(fā)機制來通知處理消息者,所以在異步機制中,處理消息者和觸發(fā)機制之間就需要一個連接的橋梁:
在銀行的例子中,這個橋梁就是小紙條上面的號碼。
在小明的例子中,這個橋梁就是軟件“?!钡穆曇簟?/p>
最后,請大家注意理解“消息通知機制”和“等待消息通知時的狀態(tài)”這兩個概念,這是理解四個概念的關鍵所在。
二、五種IO模型
1.一些概念
1.1 用戶空間與內(nèi)核空間
現(xiàn)在操作系統(tǒng)都是采用虛擬存儲器,那么對32位操作系統(tǒng)而言,它的尋址空間(虛擬存儲空間)為4G(2的32次方)。操作系統(tǒng)的核心是內(nèi)核,獨立于普通的應用程序,可以訪問受保護的內(nèi)存空間,也有訪問底層硬件設備的所有權(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),供各個進程使用,稱為用戶空間。
1.2 進程切換
為了控制進程的執(zhí)行,內(nèi)核必須有能力掛起正在CPU上運行的進程,并恢復以前掛起的某個進程的執(zhí)行。這種行為被稱為進程切換。因此可以說,任何進程都是在操作系統(tǒng)內(nèi)核的支持下運行的,是與內(nèi)核緊密相關的。
從一個進程的運行轉(zhuǎn)到另一個進程上運行,這個過程中經(jīng)過下面這些變化:
保存處理機上下文,包括程序計數(shù)器和其他寄存器。
更新PCB信息。
把進程的PCB移入相應的隊列,如就緒、在某事件阻塞等隊列。
選擇另一個進程執(zhí)行,并更新其PCB。
更新內(nèi)存管理的數(shù)據(jù)結(jié)構(gòu)。
恢復處理機上下文。
進程切換很耗資源
1.3 進程的阻塞
正在執(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資源的。
1.4 文件描述符fd
文件描述符(File descriptor)是計算機科學中的一個術(shù)語,是一個用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一個非負整數(shù)。實際上,它是一個索引值,指向內(nèi)核為每一個進程所維護的該進程打開文件的記錄表。當程序打開一個現(xiàn)有文件或者創(chuàng)建一個新文件時,內(nèi)核向進程返回一個文件描述符。在程序設計中,一些涉及底層的程序編寫往往會圍繞著文件描述符展開。但是文件描述符這一概念往往只適用于UNIX、Linux這樣的操作系統(tǒng)。
1.5 緩存 IO
緩存 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ū)拷貝到應用程序的地址空間。
緩存 IO 的缺點:
數(shù)據(jù)在傳輸過程中需要在應用程序地址空間和內(nèi)核進行多次數(shù)據(jù)拷貝操作,這些數(shù)據(jù)拷貝操作所帶來的 CPU 以及內(nèi)存開銷是非常大的。
2. Linux IO模型
網(wǎng)絡IO的本質(zhì)是socket的讀取,socket在linux系統(tǒng)被抽象為流,IO可以理解為對流的操作。剛才說了,對于一次IO訪問(以read舉例),數(shù)據(jù)會先被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中,然后才會從操作系統(tǒng)內(nèi)核的緩沖區(qū)拷貝到應用程序的地址空間。所以說,當一個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)絡上的數(shù)據(jù)分組到達,然后被復制到內(nèi)核的某個緩沖區(qū)。
第二步:把數(shù)據(jù)從內(nèi)核緩沖區(qū)復制到應用進程緩沖區(qū)。
網(wǎng)絡應用需要處理的無非就是兩大類問題,網(wǎng)絡IO,數(shù)據(jù)計算。相對于后者,網(wǎng)絡IO的延遲,給應用帶來的性能瓶頸大于后者。網(wǎng)絡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ōu)點。本節(jié)將簡要對其一一進行介紹。常見的IO模型有阻塞、非阻塞、IO多路復用,異步。以一個生動形象的例子來說明這四個概念。周末我和女友去逛街,中午餓了,我們準備去吃飯。周末人多,吃飯需要排隊,我和女友有以下幾種方案。
2.1 同步阻塞 IO(blocking IO)
2.1.1 場景描述
我和女友點完餐后,不知道什么時候能做好,只好坐在餐廳里面等,直到做好,然后吃完才離開。女友本想還和我一起逛街的,但是不知道飯能什么時候做好,只好和我一起在餐廳等,而不能去逛街,直到吃完飯才能去逛街,中間等待做飯的時間浪費掉了。這就是典型的阻塞。
2.1.2 網(wǎng)絡模型
同步阻塞 IO 模型是最常用的一個模型,也是最簡單的模型。在linux中,默認情況下所有的socket都是blocking。它符合人們最常見的思考邏輯。阻塞就是進程 “被” 休息, CPU處理其它進程去了。
在這個IO模型中,用戶空間的應用程序執(zhí)行一個系統(tǒng)調(diào)用(recvform),這會導致應用程序阻塞,什么也不干,直到數(shù)據(jù)準備好,并且將數(shù)據(jù)從內(nèi)核復制到用戶進程,最后進程再處理數(shù)據(jù),在等待數(shù)據(jù)到處理數(shù)據(jù)的兩個階段,整個進程都被阻塞。不能處理別的網(wǎng)絡IO。調(diào)用應用程序處于一種不再消費 CPU 而只是簡單等待響應的狀態(tài),因此從處理的角度來看,這是非常有效的。在調(diào)用recv()/recvfrom()函數(shù)時,發(fā)生在內(nèi)核中等待數(shù)據(jù)和復制數(shù)據(jù)的過程,大致如下圖:
2.1.3 流程描述
當用戶進程調(diào)用了recv()/recvfrom()這個系統(tǒng)調(diào)用,kernel就開始了IO的第一個階段:準備數(shù)據(jù)(對于網(wǎng)絡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)
2.2.1 場景描述
我女友不甘心白白在這等,又想去逛商場,又擔心飯好了。所以我們逛一會,回來詢問服務員飯好了沒有,來來回回好多次,飯都還沒吃都快累死了啦。這就是非阻塞。需要不斷的詢問,是否準備好了。
2.2.2 網(wǎng)絡模型
同步非阻塞就是 “每隔一會兒瞄一眼進度條” 的輪詢(polling)方式。在這種模型中,設備是以非阻塞的形式打開的。這意味著 IO 操作不會立即完成,read 操作可能會返回一個錯誤代碼,說明這個命令不能立即滿足(EAGAIN 或 EWOULDBLOCK)。
在網(wǎng)絡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下,可以通過設置socket使其變?yōu)閚on-blocking。當對一個non-blocking socket執(zhí)行讀操作時,流程如圖所示:
2.2.3 流程描述
當用戶進程發(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)點:能夠在等待任務完成的時間里干其他活了(包括提交其他任務,也就是 “后臺” 可以有多個任務在同時執(zhí)行)。
缺點:任務完成的響應延遲增大了,因為每過一段時間才去輪詢一次read操作,而任務可能在兩次輪詢之間的任意時間完成。這會導致整體數(shù)據(jù)吞吐量的降低。
2.3 IO 多路復用( IO multiplexing)
2.3.1 場景描述
與第二個方案差不多,餐廳安裝了電子屏幕用來顯示點餐的狀態(tài),這樣我和女友逛街一會,回來就不用去詢問服務員了,直接看電子屏幕就可以了。這樣每個人的餐是否好了,都直接看電子屏幕就可以了,這就是典型的IO多路復用。
2.3.2 網(wǎng)絡模型
由于同步非阻塞方式需要不斷主動輪詢,輪詢占據(jù)了很大一部分過程,輪詢會消耗大量的CPU時間,而 “后臺” 可能有多個任務在同時進行,人們就想到了循環(huán)查詢多個任務的完成狀態(tài),只要有任何一個任務完成,就去處理它。如果輪詢不是進程的用戶態(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之間的順序變得不確定了,當然也可以針對不同的編號。具體流程,如下圖所示:
2.3.3 流程描述
IO multiplexing就是我們說的select,poll,epoll,有些地方也稱這種IO方式為event driven IO。select/epoll的好處就在于單個process就可以同時處理多個網(wǎng)絡連接的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,一般都設置成為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多路復用的主要應用場景如下:
服務器需要同時處理多個處于監(jiān)聽狀態(tài)或者多個連接狀態(tài)的套接字。
服務器需要同時處理多種網(wǎng)絡協(xié)議的套接字。
了解了前面三種IO模式,在用戶進程進行系統(tǒng)調(diào)用的時候,他們在等待數(shù)據(jù)到來的時候,處理的方式不一樣,直接等待,輪詢,select或poll輪詢,兩個階段過程:
第一個階段有的阻塞,有的不阻塞,有的可以阻塞又可以不阻塞。
第二個階段都是阻塞的。
從整個IO過程來看,他們都是順序執(zhí)行的,因此可以歸為同步模型(synchronous)。都是進程主動等待且向內(nèi)核檢查狀態(tài)?!敬司浜苤匾。。 ?/p>
高并發(fā)的程序一般使用同步非阻塞方式而非多線程 + 同步阻塞方式。要理解這一點,首先要扯到并發(fā)和并行的區(qū)別。比如去某部門辦事需要依次去幾個窗口,辦事大廳里的人數(shù)就是并發(fā)數(shù),而窗口個數(shù)就是并行度。也就是說并發(fā)數(shù)是指同時進行的任務數(shù)(如同時服務的 HTTP 請求),而并行數(shù)是可以同時工作的物理資源數(shù)量(如 CPU 核數(shù))。通過合理調(diào)度任務的不同階段,并發(fā)數(shù)可以遠遠大于并行度,這就是區(qū)區(qū)幾個 CPU 可以支持上萬個用戶并發(fā)請求的奧秘。在這種高并發(fā)的情況下,為每個任務(用戶請求)創(chuàng)建一個進程或線程的開銷非常大。而同步非阻塞方式可以把多個 IO 請求丟到后臺去,這就可以在一個進程里服務大量的并發(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)
2.5.1 場景描述
女友不想逛街,又餐廳太吵了,回家好好休息一下。于是我們叫外賣,打個電話點餐,然后我和女友可以在家好好休息一下,飯好了送貨員送到家里來。這就是典型的異步,只需要打個電話說一下,然后可以做自己的事情,飯好了就送來了。
2.5.2 網(wǎng)絡模型
相對于同步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。異步過程如下圖所示:
2.5.3 流程描述
用戶進程發(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ù)可以決定何時以及如何處理這個異步任務。由于信號處理函數(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ù)庫設計的,因此通過異步 IO 的讀寫操作不會被緩存或緩沖,這就無法利用操作系統(tǒng)的緩存與緩沖機制。
很多人把 Linux 的 O_NONBLOCK 認為是異步方式,但事實上這是前面講的同步非阻塞方式。需要指出的是,雖然 Linux 上的 IO API 略顯粗糙,但每種編程框架都有封裝好的異步 IO 實現(xiàn)。操作系統(tǒng)少做事,把更多的自由留給用戶,正是 UNIX 的設計哲學,也是 Linux 上編程框架百花齊放的一個原因。
從前面 IO 模型的分類中,我們可以看出 AIO 的動機:
同步阻塞模型需要在 IO 操作開始時阻塞應用程序。這意味著不可能同時重疊進行處理和 IO 操作。
同步非阻塞模型允許處理和 IO 操作重疊進行,但是這需要應用程序根據(jù)重現(xiàn)的規(guī)則來檢查 IO 操作的狀態(tài)。
這樣就剩下異步非阻塞 IO 了,它允許處理和 IO 操作重疊進行,包括 IO 操作完成的通知。
IO多路復用除了需要阻塞之外,select 函數(shù)所提供的功能(異步阻塞 IO)與 AIO 類似。不過,它是對通知事件進行阻塞,而不是對 IO 調(diào)用進行阻塞。
2.6 關于異步阻塞
有時我們的 API 只提供異步通知方式,例如在 node.js 里,但業(yè)務邏輯需要的是做完一件事后做另一件事,例如數(shù)據(jù)庫連接初始化后才能開始接受用戶的 HTTP 請求。這樣的業(yè)務邏輯就需要調(diào)用者是以阻塞方式來工作。
為了在異步環(huán)境里模擬 “順序執(zhí)行” 的效果,就需要把同步代碼轉(zhuǎn)換成異步形式,這稱為 CPS(Continuation Passing Style)變換。
另外一種使用阻塞方式的理由是降低響應延遲。如果采用非阻塞方式,一個任務 A 被提交到后臺,就開始做另一件事 B,但 B 還沒做完,A 就完成了,這時要想讓 A 的完成事件被盡快處理(比如 A 是個緊急事務),要么丟棄做到一半的 B,要么保存 B 的中間狀態(tài)并切換回 A,任務的切換是需要時間的(不管是從磁盤載入到內(nèi)存,還是從內(nèi)存載入到高速緩存),這勢必降低 A 的響應速度。因此,對實時系統(tǒng)或者延遲敏感的事務,有時采用阻塞方式比非阻塞方式更好。
3. 五種IO模型總結(jié)
3.1 blocking和non-blocking區(qū)別
調(diào)用blocking IO會一直block住對應的進程直到操作完成,而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ù)。
三、select、poll、epoll詳解
1.select、poll、epoll
epoll跟select都能提供多路I/O復用的解決方案。在現(xiàn)在的Linux內(nèi)核里有都能夠支持,其中epoll是Linux所特有,而select則應該是POSIX所規(guī)定,一般操作系統(tǒng)均有實現(xiàn)。
1.1 select
1.1.1 基本原理
select 函數(shù)監(jiān)視的文件描述符分3類,分別是writefds、readfds、和exceptfds。調(diào)用后select函數(shù)會阻塞,直到有描述符就緒(有數(shù)據(jù) 可讀、可寫、或者有except),或者超時(timeout指定等待時間,如果立即返回設為null即可),函數(shù)返回。當select函數(shù)返回后,可以通過遍歷fdset,來找到就緒的描述符。
select目前幾乎在所有的平臺上支持,其良好跨平臺支持也是它的一個優(yōu)點。select的一個缺點在于單個進程能夠監(jiān)視的文件描述符的數(shù)量存在最大限制,在Linux上一般為1024,可以通過修改宏定義甚至重新編譯內(nèi)核的方式提升這一限制,但是這樣也會造成效率的降低。
本質(zhì)上是通過設置或者檢查存放fd標志位的數(shù)據(jù)結(jié)構(gòu)來進行下一步處理。這樣所帶來的缺點是:
select最大的缺陷就是單個進程所打開的FD是有一定限制的,它由FD_SETSIZE設置,默認值是1024。一般來說這個數(shù)目和系統(tǒng)內(nèi)存關系很大,具體數(shù)目可以cat /proc/sys/fs/file-max察看。32位機默認是1024個。64位機默認是2048.
對socket進行掃描時是線性掃描,即采用輪詢的方法,效率較低。
當套接字比較多的時候,每次select()都要通過遍歷FD_SETSIZE個Socket來完成調(diào)度,不管哪個Socket是活躍的,都遍歷一遍。這會浪費很多CPU時間。如果能給套接字注冊某個回調(diào)函數(shù),當他們活躍時,自動完成相關操作,那就避免了輪詢,這正是epoll與kqueue做的。
需要維護一個用來存放大量fd的數(shù)據(jù)結(jié)構(gòu),這樣會使得用戶空間和內(nèi)核空間在傳遞該結(jié)構(gòu)時復制開銷大。
1.2 poll
1.2.1 基本原理
poll本質(zhì)上和select沒有區(qū)別,它將用戶傳入的數(shù)組拷貝到內(nèi)核空間,然后查詢每個fd對應的設備狀態(tài),如果設備就緒則在設備等待隊列中加入一項并繼續(xù)遍歷,如果遍歷完所有fd后沒有發(fā)現(xiàn)就緒設備,則掛起當前進程,直到設備就緒或者主動超時,被喚醒后它又要再次遍歷fd。這個過程經(jīng)歷了多次無謂的遍歷。
它沒有最大連接數(shù)的限制,原因是它是基于鏈表來存儲的,但是同樣有一個缺點:
大量的fd的數(shù)組被整體復制于用戶態(tài)和內(nèi)核地址空間之間,而不管這樣的復制是不是有意義。
poll還有一個特點是“水平觸發(fā)”,如果報告了fd后,沒有被處理,那么下次poll時會再次報告該fd。
注意:
從上面看,select和poll都需要在返回后,通過遍歷文件描述符來獲取已經(jīng)就緒的socket。事實上,同時連接的大量客戶端在一時刻可能只有很少的處于就緒狀態(tài),因此隨著監(jiān)視的描述符數(shù)量的增長,其效率也會線性下降。
1.3 epoll
epoll是在2.6內(nèi)核中提出的,是之前的select和poll的增強版本。相對于select和poll來說,epoll更加靈活,沒有描述符限制。epoll使用一個文件描述符管理多個描述符,將用戶關系的文件描述符的事件存放到內(nèi)核的一個事件表中,這樣在用戶空間和內(nèi)核空間的copy只需一次。
1.3.1 基本原理
epoll支持水平觸發(fā)和邊緣觸發(fā),最大的特點在于邊緣觸發(fā),它只告訴進程哪些fd剛剛變?yōu)榫途w態(tài),并且只會通知一次。還有一個特點是,epoll使用“事件”的就緒通知方式,通過epoll_ctl注冊fd,一旦該fd就緒,內(nèi)核就會采用類似callback的回調(diào)機制來激活該fd,epoll_wait便可以收到通知。
1.3.2 epoll的優(yōu)點
沒有最大并發(fā)連接的限制,能打開的FD的上限遠大于1024(1G的內(nèi)存上能監(jiān)聽約10萬個端口)。
效率提升,不是輪詢的方式,不會隨著FD數(shù)目的增加效率下降。只有活躍可用的FD才會調(diào)用callback函數(shù);即Epoll最大的優(yōu)點就在于它只管你“活躍”的連接,而跟連接總數(shù)無關,因此在實際的網(wǎng)絡環(huán)境中,Epoll的效率就會遠遠高于select和poll。
內(nèi)存拷貝,利用mmap()文件映射內(nèi)存加速與內(nèi)核空間的消息傳遞;即epoll使用mmap減少復制開銷。
1.3.3 LT和 ET
epoll對文件描述符的操作有兩種模式:LT(level trigger)和ET(edge trigger)。LT模式是默認模式,LT模式與ET模式的區(qū)別如下
LT模式:當epoll_wait檢測到描述符事件發(fā)生并將此事件通知應用程序,應用程序可以不立即處理該事件。下次調(diào)用epoll_wait時,會再次響應應用程序并通知此事件。
ET模式:當epoll_wait檢測到描述符事件發(fā)生并將此事件通知應用程序,應用程序必須立即處理該事件。如果不處理,下次調(diào)用epoll_wait時,不會再次響應應用程序并通知此事件。
1.3.4 LT模式
LT(level triggered)是缺省的工作方式,并且同時支持block和no-block socket。在這種做法中,內(nèi)核告訴你一個文件描述符是否就緒了,然后你可以對這個就緒的fd進行IO操作。如果你不作任何操作,內(nèi)核還是會繼續(xù)通知你的。
1.3.5 ET模式
ET(edge-triggered)是高速工作方式,只支持no-block socket。在這種模式下,當描述符從未就緒變?yōu)榫途w時,內(nèi)核通過epoll告訴你。然后它會假設你知道文件描述符已經(jīng)就緒,并且不會再為那個文件描述符發(fā)送更多的就緒通知,直到你做了某些操作導致那個文件描述符不再為就緒狀態(tài)了(比如,你在發(fā)送,接收或者接收請求,或者發(fā)送接收的數(shù)據(jù)少于一定量時導致了一個EWOULDBLOCK 錯誤)。但是請注意,如果一直不對這個fd作IO操作(從而導致它再次變成未就緒),內(nèi)核不會發(fā)送更多的通知(only once)。
ET模式在很大程度上減少了epoll事件被重復觸發(fā)的次數(shù),因此效率要比LT模式高。epoll工作在ET模式的時候,必須使用非阻塞套接口,以避免由于一個文件句柄的阻塞讀/阻塞寫操作把處理多個文件描述符的任務餓死。
1.3.6 總結(jié)
在select/poll中,進程只有在調(diào)用一定的方法后,內(nèi)核才對所有監(jiān)視的文件描述符進行掃描,
epoll事先通過epoll_ctl()來注冊一個文件描述符,一旦基于某個文件描述符就緒時,內(nèi)核會采用類似callback的回調(diào)機制,迅速激活這個文件描述符,當進程調(diào)用epoll_wait()時便得到通知。(此處去掉了遍歷文件描述符,而是通過監(jiān)聽回調(diào)的的機制。這正是epoll的魅力所在。)
如果沒有大量的idle-connection或者dead-connection,epoll的效率并不會比select/poll高很多,但是當遇到大量的idle-connection,就會發(fā)現(xiàn)epoll的效率大大高于select/poll。
2. select、poll、epoll區(qū)別
2.1 支持一個進程所能打開的最大連接數(shù)
2.2 FD劇增后帶來的IO效率問題
2.3 消息傳遞方式
2.4 總結(jié)
在選擇select,poll,epoll時要根據(jù)具體的使用場合以及這三種方式的自身特點:
表面上看epoll的性能最好,但是在連接數(shù)少并且連接都十分活躍的情況下,select和poll的性能可能比epoll好,畢竟epoll的通知機制需要很多函數(shù)回調(diào)。
select低效是因為每次它都需要輪詢。但低效也是相對的,視情況而定,也可通過良好的設計改善。