在UNIX的世界中一切皆文件,文件本質(zhì)上是一串二進(jìn)制流。在數(shù)據(jù)交換過(guò)程中,需要對(duì)流進(jìn)行數(shù)據(jù)的收發(fā)操作也就是I/O輸入輸出操作(Input/Output)。
由于程序和運(yùn)行時(shí)數(shù)據(jù)在內(nèi)存中駐留,由CPU的計(jì)算核心來(lái)執(zhí)行,涉及到數(shù)據(jù)交換在磁盤、網(wǎng)絡(luò)時(shí)也需要IO。
文件描述符
對(duì)于不同的流如何才能辨別標(biāo)識(shí)呢?做到這個(gè)的就是文件描述符fd,文件描述符是一個(gè)整數(shù),對(duì)這個(gè)整數(shù)的操作就是對(duì)流的操作。
文件描述符fd(File Descriptor)是一個(gè)用于表述指向文件的引用的抽象化概念,文件描述符在形式上是一個(gè)非負(fù)整數(shù)。實(shí)際上它是一個(gè)索引值,指向內(nèi)核Kernel為每個(gè)進(jìn)程所維護(hù)的該進(jìn)程打開(kāi)文件的記錄表。當(dāng)程序打開(kāi)一個(gè)現(xiàn)有文件或創(chuàng)建一個(gè)新文件時(shí),內(nèi)核會(huì)向進(jìn)程返回一個(gè)文件描述符。
在程序設(shè)計(jì)中,涉及底層的程序編寫往往會(huì)圍繞著文件描述符展開(kāi),但是文件描述符的概念僅適用于Linux這樣類UNIX操作系統(tǒng)中。
虛擬內(nèi)存
現(xiàn)代操作系統(tǒng)均采用虛擬存儲(chǔ)器,32位操作系統(tǒng)的尋址空間(虛擬存儲(chǔ)空間)為4GB(2的32次方)。
操作系統(tǒng)的核心是內(nèi)核kernel,是獨(dú)立于普通的應(yīng)用程序,可以訪問(wèn)受保護(hù)的內(nèi)存空間,也有訪問(wèn)底層硬件設(shè)備的所有權(quán)限。為了保證用戶進(jìn)程不能直接操作內(nèi)核,保證內(nèi)核安全,操作系統(tǒng)將虛擬空間劃分為兩個(gè)部分:內(nèi)核空間、用戶空間。

在Linux操作系統(tǒng)中,會(huì)將最高的1G字節(jié)(從虛擬地址0xC0000000到0xFFFFFFFF)提供給內(nèi)核kernel使用,稱之為內(nèi)核空間。將較低的3G字節(jié)(從虛擬地址0x00000000到0xBFFFFFFF)提供給進(jìn)程使用,稱之為用戶空間。
內(nèi)核空間中存放的是內(nèi)核代碼和數(shù)據(jù),進(jìn)程的用戶空間中存放的是用戶程序的代碼和數(shù)據(jù)。不管是內(nèi)核空間還是用戶空間,它們都處于虛擬空間中。
Linux提供了兩級(jí)保護(hù)機(jī)制:0級(jí)供內(nèi)核使用、3級(jí)供用戶程序使用

操作系統(tǒng)和驅(qū)動(dòng)程序運(yùn)行在內(nèi)核空間,應(yīng)用程序運(yùn)行在用戶空間,兩者不能簡(jiǎn)單地使用指針傳遞數(shù)據(jù)。因?yàn)長(zhǎng)inux使用的虛擬內(nèi)存機(jī)制,必須通過(guò)系統(tǒng)調(diào)用請(qǐng)求內(nèi)核來(lái)協(xié)助完成IO動(dòng)作。
內(nèi)核會(huì)為每個(gè)IO設(shè)備維護(hù)一個(gè)緩沖區(qū),用戶空間的數(shù)據(jù)可能被換出,當(dāng)內(nèi)核空間使用用戶空間指針時(shí)對(duì)應(yīng)的數(shù)據(jù)可能不再內(nèi)存中。
對(duì)于一個(gè)輸入操作來(lái)說(shuō),進(jìn)程IO系統(tǒng)調(diào)用后內(nèi)核會(huì)先去看緩沖區(qū)中有沒(méi)有相應(yīng)的緩存數(shù)據(jù),若有數(shù)據(jù)則會(huì)直接復(fù)制到進(jìn)程空間中,若沒(méi)有的話會(huì)到設(shè)備中讀取,因?yàn)樵O(shè)備IO一般速度較慢需要等待。
Linux系統(tǒng)中的每次IO都需要經(jīng)過(guò)兩個(gè)階段
- 內(nèi)核準(zhǔn)備數(shù)據(jù)
將數(shù)據(jù)從磁盤文件中加載到內(nèi)核內(nèi)存空間(內(nèi)核緩沖區(qū)),等待數(shù)據(jù)準(zhǔn)備完畢,耗時(shí)較長(zhǎng)。 - 將數(shù)據(jù)從內(nèi)核拷貝到用戶空間
將數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到用戶空間進(jìn)程的內(nèi)存中,耗時(shí)較短。

IO包括內(nèi)存IO、網(wǎng)絡(luò)IO、磁盤IO三種,常說(shuō)的IO是指后兩者。以文件IO為例,一個(gè)IO讀過(guò)程是文件數(shù)據(jù)從“磁盤-內(nèi)核緩存區(qū)-用戶內(nèi)存”的過(guò)程。
緩存IO
緩存IO又稱為標(biāo)準(zhǔn)IO,大多數(shù)文件系統(tǒng)默認(rèn)的IO操作都是緩存IO,在Linux的緩存IO機(jī)制中,操作系統(tǒng)會(huì)將IO的數(shù)據(jù)緩存在文件系統(tǒng)的頁(yè)緩存page cache。數(shù)據(jù)會(huì)先被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中,然后才會(huì)從操作系統(tǒng)內(nèi)核的緩存區(qū)拷貝到應(yīng)用程序的地址空間中。這種做法的缺點(diǎn)是需要在應(yīng)用程序地址空間和內(nèi)核進(jìn)行多次拷貝,這些拷貝動(dòng)作所帶來(lái)的CPU以及內(nèi)存開(kāi)銷是非常大的。
進(jìn)程切換
為了控制進(jìn)程的執(zhí)行,內(nèi)核必須有能力掛起正在CPU上運(yùn)行的進(jìn)程,并恢復(fù)以前掛起的某個(gè)進(jìn)程的執(zhí)行。這種行為稱為進(jìn)程的切換。任何進(jìn)程都是在操作系統(tǒng)內(nèi)核的支持下運(yùn)行的,是與內(nèi)核緊密相關(guān)的。
進(jìn)程切換的過(guò)程會(huì)經(jīng)過(guò)以下變化
- 保存處理機(jī)上下文,包括程序計(jì)數(shù)器和其它寄存器。
- 更新PCB信息
- 將進(jìn)程的PCB移入相應(yīng)的隊(duì)列,如就緒、在某事件阻塞等隊(duì)列。
- 選擇另外一個(gè)進(jìn)程執(zhí)行并更新PCB
- 更新內(nèi)存管理的數(shù)據(jù)結(jié)構(gòu)
- 恢復(fù)處理機(jī)上下文
進(jìn)程阻塞
正在執(zhí)行的進(jìn)程由于期待的某些事件未發(fā)生,比如請(qǐng)求系統(tǒng)資源失敗、等待操作的完成、新數(shù)據(jù)尚未到達(dá)、無(wú)新工作做等時(shí)會(huì)由操作系統(tǒng)自動(dòng)執(zhí)行阻塞原語(yǔ)Block,使自己由運(yùn)行狀態(tài)變?yōu)樽枞麪顟B(tài)。由此可見(jiàn),進(jìn)程的阻塞是進(jìn)程自身的一種主動(dòng)行為,也因此只有處于運(yùn)行態(tài)的進(jìn)程(獲得CPU)才可以轉(zhuǎn)變?yōu)樽枞麪顟B(tài)。當(dāng)進(jìn)程進(jìn)入阻塞狀態(tài)后是不會(huì)占用CPU資源的。通俗來(lái)說(shuō),就是要等別人做完后你才能繼續(xù)工作。
同步與異步
由于CPU和內(nèi)存的速度遠(yuǎn)高于外設(shè),所以IO編程中存在嚴(yán)重不匹配的問(wèn)題。比如將100M數(shù)據(jù)寫入磁盤,CPU輸出100M數(shù)據(jù)只需0.01秒,磁盤接收卻需要10秒,怎么辦?有兩種辦法可以解決這個(gè)問(wèn)題:
- 讓CPU等著,也就是程序暫停執(zhí)行后續(xù)代碼,等寫入完成后再接著后續(xù)執(zhí)行,這種模式稱為同步IO。
- CPU不等待只是告訴磁盤:“你慢慢寫不著急,我先干別的事兒去了!”,于是后續(xù)代碼立即執(zhí)行,這種模式稱為異步IO。
- 同步和異步關(guān)注的是消息通信機(jī)制
- 同步與異步描述的是用戶線程與內(nèi)核的交互方式
- 同步
synchronous是指用戶線程發(fā)起IO請(qǐng)求后需要等待或輪詢內(nèi)核IO操作完成后才能繼續(xù)執(zhí)行。 - 異步
asynchronous是指用戶線程發(fā)起IO請(qǐng)求后仍然繼續(xù)執(zhí)行,當(dāng)內(nèi)核IO操作完成后會(huì)通知用戶線程或調(diào)用用戶線程注冊(cè)的回調(diào)函數(shù)。
同步與異步的區(qū)別
- 數(shù)據(jù)從“內(nèi)核緩存區(qū)-用戶內(nèi)存”這個(gè)過(guò)程是否需要用戶進(jìn)程等待,實(shí)際IO讀寫是否阻塞請(qǐng)求進(jìn)程。
- 是否等待IO執(zhí)行的結(jié)果,使用異步IO來(lái)編寫程序性能會(huì)遠(yuǎn)遠(yuǎn)高于同步IO,但異步IO的缺點(diǎn)是編程模型復(fù)雜。
阻塞與非阻塞
- 阻塞與非阻塞關(guān)注的是調(diào)用者在等待結(jié)果返回之前所處的狀態(tài)
- 阻塞與非阻塞描述的是用戶線程調(diào)用內(nèi)核IO操作的方式
- 阻塞
blocking是指IO操作需要徹底完成后才返回到用戶空間,調(diào)用結(jié)果返回之前調(diào)用者被掛起。 - 非阻塞
noblocking是指IO操作被調(diào)用后立即返回給用戶一個(gè)狀態(tài)值,無(wú)需等到IO操作徹底完成。 - 阻塞與非阻塞是函數(shù)或方法的實(shí)現(xiàn)方式,在數(shù)據(jù)就緒之前是立即返回還是等待,發(fā)起IO請(qǐng)求是否會(huì)被阻塞。
網(wǎng)絡(luò)IO
網(wǎng)絡(luò)應(yīng)用需要處理的兩大類問(wèn)題是網(wǎng)絡(luò)IO和數(shù)據(jù)計(jì)算。相對(duì)于數(shù)據(jù)計(jì)算,網(wǎng)絡(luò)IO的延遲會(huì)給應(yīng)用帶來(lái)性能上的瓶頸大于后者。
網(wǎng)路IO的本質(zhì)是socket的讀取操作,socket在Linux操作系統(tǒng)中被抽象為流stream。IO可以理解為對(duì)流的操作。IO編程中Stream流是一個(gè)重要的概念,可以把流想象成水管中的水,只能單向流向。Input Stream是數(shù)據(jù)從外部(如磁盤、網(wǎng)絡(luò)等)流進(jìn)內(nèi)存,Output Steam是數(shù)據(jù)從內(nèi)存流到外設(shè)。

對(duì)于一次磁盤IO訪問(wèn),比如以read為例,數(shù)據(jù)會(huì)先被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū),然后才會(huì)從系統(tǒng)內(nèi)核緩沖區(qū)拷貝到應(yīng)用程序的地址空間中。所以說(shuō),當(dāng)一個(gè)read讀操作發(fā)生時(shí),它會(huì)經(jīng)歷兩個(gè)階段:等待數(shù)據(jù)準(zhǔn)備、將數(shù)據(jù)從內(nèi)核拷貝到進(jìn)程中
網(wǎng)絡(luò)IO除了轉(zhuǎn)入內(nèi)核調(diào)用外,與傳統(tǒng)的磁盤IO不同的是,網(wǎng)絡(luò)IO的讀寫對(duì)于socket流而言大致可分為兩個(gè)階段:
- 等待:等待網(wǎng)絡(luò)上的數(shù)據(jù)分組到達(dá),然后復(fù)制到內(nèi)核的某個(gè)緩沖區(qū)。
- 復(fù)制:將數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到應(yīng)用進(jìn)程緩沖區(qū)
如果內(nèi)核空間緩沖區(qū)中已經(jīng)有數(shù)據(jù)了就可以省略掉第一步,為什么不能直接讓磁盤控制器將數(shù)據(jù)送到應(yīng)用程序的地址空間中呢?因?yàn)閼?yīng)用程序不能直接操作底層硬件。
相比于傳統(tǒng)的網(wǎng)絡(luò)IO,普通文件描述符的操作可分為兩步,以read讀操作為例,利用read()函數(shù)從socket中同步阻塞的讀取數(shù)據(jù)。

需要注意的是不要使用操作磁盤文件IO的經(jīng)驗(yàn)去看待網(wǎng)絡(luò)IO,為什么呢?
因?yàn)閷?shí)際上在磁盤IO中等待階段是不存在的,因?yàn)榇疟P文件并不像網(wǎng)絡(luò)IO那樣,需要等待遠(yuǎn)程傳輸數(shù)據(jù)。所以,習(xí)慣操作磁盤IO的開(kāi)發(fā)者開(kāi)始無(wú)法理解同步阻塞IO的工作過(guò)程,也無(wú)法理解為什么read()方法不會(huì)返回。
客戶端發(fā)起一個(gè)HTTP請(qǐng)求,服務(wù)器處理響應(yīng)HTTP請(qǐng)求,此過(guò)程再服務(wù)器以網(wǎng)絡(luò)IO的角度看經(jīng)歷了哪些階段?

服務(wù)器構(gòu)建網(wǎng)絡(luò)數(shù)據(jù)包觸發(fā)IO的過(guò)程
- 用戶空間進(jìn)程通過(guò)
recvfrom函數(shù)接收等待接收數(shù)據(jù)包,并將接收到的數(shù)據(jù)包在內(nèi)核中通過(guò)四表五鏈檢查網(wǎng)絡(luò)狀態(tài),若通過(guò)網(wǎng)絡(luò)檢查則提交給用戶空間的HTTP進(jìn)程。 - HTTP進(jìn)程解析請(qǐng)求并發(fā)起系統(tǒng)調(diào)用
read函數(shù),到達(dá)內(nèi)核空間。 - 內(nèi)核空間執(zhí)行
read函數(shù)讀取磁盤內(nèi)容并將此內(nèi)容加載到內(nèi)存 - 內(nèi)核空間提交給用戶空間HTTP進(jìn)程并告知數(shù)據(jù)已經(jīng)
read完畢 - 用戶空間HTTP進(jìn)程根據(jù)請(qǐng)求報(bào)文進(jìn)行構(gòu)建響應(yīng)報(bào)文
- 構(gòu)建完HTTP響應(yīng)報(bào)文后通知內(nèi)核空間構(gòu)建網(wǎng)絡(luò)封裝
- 內(nèi)核空間再次通過(guò)四表五鏈網(wǎng)絡(luò)狀態(tài),通過(guò)網(wǎng)卡發(fā)送構(gòu)建號(hào)的HTTP響應(yīng)報(bào)文。
單純根據(jù)流程可得到以下信息
- 單進(jìn)程接收響應(yīng)數(shù)據(jù)報(bào)文調(diào)用
recvform函數(shù)時(shí),與此同時(shí)不執(zhí)行其它函數(shù)調(diào)用,此時(shí)嚴(yán)重影響效率。 - 當(dāng)內(nèi)核空間執(zhí)行
read函數(shù)后提交給用戶空間進(jìn)行HTTP封裝,最后調(diào)用內(nèi)核空間進(jìn)行網(wǎng)絡(luò)封裝構(gòu)建并發(fā)送報(bào)文。
根據(jù)上述流程可發(fā)現(xiàn)一次網(wǎng)絡(luò)IO在邏輯上實(shí)際上是處于兩種狀態(tài)的,在這兩種狀態(tài)上進(jìn)行優(yōu)化IO才可以優(yōu)化整體的性能。這兩種形態(tài)在Linux網(wǎng)絡(luò)編程中定義如下:

- 等待數(shù)據(jù)準(zhǔn)備
從邏輯上看是內(nèi)核網(wǎng)絡(luò)驅(qū)動(dòng)等待接收網(wǎng)絡(luò)數(shù)據(jù)包,表現(xiàn)在內(nèi)核形態(tài)上,同時(shí)也是數(shù)據(jù)流可得信息的第一步。在用戶空間封裝完畢后如何通知內(nèi)核再次進(jìn)行網(wǎng)絡(luò)報(bào)文的構(gòu)建,在此狀態(tài)下誕生了兩種狀態(tài)同步synchronous和異步asynchronous,同步是為進(jìn)程自己主動(dòng)等待函數(shù)執(zhí)行成功并返回消息后才能繼續(xù)執(zhí)行其它函數(shù),異步為函數(shù)執(zhí)行完畢后主動(dòng)通知進(jìn)程執(zhí)行其它流程,最后內(nèi)核空間對(duì)網(wǎng)絡(luò)IO進(jìn)行解封裝。
- 將數(shù)據(jù)從內(nèi)核拷貝到進(jìn)程中
邏輯意義上是內(nèi)核空間調(diào)用并執(zhí)行系統(tǒng)調(diào)用函數(shù)后,將執(zhí)行的結(jié)果反饋給用戶空間,讓用戶空間進(jìn)行構(gòu)建響應(yīng)報(bào)文。用戶空間的進(jìn)程能夠執(zhí)行其它函數(shù),從而提升整體性能。在此狀態(tài)下誕生了兩種狀態(tài)阻塞blocking和非阻塞nonblocking,阻塞狀態(tài)是指IO操作需要徹底完成后才能返回到用戶空間,調(diào)用結(jié)果返回之前調(diào)用者被掛起,即進(jìn)程調(diào)用函數(shù)后被掛起直到函數(shù)返回結(jié)果后才能執(zhí)行其它操作,非阻塞狀態(tài)是指IO操作被調(diào)用后立即返回給用戶一個(gè)狀態(tài)值,無(wú)需等到IO操作徹底完成,最終的調(diào)用結(jié)果返回之前調(diào)用者不會(huì)被掛起,即進(jìn)程在執(zhí)行函數(shù)后無(wú)需等待執(zhí)行結(jié)果仍可繼續(xù)執(zhí)行其它函數(shù)。
IO模型
為什么會(huì)出現(xiàn)IO模型呢?
IO操作根據(jù)設(shè)備類型一般分為內(nèi)存IO、網(wǎng)絡(luò)IO、磁盤IO,其中內(nèi)存IO的速度是最快的,計(jì)算機(jī)的性能瓶頸一般不在內(nèi)存IO上。盡管網(wǎng)絡(luò)IO可通過(guò)購(gòu)買獨(dú)享帶寬和高速網(wǎng)卡來(lái)提升速度,磁盤IO可使用RAID磁盤整列來(lái)提升磁盤IO的速度。但是由于IO操作都是由系統(tǒng)內(nèi)核調(diào)用來(lái)完成的,系統(tǒng)調(diào)用是又通過(guò)CPU來(lái)調(diào)度的。由于CPU的速度遠(yuǎn)遠(yuǎn)快于IO操作,導(dǎo)致浪費(fèi)CPU寶貴的時(shí)間來(lái)等待慢速的IO操作。為了讓快速的CPU和慢速IO設(shè)備能更好的協(xié)調(diào)工作,減少CPU在IO調(diào)用的上的消耗,逐漸發(fā)展出各種IO模型。
阻塞IO模型BIO blocking I/O
同步阻塞IO模型是最常用也是最簡(jiǎn)單的IO模型,在Linux系統(tǒng)中默認(rèn)情況下,所有的套接字socket都是阻塞的。這里的阻塞是指當(dāng)前發(fā)起IO操作的進(jìn)程會(huì)被阻塞,同步阻塞IO是指當(dāng)進(jìn)程調(diào)用某些IO操作的系統(tǒng)調(diào)用或庫(kù)函數(shù)時(shí),比如accept()、send()、recv()等時(shí)進(jìn)程會(huì)暫停下來(lái)等待IO操作結(jié)束后再繼續(xù)運(yùn)行。

同步阻塞IO中進(jìn)程的等待時(shí)間可能會(huì)包含兩部分:一個(gè)是等待數(shù)據(jù)就緒,比如等待數(shù)據(jù)可以讀和可以寫。另一個(gè)是等待數(shù)據(jù)的復(fù)制,當(dāng)數(shù)據(jù)準(zhǔn)備就緒后對(duì)數(shù)據(jù)的讀寫操作會(huì)比較耗時(shí)。

傳統(tǒng)的IO模型,在讀寫數(shù)據(jù)過(guò)程中會(huì)發(fā)生阻塞現(xiàn)象。當(dāng)用戶線程發(fā)出IO請(qǐng)求后,內(nèi)核會(huì)去查看數(shù)據(jù)是否就緒,如果沒(méi)有就緒就會(huì)等待數(shù)據(jù)就緒,而用戶線程會(huì)處于阻塞狀態(tài),用戶線程交出CPU。當(dāng)數(shù)據(jù)就緒后內(nèi)核會(huì)將數(shù)據(jù)拷貝到用戶線程,并返回結(jié)果給用戶線程,用戶線程才結(jié)束阻塞狀態(tài)。
在內(nèi)核將數(shù)據(jù)準(zhǔn)備好之前,系統(tǒng)調(diào)用會(huì)一直等待所有的socket,默認(rèn)是阻塞方式。
典型應(yīng)用
Linux中如果不對(duì)文件描述符fd做特殊設(shè)置,直接調(diào)用read就是同步阻塞IO,同步阻塞IO的兩個(gè)階段都需要等待完成后,read才會(huì)返回。
read(fd, buffer, count)
也就是說(shuō),如果遠(yuǎn)程一直沒(méi)有發(fā)送消息,read就永遠(yuǎn)不會(huì)有返回,整個(gè)線程就會(huì)阻塞在這里。
data = socket.read()// 如果數(shù)據(jù)沒(méi)有就緒會(huì)一直阻塞在read方法
如果數(shù)據(jù)沒(méi)有就緒會(huì)一直阻塞在read方法
fd = connect();// 文件描述符
write(fd);
read(fd);
close(fd);
程序的read必須在write之后執(zhí)行,當(dāng)write阻塞住了,read就不能執(zhí)行下去,一直處于等待狀態(tài)。
網(wǎng)絡(luò)模型
阻塞IO模型中應(yīng)用程序?yàn)榱藞?zhí)行read讀操作,會(huì)調(diào)用相應(yīng)的system call系統(tǒng)調(diào)用,將系統(tǒng)控制權(quán)交給內(nèi)核,然后就進(jìn)入等待,等待的過(guò)程是被阻塞的,內(nèi)核開(kāi)始執(zhí)行system call系統(tǒng)調(diào)用,執(zhí)行完畢后會(huì)向應(yīng)用程序返回響應(yīng),應(yīng)用程序得到響應(yīng)后就不再阻塞并繼續(xù)后續(xù)工作。

上圖所示
進(jìn)程調(diào)用一個(gè)recvfrom請(qǐng)求,但不會(huì)立即收到回復(fù),直到數(shù)據(jù)返回后才將數(shù)據(jù)從內(nèi)核空間復(fù)制到程序空間。
當(dāng)用戶進(jìn)程調(diào)用了recvfrom系統(tǒng)調(diào)用,內(nèi)核kernel就開(kāi)始了IO的第一個(gè)階段“準(zhǔn)備數(shù)據(jù)”。對(duì)于網(wǎng)絡(luò)IO來(lái)說(shuō)很多時(shí)候數(shù)據(jù)在一開(kāi)始還沒(méi)有到達(dá)。比如還沒(méi)有收到一個(gè)完整的UDP包,此時(shí)內(nèi)核kernel要等待足夠的數(shù)據(jù)到來(lái)。磁盤IO的情況就是等待磁盤數(shù)據(jù)從磁盤上讀取到內(nèi)核態(tài)內(nèi)存中。這個(gè)過(guò)程需要等待,也就是說(shuō)數(shù)據(jù)被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中是需要一個(gè)過(guò)程的。
用戶進(jìn)程這邊整個(gè)進(jìn)程會(huì)被阻塞,當(dāng)然是進(jìn)程自己選擇的阻塞。當(dāng)內(nèi)核一直等到數(shù)據(jù)準(zhǔn)備就緒后,就會(huì)將數(shù)據(jù)從內(nèi)核中拷貝到用戶內(nèi)存中,處于系統(tǒng)安全考慮,用戶態(tài)的程序是沒(méi)有權(quán)限直接讀取內(nèi)核態(tài)內(nèi)存,因此內(nèi)核負(fù)責(zé)將內(nèi)核態(tài)內(nèi)存中的數(shù)據(jù)拷貝一份到用戶態(tài)內(nèi)存中。然后內(nèi)核返回結(jié)果,用戶進(jìn)程才會(huì)接觸阻塞block的狀態(tài),重新運(yùn)行起來(lái)。所以,阻塞時(shí)IO的特點(diǎn)就是在IO執(zhí)行的兩個(gè)階段都被阻塞了。
優(yōu)缺點(diǎn)
- 優(yōu)點(diǎn):實(shí)時(shí)性高能夠及時(shí)返回?cái)?shù)據(jù),響應(yīng)及時(shí)無(wú)延遲。
- 缺點(diǎn):需要阻塞等待且性能差,對(duì)用戶來(lái)說(shuō)等待就要付出性能代價(jià)。
適用場(chǎng)景
BIO方式適用于連接數(shù)量較少且固定的架構(gòu),這種方式對(duì)服務(wù)器資源要求比較高,服務(wù)器實(shí)現(xiàn)模式為一個(gè)連接一個(gè)線程,即客戶端有連接請(qǐng)求時(shí)服務(wù)端就需要啟動(dòng)一個(gè)線程進(jìn)行處理。如果這個(gè)連接不做任何事情則會(huì)造成不必要的線程開(kāi)銷,當(dāng)然可以通過(guò)線程池機(jī)制加以改善。
非阻塞IO模型NIO noblocking I/O
同步非阻塞IO對(duì)比同步阻塞IO而言,它不會(huì)去等待數(shù)據(jù)的就緒,如果數(shù)據(jù)不可讀或不可寫,相關(guān)的系統(tǒng)調(diào)用會(huì)立即高速進(jìn)程并立即返回。這樣做的好處是結(jié)合反復(fù)輪詢來(lái)嘗試數(shù)據(jù)是否就緒,那么在一個(gè)進(jìn)程中就可以同時(shí)處理多個(gè)IO操作。

非阻塞IO一般只針對(duì)網(wǎng)絡(luò)IO有效,當(dāng)在socket的選項(xiàng)中設(shè)置O_NONBLOCK時(shí),此時(shí)socket的send()或recv()就會(huì)采用非阻塞方式。對(duì)于磁盤IO非阻塞IO并不會(huì)產(chǎn)生效果。為什么呢?
- 文件描述符
fd在read之前有可能會(huì)重新進(jìn)入不可讀的狀態(tài),要么被其他人都走了(驚群?jiǎn)栴}),還有可能被內(nèi)核拋棄了??偟膩?lái)說(shuō),fd因?yàn)樵?code>read之前數(shù)據(jù)被其它方式讀走,fd重新變?yōu)椴豢勺x。此時(shí)使用阻塞時(shí)IO的read函數(shù)就會(huì)阻塞整個(gè)線程。 -
epoll只是返回了可讀事件并沒(méi)有返回可以讀多少數(shù)據(jù)量,因此非阻塞IO的做法是都多次直到不能讀。而阻塞式IO卻只能讀一次,因?yàn)槿f(wàn)一一次就讀完了緩沖區(qū)的所有數(shù)據(jù),第二次讀的時(shí)候read就會(huì)有阻塞了。對(duì)于epoll的ET模式來(lái)說(shuō),緩沖區(qū)的數(shù)據(jù)只會(huì)在改變時(shí)通知一次,如果此次沒(méi)有消費(fèi)完,在下次數(shù)據(jù)到來(lái)之前,可讀事件再就也不會(huì)通知。這對(duì)只能調(diào)用一次的read的阻塞式IO來(lái)說(shuō),未讀完的數(shù)據(jù)就有可能永遠(yuǎn)讀不到了。
實(shí)現(xiàn)原理
當(dāng)用戶線程發(fā)起一個(gè)read讀操作后并不需要等待,而是馬上就得到一個(gè)結(jié)果。如果結(jié)果是一個(gè)error錯(cuò)誤,就表示數(shù)據(jù)還沒(méi)有準(zhǔn)備好,于是可以再次發(fā)送read讀操作。一旦內(nèi)存中的數(shù)據(jù)準(zhǔn)備好了并且又再次收到用戶線程的請(qǐng)求,那么會(huì)馬上就將數(shù)據(jù)拷貝到用戶線程然后返回。事實(shí)上,在非阻塞IO模型中,用戶線程需要不斷詢問(wèn)內(nèi)核數(shù)據(jù)是否就緒,換句話說(shuō)非阻塞IO不會(huì)交出CPU而會(huì)一直占用CPU。

與阻塞時(shí)I/O不同的是,非阻塞的recvfrom系統(tǒng)調(diào)用執(zhí)行后,進(jìn)程并不會(huì)被阻塞,內(nèi)核會(huì)立即返回給進(jìn)程。如果數(shù)據(jù)還未就緒(準(zhǔn)備好),此時(shí)會(huì)返回一個(gè)錯(cuò)誤error(EAGAIN或EWOULDBLOCK)。
進(jìn)程在返回之后可以處理其它業(yè)務(wù)邏輯,過(guò)會(huì)兒再發(fā)起recvfrom系統(tǒng)調(diào)用。采用這種輪詢的方式不斷檢查內(nèi)核數(shù)據(jù),直到數(shù)據(jù)準(zhǔn)備就緒,再拷貝數(shù)據(jù)到進(jìn)程進(jìn)行數(shù)據(jù)處理。
上圖所示:前三次調(diào)用recvfrom請(qǐng)求都沒(méi)有數(shù)據(jù)返回,內(nèi)核每次都返回一個(gè)錯(cuò)誤errno(EWOULDBLOCK), 但并不會(huì)阻塞進(jìn)程。當(dāng)?shù)谒拇握{(diào)用recvfrom時(shí)數(shù)據(jù)已經(jīng)準(zhǔn)備就緒,則將其從內(nèi)核空間拷貝到程序空間進(jìn)行處理數(shù)據(jù)。
值得注意的是,在非阻塞狀態(tài)下,IO執(zhí)行的等待階段并不是完全阻塞的,但第二個(gè)階段依然處于一個(gè)阻塞狀態(tài)。
典型應(yīng)用
while(true)
{
data = socket.read();
if(data != error)
{
//handle data
break;
}
}
對(duì)于非阻塞IO有一個(gè)非常嚴(yán)重的問(wèn)題是在while循環(huán)中需要不斷地去詢問(wèn)內(nèi)核數(shù)據(jù)是否就緒,這樣會(huì)導(dǎo)致CPU占用率非常高,因此一般情況下很少使用while循環(huán)這種方式來(lái)讀取數(shù)據(jù)。
網(wǎng)絡(luò)模型
當(dāng)用戶進(jìn)程發(fā)出read讀操作時(shí)會(huì)調(diào)用相應(yīng)的system call,這個(gè)system call會(huì)立即從內(nèi)核中返回。但在返回的這個(gè)時(shí)間點(diǎn)中內(nèi)核中的數(shù)據(jù)可能還沒(méi)有準(zhǔn)備好,也就是說(shuō)內(nèi)核只是很快就返回了system call,只有這樣才不會(huì)阻塞用戶進(jìn)程。對(duì)于應(yīng)用程序,雖然這個(gè)IO操作很快就返回了,但并不知道這個(gè)IO操作是否真正成功了,為了知道IO操作是否成功,應(yīng)用程序需要主動(dòng)循環(huán)的去詢問(wèn)內(nèi)核。

每次客戶詢問(wèn)內(nèi)核是否有數(shù)據(jù)準(zhǔn)備好,即文件描述符緩沖區(qū)是否就緒。當(dāng)有數(shù)據(jù)報(bào)準(zhǔn)備好時(shí),就進(jìn)行拷貝數(shù)據(jù)報(bào)。當(dāng)沒(méi)有數(shù)據(jù)報(bào)準(zhǔn)備好時(shí)也不會(huì)阻塞程序,內(nèi)核直接返回未準(zhǔn)備就緒的信號(hào),等待用戶程序的下一個(gè)輪詢。但輪詢對(duì)CPU來(lái)說(shuō)是較大的浪費(fèi),一般只有在特定場(chǎng)景下才使用。
優(yōu)缺點(diǎn)
- 優(yōu)點(diǎn):能夠在等待的事件里去做其它的事情
- 缺點(diǎn):任務(wù)完成得響應(yīng)延遲增大了,因?yàn)槊窟^(guò)一段時(shí)間去輪詢一次
read讀操作 ,而任務(wù)可能在兩次輪詢之間的任意時(shí)間完成,這將導(dǎo)致整體數(shù)據(jù)吞吐量的降低。
同步非阻塞與同步阻塞之間有什么優(yōu)缺點(diǎn)呢?
- 優(yōu)點(diǎn):同步非阻塞能夠在等待任務(wù)完成得時(shí)間里做其它事情,包括提交其它任務(wù),也就是說(shuō)“后臺(tái)”可以有多個(gè)任務(wù)在同時(shí)執(zhí)行。
- 缺點(diǎn):同步非阻塞任務(wù)完成得響應(yīng)時(shí)間延遲增大了,因?yàn)槊窟^(guò)一段時(shí)間需要去輪詢一次
read讀操作,任務(wù)可能在兩次輪詢之間的任意時(shí)間中已經(jīng)完成了。這將導(dǎo)致整體數(shù)據(jù)吞吐量的降低。
多路復(fù)用IO模型 I/O multiplexing
多路復(fù)用IO模型是目前使用較多的一種模型,Java NIO實(shí)際上就是多路復(fù)用IO。
IO復(fù)用也叫做多路IO就緒通知,是一種進(jìn)程預(yù)先告知內(nèi)核的能力,內(nèi)核發(fā)現(xiàn)進(jìn)程指定的一個(gè)或多個(gè)IO條件就緒了,就會(huì)去通知進(jìn)程,使得一個(gè)進(jìn)程能在一連串的事件上等待。
簡(jiǎn)單來(lái)說(shuō),就是指定一個(gè)線程,通過(guò)記錄IO流的狀態(tài)來(lái)同時(shí)管理多個(gè)IO,以提高服務(wù)器的吞吐能力。IO多路復(fù)用的好處在于單個(gè)進(jìn)程就可以同時(shí)處理多個(gè)網(wǎng)絡(luò)連接的IO。

IO多路復(fù)用的基本原理是不再由應(yīng)用程序自己監(jiān)視連接,取而代之由內(nèi)核替應(yīng)用程序監(jiān)視文件描述符。
多路IO就緒通知模型允許進(jìn)程通過(guò)一種方法同時(shí)監(jiān)視所有的文件描述符,并能快速獲得所有就緒的文件描述符,然后針對(duì)這些文件描述符進(jìn)行數(shù)據(jù)訪問(wèn)。簡(jiǎn)單來(lái)說(shuō),就是提供了對(duì)大量文件描述符就緒檢查的高性能方案。
需要注意的是,IO就緒模型只是解決了快速獲取就緒的文件描述符的問(wèn)題,在得知數(shù)據(jù)就緒后,就數(shù)據(jù)訪問(wèn)本身而言,還是需要選擇阻塞或非阻塞的訪問(wèn)方式。
實(shí)現(xiàn)方式
由于平臺(tái)和歷史原因,多路IO就緒通知有多種不同的實(shí)現(xiàn)方式,性能上也存在一定的差異。IO 復(fù)用的實(shí)現(xiàn)方式主要包括select、poll、epoll。
- select
select最早出現(xiàn)于1934年BSD4.2中,通過(guò)一個(gè)select()系統(tǒng)調(diào)用來(lái)監(jiān)視包含多個(gè)文件描述符的數(shù)組,當(dāng)select()返回后這個(gè)數(shù)組中就緒的文件描述符會(huì)被內(nèi)核修改標(biāo)志位,使得進(jìn)程可以獲得這些文件描述符,從而進(jìn)行后續(xù)的讀寫操作。
select的缺點(diǎn)在于單進(jìn)程能夠監(jiān)視的文件描述符的數(shù)量存在最大限制,在Linux上一般是1024,不過(guò)可以通過(guò)修改宏定義或重新編譯內(nèi)核的方式來(lái)提升限制。所以,如果使用select()的服務(wù)器已經(jīng)維持了1024個(gè)連接,后續(xù)的請(qǐng)求可能會(huì)被拒絕。
另外,select()維護(hù)著存儲(chǔ)大量文件描述符的數(shù)據(jù)結(jié)構(gòu),隨著文件描述符數(shù)量的增加,復(fù)制開(kāi)銷也線性增長(zhǎng)。
另一方面,由于網(wǎng)絡(luò)延遲使大量TCP連接處于非活躍狀態(tài),調(diào)用select()會(huì)對(duì)所有的socket進(jìn)行一次線性掃描,也會(huì)浪費(fèi)一定的開(kāi)銷。
- poll
poll誕生于1986年的System V Release3,顯然UNIX不愿意直接沿用BSD的select,而是重新實(shí)現(xiàn)了一遍。poll和select本質(zhì)上沒(méi)有太多區(qū)別,只是poll沒(méi)有最大文件描述符數(shù)量的限制。
select和poll的原理基本相同:
- 注冊(cè)待監(jiān)聽(tīng)的文件描述符
fd,這里的fd創(chuàng)建時(shí)最好是非阻塞的。 - 每次調(diào)用都去檢查
fd文件描述符的狀態(tài),當(dāng)有一個(gè)或多個(gè)fd就緒時(shí)返回。 - 返回結(jié)果中包含已就緒和未就緒的文件描述符
fd
相比select,poll解決了單進(jìn)程能夠打開(kāi)文件描述符數(shù)量有限的問(wèn)題,由于select受限于FD_SIZE的限制,若修改FD_SIZE宏需重新編譯內(nèi)核。poll通過(guò)一個(gè)pollfd數(shù)組向內(nèi)核傳遞需要關(guān)注的事件,避開(kāi)了文件描述符的數(shù)量限制。
此外,select和poll共同具有一個(gè)很大的缺點(diǎn)是包含大量文件描述符fd的數(shù)組會(huì)被整體復(fù)制到用戶態(tài)和內(nèi)核態(tài)地址空間之間,不論這些文件描述符fd是否就緒,其開(kāi)銷會(huì)隨著文件描述符fd數(shù)量增多而線性增大。
另外,select()和poll()將就緒的文件描述符fd告訴進(jìn)程后,如果進(jìn)程沒(méi)有對(duì)其進(jìn)行IO操作,那么下次調(diào)用select()或poll()時(shí)會(huì)再次報(bào)告這些文件描述符,所以它們一般不會(huì)丟失就緒的消息,這種方式稱為水平觸發(fā)(Level Triggered)。
- epoll
而epoll的出現(xiàn)解決select和poll的缺點(diǎn):
-
epoll基于事件驅(qū)動(dòng)的方式避免了每次都要將所有fd都掃描一遍 -
epoll_wait只返回就緒的fd -
epoll使用nmap內(nèi)存映射技術(shù)避免了內(nèi)存復(fù)制的開(kāi)銷 -
epoll的fd數(shù)量上限是操作系統(tǒng)的最大文件句柄數(shù)量,此數(shù)量和內(nèi)存相關(guān),通常大于1024。
目前epoll是Linux2.6下最高效的IO復(fù)用方式,也是Nginx、Node的IO實(shí)現(xiàn)方式。
- 水平觸發(fā)與邊緣觸發(fā)
此外,對(duì)于IO復(fù)用還有一個(gè)水平觸發(fā)和邊緣觸發(fā)的概念:
- 水平觸發(fā):當(dāng)就緒的
fd未被用戶進(jìn)程處理后,下一次查詢依舊會(huì)返回,這是select和poll的觸發(fā)方式。 - 邊緣觸發(fā):無(wú)論就緒的
fd是否被處理,下一次不再返回。
理論上邊緣觸發(fā)的性能更高,但是實(shí)現(xiàn)相當(dāng)復(fù)雜,任何以外的丟失事件都會(huì)造成請(qǐng)求處理錯(cuò)誤。epoll默認(rèn)采用水平觸發(fā)的方式,可通過(guò)配置選項(xiàng)可使用邊緣觸發(fā)。
實(shí)現(xiàn)原理
- 當(dāng)進(jìn)程調(diào)用
select時(shí)會(huì)被阻塞 - 此時(shí)內(nèi)核會(huì)監(jiān)視所有
select負(fù)責(zé)的socket,當(dāng)socket的數(shù)據(jù)準(zhǔn)備就緒后立即返回 - 進(jìn)程再次調(diào)用
read讀操作,數(shù)據(jù)從內(nèi)核中拷貝到進(jìn)程。
在多路復(fù)用IO模型中會(huì)有一個(gè)線程不斷去輪詢多個(gè)socket的狀態(tài),只有當(dāng)socket真正有讀寫事件時(shí),才真正調(diào)用實(shí)際的IO讀寫操作。
在多路復(fù)用IO模型中,只需要使用一個(gè)線程就可以管理多個(gè)socket,系統(tǒng)不需要建立新的線程或進(jìn)程,也不必維護(hù)這些進(jìn)程和線程,只有在真正有socket讀寫事件進(jìn)行時(shí)才會(huì)使用IO資源,所以它大大減少了資源占用。

上圖所示:這里需要兩個(gè)系統(tǒng)調(diào)用system call分別是select和recvfrom,阻塞IO只調(diào)用了一個(gè)系統(tǒng)調(diào)用recvfrom。如果處理的連接數(shù)不是很高的話,使用IO復(fù)用的服務(wù)器并不一定比使用“多線程+非阻塞IO”的性能更好,可能延遲還更大。
IO復(fù)用的優(yōu)勢(shì)并不是對(duì)于單個(gè)連接能處理的更快,而是單個(gè)進(jìn)程就可以同時(shí)處理多個(gè)網(wǎng)絡(luò)連接的IO。實(shí)際使用時(shí),對(duì)于每個(gè)socket都可以設(shè)置為非阻塞的。
上圖所示,整個(gè)用戶進(jìn)程其實(shí)是一直被阻塞的。只不過(guò)進(jìn)程是被select函數(shù)阻塞,而不是被IO操作給阻塞。所以IO多路復(fù)用是阻塞在select、epoll這樣的系統(tǒng)調(diào)用之上,而沒(méi)有阻塞在整整的IO系統(tǒng)調(diào)用如recvfrom上。
網(wǎng)路模型
當(dāng)用戶進(jìn)程發(fā)出read讀操作時(shí)會(huì)調(diào)用相應(yīng)的system call之后,并不等待內(nèi)核的返回結(jié)果而時(shí)立即返回。雖然返回結(jié)果的調(diào)用函數(shù)是一個(gè)異步的方式,但應(yīng)用程序會(huì)被像select、poll、epoll等具有多個(gè)文件描述符的函數(shù)阻塞住,一直等到system call有結(jié)果返回后再通知應(yīng)用程序。這種情況從IO操作的實(shí)際效果來(lái)看,異步阻塞IO和同步阻塞IO是一樣的,應(yīng)用程序都是一直等到IO操作成功后(數(shù)據(jù)已經(jīng)被寫入或讀?。?,才開(kāi)始進(jìn)行后續(xù)工作。不同點(diǎn)在于異步阻塞IO用一個(gè)select函數(shù)可以為多個(gè)文件描述符提供通知,提供了并發(fā)性。
例如:如果有1w個(gè)并發(fā)的read讀請(qǐng)求,但網(wǎng)絡(luò)上仍然沒(méi)有數(shù)據(jù),此時(shí)這1w個(gè)read會(huì)同時(shí)各自阻塞,現(xiàn)在用select、poll、epoll這樣的函數(shù)來(lái)專門負(fù)責(zé)阻塞同時(shí)監(jiān)聽(tīng)這1w個(gè)請(qǐng)求的狀態(tài),一旦有數(shù)據(jù)到達(dá)就負(fù)責(zé)通知,這樣就將1w個(gè)等待和阻塞轉(zhuǎn)化為一個(gè)專門的函數(shù)來(lái)負(fù)責(zé)與管理。

IO多路復(fù)用多了一個(gè)select函數(shù),select函數(shù)有一個(gè)參數(shù)是文件描述符集合,對(duì)這些文件描述符進(jìn)行循環(huán)監(jiān)聽(tīng),當(dāng)某個(gè)文件描述符就緒時(shí)就對(duì)這個(gè)文件描述符進(jìn)行處理。其中select只負(fù)責(zé)等,recvfrom只負(fù)責(zé)拷貝。IO多路復(fù)用屬于阻塞IO,但可以對(duì)多個(gè)文件描述符進(jìn)行阻塞監(jiān)聽(tīng),所以效率比阻塞IO要高。
異步IO與同步IO的區(qū)別
同步IO是需要應(yīng)用程序主動(dòng)地循環(huán)去詢問(wèn)是否有數(shù)據(jù),異步IO是通過(guò)像select等IO多路復(fù)用函數(shù)來(lái)同時(shí)檢測(cè)多個(gè)事件句柄來(lái)告知應(yīng)用程序是否有數(shù)據(jù)。
高并發(fā)的程序一般使用“同步非阻塞”模式,而不是“多線程+同步阻塞”模式。要理解這一點(diǎn)需要先弄清楚并發(fā)和并行的區(qū)別。并發(fā)數(shù)是同時(shí)進(jìn)行的任務(wù)數(shù),并行數(shù)是可以同時(shí)工作的物理資源數(shù)量(如CPU核數(shù))。
通過(guò)合理調(diào)度任務(wù)的不同階段,并發(fā)數(shù)可以遠(yuǎn)遠(yuǎn)大于并行數(shù)。這就是區(qū)區(qū)幾個(gè)CPU可以支持上萬(wàn)用戶并發(fā)請(qǐng)求的原因。在這種高并發(fā)的情況下,為每個(gè)用戶請(qǐng)求創(chuàng)建一個(gè)進(jìn)程或線程的開(kāi)銷非常大,而同步非阻塞方式可以把多個(gè)IO請(qǐng)求丟到后臺(tái)去,這樣CPU就可以服務(wù)大量的并發(fā)IO請(qǐng)求了。
IO多路復(fù)用究竟是同步阻塞還是異步阻塞模型呢?
同步是需要主動(dòng)等待消息通知,異步則是被動(dòng)接受消息通知,通過(guò)回調(diào)、通知、狀態(tài)等方式來(lái)被動(dòng)獲取消息。IO多路復(fù)用在阻塞到select階段時(shí),用戶進(jìn)程是主動(dòng)等待并調(diào)用select函數(shù)來(lái)獲取就緒狀態(tài)消息,并且其進(jìn)程狀態(tài)為阻塞。所以IO多路復(fù)用是同步阻塞模式。
優(yōu)勢(shì)
與傳統(tǒng)的多進(jìn)程或多線程模型相比,IO多路復(fù)用的最大優(yōu)勢(shì)是系統(tǒng)開(kāi)銷小,系統(tǒng)無(wú)需創(chuàng)建新的進(jìn)程或線程,也無(wú)需維護(hù)這些進(jìn)程和線程的運(yùn)行,因此降低了系統(tǒng)的維護(hù)工作量并節(jié)省了系統(tǒng)資源。
應(yīng)用場(chǎng)景
- 服務(wù)器需要同時(shí)處理多個(gè)處于監(jiān)聽(tīng)狀態(tài)或多個(gè)連接狀態(tài)的套接字
- 服務(wù)器需要同時(shí)處理多種網(wǎng)絡(luò)協(xié)議的套接字
- 服務(wù)器需要監(jiān)聽(tīng)多個(gè)端口或處理多種服務(wù)
- 服務(wù)器需要同時(shí)處理用戶輸入和網(wǎng)絡(luò)連接
信號(hào)驅(qū)動(dòng)IO模型singal blocking I/O
在信號(hào)驅(qū)動(dòng)IO模型中,當(dāng)用戶線程發(fā)起一個(gè)IO請(qǐng)求操作, 會(huì)給對(duì)應(yīng)的socket注冊(cè)一個(gè)信號(hào)函數(shù),然后用戶線程會(huì)繼續(xù)執(zhí)行,當(dāng)內(nèi)核數(shù)據(jù)就緒時(shí)會(huì)發(fā)送一個(gè)信號(hào)給用戶線程,用戶線程接收到信號(hào)后,便在信號(hào)函數(shù)中調(diào)用IO讀寫操作來(lái)進(jìn)行實(shí)際的IO請(qǐng)求操作。

業(yè)務(wù)流程
- 開(kāi)啟套接字信號(hào)驅(qū)動(dòng)IO功能
- 系統(tǒng)調(diào)用
sigaction執(zhí)行信號(hào)處理函數(shù),信號(hào)處理函數(shù)是非阻塞的會(huì)立即返回。 - 數(shù)據(jù)就緒并生成
sigio信號(hào),通過(guò)信號(hào)回調(diào)通知應(yīng)用讀取數(shù)據(jù)。
網(wǎng)絡(luò)模型
應(yīng)用程序提交read讀請(qǐng)求后調(diào)用system call,然后內(nèi)核開(kāi)始處理相應(yīng)的IO操作。同時(shí)應(yīng)用程序并不等內(nèi)核返回響應(yīng)就會(huì)開(kāi)始執(zhí)行其它的處理操作(應(yīng)用程序沒(méi)有被IO阻塞)。
當(dāng)內(nèi)核執(zhí)行完畢返回read響應(yīng),會(huì)產(chǎn)生一個(gè)信號(hào)或執(zhí)行一個(gè)基于回調(diào)函數(shù)來(lái)完成這次IO處理過(guò)程。在這里IO的讀寫操作是在IO事件發(fā)生之后由應(yīng)用程序來(lái)完成的。異步IO讀寫操作總是立即返回,不論IO是否阻塞,因?yàn)檎嬲淖x寫操作已經(jīng)由內(nèi)核掌管。
也就是說(shuō),同步IO模型要求用戶自行執(zhí)行IO操作(將數(shù)據(jù)從內(nèi)核緩沖區(qū)移動(dòng)到用戶緩沖區(qū),或相反),異步操作機(jī)制則由內(nèi)核來(lái)執(zhí)行IO操作。簡(jiǎn)單來(lái)說(shuō),同步IO向應(yīng)用程序通知的是IO就緒事件,而異步IO向應(yīng)用程序通知的是IO完成事件。

信號(hào)驅(qū)動(dòng)IO模型中應(yīng)用程序告訴內(nèi)核,當(dāng)數(shù)據(jù)包準(zhǔn)備好的時(shí)候,給我發(fā)送一個(gè)信號(hào),對(duì)SIGIO信號(hào)進(jìn)行捕捉,并且調(diào)用我的信號(hào)處理函數(shù)來(lái)獲取數(shù)據(jù)報(bào)。
問(wèn)題缺陷
信號(hào)驅(qū)動(dòng)IO模式存在一個(gè)很大的問(wèn)題是Linux中信號(hào)隊(duì)列是有限的,如果超過(guò)限制則無(wú)法讀取數(shù)據(jù)。
異步IO模型
異步IO又叫做事件驅(qū)動(dòng)IO,異步IO操作是需要操作系統(tǒng)底層支持。
異步IO模型是最理想的IO模型,在異步IO模型中當(dāng)用戶線程發(fā)起read讀操作后立即就可以開(kāi)始去做其它的事情。從內(nèi)核角度看,當(dāng)內(nèi)核收到一個(gè)asynchronous read之后會(huì)立即返回,說(shuō)明read請(qǐng)求已經(jīng)成功發(fā)起了,因此不會(huì)對(duì)用戶線程產(chǎn)生任何阻塞block。
然后,內(nèi)核會(huì)等待數(shù)據(jù)準(zhǔn)備完成,然后將數(shù)據(jù)拷貝到用戶線程,當(dāng)這一切都完成之后,內(nèi)核會(huì)給用戶線程發(fā)送一個(gè)信號(hào),告訴它read讀操作完成了。也就是說(shuō)用戶線程完全不需要知道實(shí)際整個(gè)IO操作是如何進(jìn)行的,只需要先發(fā)起一個(gè)請(qǐng)求,當(dāng)接收內(nèi)核返回的成功信號(hào)時(shí),表示IO操作已經(jīng)完成可以直接去使用數(shù)據(jù)了。

在異步IO模型中,IO操作的兩個(gè)階段都不會(huì)阻塞用戶線程,這兩個(gè)階段都是由內(nèi)核自動(dòng)完成的,然后發(fā)送一個(gè)信號(hào)告知用戶線程操作已經(jīng)完成。用戶線程中不需要再次調(diào)用IO函數(shù)進(jìn)行具體的讀寫。這點(diǎn)和信號(hào)驅(qū)動(dòng)模型有所不同。在信號(hào)驅(qū)動(dòng)模型中,當(dāng)用戶線程接收到信號(hào)表示數(shù)據(jù)已經(jīng)就緒,然后需要用戶線程調(diào)用IO函數(shù)進(jìn)行實(shí)際的讀寫操作,在異步IO模型中,收到信號(hào)表示IO操作已經(jīng)完成,不需要再在用戶線程中調(diào)用IO函數(shù)進(jìn)行實(shí)際讀寫操作。

異步IO和異步概念一樣,當(dāng)一個(gè)異步過(guò)程調(diào)用發(fā)出后,調(diào)用者不能立即得到結(jié)果,實(shí)際處理這個(gè)調(diào)用的函數(shù)在完成后,通過(guò)狀態(tài)、通知、回調(diào)函數(shù)來(lái)通知調(diào)用者的IO操作。
異步IO的工作機(jī)制是告知內(nèi)核啟動(dòng)某個(gè)操作,并讓內(nèi)核在整個(gè)操作完成后通知我們,這種模型與信號(hào)驅(qū)動(dòng)的IO區(qū)域在于,信號(hào)驅(qū)動(dòng)IO是由內(nèi)核通知我們何時(shí)可以啟動(dòng) 一個(gè)IO操作,這個(gè)IO操作由用戶自定義的信號(hào)函數(shù)來(lái)實(shí)現(xiàn),而異步IO模型是由內(nèi)核告知我們IO操作何時(shí)完成。
小結(jié)
前四種IO模型實(shí)際上都屬于同步IO,只有最后一種是真正的異步IO,因?yàn)闊o(wú)論是多路復(fù)用IO還是信號(hào)驅(qū)動(dòng)模型,IO操作的第二階段都會(huì)引起用戶線程阻塞,也就是內(nèi)核進(jìn)行數(shù)據(jù)拷貝的過(guò)程會(huì)讓用戶線程阻塞。

另外根據(jù)阻塞程度效率由低到高的順序是:阻塞IO > 非阻塞IO > 多路復(fù)用IO > 信號(hào)驅(qū)動(dòng)IO > 異步IO
IO設(shè)計(jì)模式
傳統(tǒng)IO設(shè)計(jì)模式
在傳統(tǒng)網(wǎng)路服務(wù)設(shè)計(jì)模式中,有兩種經(jīng)典的模式:多線程、線程池
多線程模式
多線程模式簡(jiǎn)單來(lái)說(shuō)就是來(lái)了客戶端服務(wù)器就會(huì)建立一個(gè)線程來(lái)處理該客戶端的讀寫事件

多線程模型雖然處理簡(jiǎn)單但由于服務(wù)器為每個(gè)客戶端的連接都建立一個(gè)線程去處理,資源占用非常大。因此當(dāng)連接數(shù)達(dá)到上限時(shí),后續(xù)的用戶連接請(qǐng)求將會(huì)直接導(dǎo)致資源瓶頸,嚴(yán)重的可能會(huì)直接導(dǎo)致服務(wù)器崩潰。
線程池模式
為了解決這種“一個(gè)線程對(duì)應(yīng)一個(gè)客戶端”模式帶來(lái)的弊端,提出了線程池的方式,也就是說(shuō)創(chuàng)建一個(gè)固定大小的線程池,來(lái)一個(gè)客戶端就從線程池中獲取一個(gè)空閑的線程來(lái)處理,當(dāng)客戶端處理完讀寫操作之后就交出對(duì)線程的占用。這樣就避免為每個(gè)客戶端創(chuàng)建線程帶來(lái)的資源浪費(fèi),使得線程可以復(fù)用。
線程池的弊端在于如果連接池中大多是長(zhǎng)連接可能會(huì)導(dǎo)致一段時(shí)間內(nèi),線程池中的線程都被占用,再有用戶請(qǐng)求連接時(shí)由于沒(méi)有可用的空閑線程來(lái)處理,會(huì)導(dǎo)致客戶端連接失敗,從而影響用戶體驗(yàn),因此線程持比較適合大量的短連接的應(yīng)用。
高性能IO設(shè)計(jì)模式
Reactor
在Reactor模式中會(huì)先對(duì)每個(gè)客戶端注冊(cè)感興趣的事件,然后有一個(gè)線程專門去輪詢每個(gè)客戶端是否有事件發(fā)生,當(dāng)有事件發(fā)生時(shí)便順序處理每個(gè)事件,當(dāng)所有事件都處理完畢后便再轉(zhuǎn)去繼續(xù)輪詢。

IO模型中的多路復(fù)用IO模型采用的就時(shí)Reactor模式

Proactor
在Proactor模式中當(dāng)檢測(cè)到有事件發(fā)生時(shí)會(huì)新起一個(gè)異步操作,然后交由內(nèi)核線程去處理,當(dāng)內(nèi)核線程完成IO操作之后,發(fā)送一個(gè)通知告知操作已經(jīng)完成。IO模型中的異步IO模型采用的就時(shí)Proactor模式。
未完待續(xù)...