Linux的網(wǎng)絡(luò)IO模型
網(wǎng)絡(luò)IO的本質(zhì)是socket的讀寫,socket在Linux中被抽象為流,IO可以理解為對流的操作。
IO的分類和范疇
IO本身可以分為內(nèi)存IO、網(wǎng)絡(luò)IO和磁盤IO還有緩存IO等,一般討論IO時更多是指后(網(wǎng)絡(luò)IO和磁盤IO,因為這兩個是最慢的哈哈),此處特別分析和說明網(wǎng)絡(luò)IO。
操作處理的分類
阻塞/非阻塞
針對函數(shù)/方法的實現(xiàn)方式而言,即數(shù)據(jù)就緒之前是立刻返回還是等待,即發(fā)起IO請求后是否會阻塞。
阻塞IO機(jī)制
阻塞IO情況下,當(dāng)用戶調(diào)用read后,用戶線程會被阻塞,等內(nèi)核數(shù)據(jù)準(zhǔn)備好并且數(shù)據(jù)從內(nèi)核緩沖區(qū)拷貝到用戶態(tài)緩存區(qū)后read才會返回。可以看到是阻塞的兩個部分。
CPU把數(shù)據(jù)從磁盤讀到內(nèi)核緩沖區(qū)。
CPU把數(shù)據(jù)從內(nèi)核緩沖區(qū)拷貝到用戶緩沖區(qū)。

非阻塞IO機(jī)制

非阻塞IO發(fā)出read請求后發(fā)現(xiàn)數(shù)據(jù)沒準(zhǔn)備好,會繼續(xù)往下執(zhí)行,此時應(yīng)用程序會不斷輪詢polling內(nèi)核詢問數(shù)據(jù)是否準(zhǔn)備好,當(dāng)數(shù)據(jù)沒有準(zhǔn)備好時,內(nèi)核立即返回EWOULDBLOCK錯誤。
直到數(shù)據(jù)被拷貝到應(yīng)用程序緩沖區(qū),read請求才獲取到結(jié)果。并且你要注意!這里最后一次 read 調(diào)用獲取數(shù)據(jù)的過程,是一個同步的過程,是需要等待的過程。這里的同步指的是內(nèi)核態(tài)的數(shù)據(jù)拷貝到用戶程序的緩存區(qū)這個過程。
同步/異步
IO讀操作指數(shù)據(jù)流經(jīng):網(wǎng)絡(luò) -> 內(nèi)核緩沖區(qū) -> 用戶內(nèi)存
同步和異步的主要區(qū)別在于數(shù)據(jù)從內(nèi)核緩沖區(qū) -> 用戶內(nèi)存這個過程需不需要用戶進(jìn)程等待。
等待內(nèi)核態(tài)準(zhǔn)備數(shù)據(jù)結(jié)束之后,會自動回通知用戶態(tài)的線程進(jìn)行讀取信息數(shù)據(jù),此時之前用戶態(tài)的線程不需要等待,可以去做其他操作。
對于一個網(wǎng)絡(luò)IO,會涉及到兩個系統(tǒng)對象,一個是調(diào)用這個IO的process(or thread)【用戶態(tài)】,另一個就是系統(tǒng)內(nèi)核(kernel)【內(nèi)核態(tài)】
當(dāng)一個用戶態(tài)發(fā)生read操作發(fā)生時,它會經(jīng)歷兩個階段:
第一階段:用戶態(tài)線程等待內(nèi)核態(tài)的數(shù)據(jù)準(zhǔn)備 (Waiting for the data to be ready)。
-
第二階段:用戶態(tài)線程,將數(shù)據(jù)從內(nèi)核拷貝到進(jìn)程中 (Copying the data from the kernel to the process)。
第一步:通常涉及等待網(wǎng)絡(luò)上的數(shù)據(jù)分組到達(dá),然后被復(fù)制到內(nèi)核的某個緩沖區(qū)。
第二步:把數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到(用戶態(tài))應(yīng)用進(jìn)程緩沖區(qū)。網(wǎng)絡(luò)應(yīng)用處理的是兩大類問題:網(wǎng)絡(luò)IO、數(shù)據(jù)計算。前者給應(yīng)用帶來的性能瓶頸更大。
網(wǎng)絡(luò)IO的模型大致有如下幾種:
- 同步模型(synchronous IO)
- 阻塞IO模型(blocking IO)
- 非阻塞IO模型(non-blocking IO)
- 多路復(fù)用IO模型(multiplexing IO)
- 信號驅(qū)動IO模型(signal-driven IO)
- 異步IO(asynchronous IO)
阻塞IO模型(blocking IO)
在Linux中,默認(rèn)情況下所有的socket都是blocking,一個典型的讀操作流程如下:

當(dāng)用戶進(jìn)程調(diào)用了recvfrom這個系統(tǒng)調(diào)用,如上所述,會有兩個階段
準(zhǔn)備數(shù)據(jù):
很多時候數(shù)據(jù)在一開始還沒有到達(dá),這個時候kernel就要等待足夠的數(shù)據(jù)到來。而用戶進(jìn)程會一直阻塞。
當(dāng)kernel等到數(shù)據(jù)準(zhǔn)備好了,它會將數(shù)據(jù)從kernel中拷貝到用戶內(nèi)存,然后kernel返回,用戶進(jìn)程結(jié)束block狀態(tài),重新運行。
Blocking IO的特點就是IO執(zhí)行的兩個階段都是block了的。
非阻塞IO模型(non-blocking IO)[poll]
在Linux中,可以通過設(shè)置socket使其變?yōu)閚on-blocking,其流程如下:

當(dāng)用戶進(jìn)程調(diào)用了recvfrom這個系統(tǒng)調(diào)用,如果kernel中的數(shù)據(jù)還沒有準(zhǔn)備好,那么用戶進(jìn)程不會block而是立刻返回一個error,即從用戶的角度而言,不需要等待,馬上得到一個結(jié)果。
從圖中可以看出,用戶進(jìn)程在判斷結(jié)果是一個error后,了解到數(shù)據(jù)還沒有準(zhǔn)備好,于是就不斷重復(fù)上述操作直至kernel中的數(shù)據(jù)準(zhǔn)備好,然后它馬上將數(shù)據(jù)拷貝到了用戶內(nèi)存,然后返回。
多路復(fù)用IO模型(multiplexing IO)
select/epoll/evpoll,也被稱作是Event-Driven IO。好處是單個process可以同時處理多個網(wǎng)絡(luò)連接的IO。
基本原理可見下面的“IO復(fù)用技術(shù)”。也叫多路IO就緒通知。
這是一種進(jìn)程預(yù)先告知內(nèi)核的能力,讓內(nèi)核發(fā)現(xiàn)進(jìn)程指定的一個或多個IO條件就緒了,就通知用戶進(jìn)程。
使得一個進(jìn)程能在一連串的事件上等待。

這個流程和Blocking IO的流程其實并沒有太多不同,事實上僅從圖中看起來,由于需要進(jìn)行兩次系統(tǒng)調(diào)用,可能更差一些。但是,Select的優(yōu)勢在于它可以同時處理多個連接。
如果處理的連接數(shù)不是很高的話,使用“Select/Epoll 的 Web Server”不一定比使用“多線程 + BIO的Web Server”性能更好,反而延遲會更大。
Select/Epoll的優(yōu)勢并不是對于單個連接能處理得更快,而是在于能處理更多的連接。
在IO多路復(fù)用模型中,實際中,對于每一個socket,一般都設(shè)置成為non-blocking,但是,如上圖所示,整個用戶的process其實是一直被block的。只不過process是被select這個函數(shù)block,而不是被socket IO給block。
異步IO(asynchronous IO)
用戶進(jìn)程發(fā)起read操作之后,立刻就可以開始去做其它的事。

從kernel的角度,當(dāng)它受到一個asynchronous read之后,首先它會立刻返回,所以不會對用戶進(jìn)程產(chǎn)生任何block。
kernel會等待數(shù)據(jù)準(zhǔn)備完成,然后將數(shù)據(jù)拷貝到用戶內(nèi)存,當(dāng)這一切都完成之后,kernel會給用戶進(jìn)程發(fā)送一個signal,告訴它read操作完成了。
比較

非阻塞和異步的區(qū)別
經(jīng)過上面的介紹,會發(fā)現(xiàn)non-blocking IO和asynchronous IO的區(qū)別還是很明顯的。
- 在non-blocking IO中,雖然進(jìn)程大部分時間都不會被block,但是它仍然要求進(jìn)程去主動的check,并且當(dāng)數(shù)據(jù)準(zhǔn)備完成以后,也需要進(jìn)程主動的再次調(diào)用recvfrom來將數(shù)據(jù)拷貝到用戶內(nèi)存。
- asynchronous IO則完全不同。它就像是用戶進(jìn)程將整個IO操作交給了他人(kernel)完成,然后他人做完后發(fā)信號通知。在此期間,用戶進(jìn)程不需要去檢查IO操作的狀態(tài),也不需要主動的去拷貝數(shù)據(jù)。
IO復(fù)用技術(shù)
在IO編程過程中,當(dāng)需要處理多個請求時,可以使用多線程和IO復(fù)的方式進(jìn)行處理。
IO復(fù)用是什么?
把多個IO的阻塞復(fù)用到一個select之類的阻塞上,從而使得系統(tǒng)在單線程的情況下同時支持處理多個請求。
IO復(fù)用常見的應(yīng)用場景:
- 服務(wù)器需要同時處理多個處于監(jiān)聽狀態(tài)和多個連接狀態(tài)的套接字;
- 服務(wù)器需要處理多種網(wǎng)絡(luò)協(xié)議的套接字
- IO復(fù)用的實現(xiàn)方式目前主要有select、poll和epoll/evpoll。
select和poll的原理基本相同:
注冊待偵聽的fd(這里的fd創(chuàng)建時最好使用非阻塞)
每次調(diào)用都去檢查這些fd的狀態(tài),當(dāng)有一個或者多個fd就緒的時候返回
返回結(jié)果中包括已就緒和未就緒的fd
select和poll與epoll機(jī)制的比較
Linux網(wǎng)絡(luò)編程過程中,相比于select/poll,epoll是有著更明顯優(yōu)勢的一種選擇。
-
支持一個進(jìn)程打開的socket描述符不受限制(僅受限于操作系統(tǒng)的最大文件句柄數(shù) unlimit)。
Select的缺陷:一個進(jìn)程所打開的FD受限,默認(rèn)是2048;盡管數(shù)值可以更改,但同樣可能導(dǎo)致網(wǎng)絡(luò)效率下降;可以選擇多進(jìn)程的解決方案,但是進(jìn)程的創(chuàng)建本身代價不小,而且進(jìn)程間數(shù)據(jù)同步遠(yuǎn)比不上線程間同步的高效。
epoll所支持的FD上限是最大可以打開文件的數(shù)目:/proc/sys/fs/file-max
IO效率可能隨著文件描述符數(shù)目的增加而線性下降。
-
epoll掃描系統(tǒng)的機(jī)制不同
select/poll是線性掃描FD的集合;
epoll是根據(jù)FD上面的回調(diào)函數(shù)實現(xiàn)的,活躍的socket會主動去調(diào)用該回調(diào)函數(shù),其它socket則不會,相當(dāng)于市是一個AIO,只不過推動力在OS內(nèi)核。
使用mmap加速內(nèi)核與用戶空間的消息傳遞,zero-copy的一種。
epoll的API更加簡單。
-
IO復(fù)用還有一個 水平觸發(fā) 和 邊緣觸發(fā) 的概念:
- 水平觸發(fā):當(dāng)就緒的fd未被用戶進(jìn)程處理后,下一次查詢依舊會返回,這是select和poll的觸發(fā)方式。
- 邊緣觸發(fā):無論就緒的fd是否被處理,下一次不再返回。理論上性能更高,但是實現(xiàn)相當(dāng)復(fù)雜,并且任何意外的丟失事件都會造成請求處理錯誤。epoll默認(rèn)使用水平觸發(fā),通過相應(yīng)選項可以使用邊緣觸發(fā)。
IO模型的總結(jié)
最后,再舉幾個不是很恰當(dāng)?shù)睦觼碚f明這四個IO Model,有A,B,C,D四個人在釣魚:
A用的是最老式的魚竿,所以呢,得一直守著,等到魚上鉤了再拉桿;(同步阻塞)
B的魚竿有個功能,能夠顯示是否有魚上鉤,所以呢,B就和旁邊的MM聊天,隔會再看看有沒有魚上鉤,有的話就迅速拉桿;(非阻塞)
C用的魚竿和B差不多,但他想了一個好辦法,就是同時放好幾根魚竿,然后守在旁邊,一旦有顯示說魚上鉤了,它就將對應(yīng)的魚竿拉起來;(io多路復(fù)用機(jī)制)
D是個有錢人,干脆雇了一個人幫他釣魚,一旦那個人把魚釣上來了,就給D發(fā)個短信。(異步機(jī)制)