IO模型

在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ì)流的操作。

文件描述符fdFile 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é)(從虛擬地址0xC00000000xFFFFFFFF)提供給內(nèi)核kernel使用,稱之為內(nèi)核空間。將較低的3G字節(jié)(從虛擬地址0x000000000xBFFFFFFF)提供給進(jìn)程使用,稱之為用戶空間。

內(nèi)核空間中存放的是內(nèi)核代碼和數(shù)據(jù),進(jìn)程的用戶空間中存放的是用戶程序的代碼和數(shù)據(jù)。不管是內(nèi)核空間還是用戶空間,它們都處于虛擬空間中。

Linux提供了兩級(jí)保護(hù)機(jī)制:0級(jí)供內(nèi)核使用、3級(jí)供用戶程序使用

虛擬內(nèi)存

操作系統(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è)階段

  1. 內(nèi)核準(zhǔn)備數(shù)據(jù)
    將數(shù)據(jù)從磁盤文件中加載到內(nèi)核內(nèi)存空間(內(nèi)核緩沖區(qū)),等待數(shù)據(jù)準(zhǔn)備完畢,耗時(shí)較長(zhǎng)。
  2. 將數(shù)據(jù)從內(nèi)核拷貝到用戶空間
    將數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到用戶空間進(jìn)程的內(nèi)存中,耗時(shí)較短。
數(shù)據(jù)從外部磁盤向運(yùn)行中程序的內(nèi)存中移動(dòng)的過(guò)程

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ò)以下變化

  1. 保存處理機(jī)上下文,包括程序計(jì)數(shù)器和其它寄存器。
  2. 更新PCB信息
  3. 將進(jìn)程的PCB移入相應(yīng)的隊(duì)列,如就緒、在某事件阻塞等隊(duì)列。
  4. 選擇另外一個(gè)進(jìn)程執(zhí)行并更新PCB
  5. 更新內(nèi)存管理的數(shù)據(jù)結(jié)構(gòu)
  6. 恢復(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)題:

  1. 讓CPU等著,也就是程序暫停執(zhí)行后續(xù)代碼,等寫入完成后再接著后續(xù)執(zhí)行,這種模式稱為同步IO。
  2. 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è)。

socket通信模型

對(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è)階段:

  1. 等待:等待網(wǎng)絡(luò)上的數(shù)據(jù)分組到達(dá),然后復(fù)制到內(nèi)核的某個(gè)緩沖區(qū)。
  2. 復(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ù)。

read調(diào)用

需要注意的是不要使用操作磁盤文件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ǎng)絡(luò)數(shù)據(jù)流

服務(wù)器構(gòu)建網(wǎng)絡(luò)數(shù)據(jù)包觸發(fā)IO的過(guò)程

  1. 用戶空間進(jìn)程通過(guò)recvfrom函數(shù)接收等待接收數(shù)據(jù)包,并將接收到的數(shù)據(jù)包在內(nèi)核中通過(guò)四表五鏈檢查網(wǎng)絡(luò)狀態(tài),若通過(guò)網(wǎng)絡(luò)檢查則提交給用戶空間的HTTP進(jìn)程。
  2. HTTP進(jìn)程解析請(qǐng)求并發(fā)起系統(tǒng)調(diào)用read函數(shù),到達(dá)內(nèi)核空間。
  3. 內(nèi)核空間執(zhí)行read函數(shù)讀取磁盤內(nèi)容并將此內(nèi)容加載到內(nèi)存
  4. 內(nèi)核空間提交給用戶空間HTTP進(jìn)程并告知數(shù)據(jù)已經(jīng)read完畢
  5. 用戶空間HTTP進(jìn)程根據(jù)請(qǐng)求報(bào)文進(jìn)行構(gòu)建響應(yīng)報(bào)文
  6. 構(gòu)建完HTTP響應(yīng)報(bào)文后通知內(nèi)核空間構(gòu)建網(wǎng)絡(luò)封裝
  7. 內(nèi)核空間再次通過(guò)四表五鏈網(wǎng)絡(luò)狀態(tài),通過(guò)網(wǎng)卡發(fā)送構(gòu)建號(hào)的HTTP響應(yīng)報(bào)文。

單純根據(jù)流程可得到以下信息

  1. 單進(jìn)程接收響應(yīng)數(shù)據(jù)報(bào)文調(diào)用recvform函數(shù)時(shí),與此同時(shí)不執(zhí)行其它函數(shù)調(diào)用,此時(shí)嚴(yán)重影響效率。
  2. 當(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ò)編程中定義如下:

網(wǎng)絡(luò)IO
  1. 等待數(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)行解封裝。

  1. 將數(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)行。

socket

同步阻塞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í)。

同步阻塞IO是指用戶線程在內(nèi)核進(jìn)行IO操作時(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í)socketsend()recv()就會(huì)采用非阻塞方式。對(duì)于磁盤IO非阻塞IO并不會(huì)產(chǎn)生效果。為什么呢?

  • 文件描述符fdread之前有可能會(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。

同步非阻塞IO模型

與阻塞時(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ò)誤errorEAGAINEWOULDBLOCK)。

進(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ò)誤errnoEWOULDBLOCK), 但并不會(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ù)用

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)方式主要包括selectpoll、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)了一遍。pollselect本質(zhì)上沒(méi)有太多區(qū)別,只是poll沒(méi)有最大文件描述符數(shù)量的限制。

selectpoll的原理基本相同:

  1. 注冊(cè)待監(jiān)聽(tīng)的文件描述符fd,這里的fd創(chuàng)建時(shí)最好是非阻塞的。
  2. 每次調(diào)用都去檢查fd文件描述符的狀態(tài),當(dāng)有一個(gè)或多個(gè)fd就緒時(shí)返回。
  3. 返回結(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ù)量限制。

此外,selectpoll共同具有一個(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)解決selectpoll的缺點(diǎn):

  1. epoll基于事件驅(qū)動(dòng)的方式避免了每次都要將所有fd都掃描一遍
  2. epoll_wait只返回就緒的fd
  3. epoll使用nmap內(nèi)存映射技術(shù)避免了內(nèi)存復(fù)制的開(kāi)銷
  4. epollfd數(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ā)的概念:

  1. 水平觸發(fā):當(dāng)就緒的fd未被用戶進(jìn)程處理后,下一次查詢依舊會(huì)返回,這是selectpoll的觸發(fā)方式。
  2. 邊緣觸發(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)原理

  1. 當(dāng)進(jìn)程調(diào)用select時(shí)會(huì)被阻塞
  2. 此時(shí)內(nèi)核會(huì)監(jiān)視所有select負(fù)責(zé)的socket,當(dāng)socket的數(shù)據(jù)準(zhǔn)備就緒后立即返回
  3. 進(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資源,所以它大大減少了資源占用。

多路復(fù)用模型

上圖所示:這里需要兩個(gè)系統(tǒng)調(diào)用system call分別是selectrecvfrom,阻塞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、pollepoll等具有多個(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ù)用

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)求操作。

信號(hào)驅(qū)動(dòng)IO模型

業(yè)務(wù)流程

  1. 開(kāi)啟套接字信號(hào)驅(qū)動(dòng)IO功能
  2. 系統(tǒng)調(diào)用sigaction執(zhí)行信號(hào)處理函數(shù),信號(hào)處理函數(shù)是非阻塞的會(huì)立即返回。
  3. 數(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

信號(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模型中,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ì)讓用戶線程阻塞。

IO模型

另外根據(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ù)輪詢。

Reactor

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

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ù)...

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容