??作者:一只大喵咪1201
??專欄:《網(wǎng)絡(luò)》
??格言:你只管努力,剩下的交給時(shí)間!
??五種IO模型
在學(xué)習(xí)系統(tǒng)部分的時(shí)候,本喵就講解過(guò)IO,當(dāng)時(shí)我們學(xué)習(xí)的IO就是從文件中讀數(shù)據(jù)和寫(xiě)數(shù)據(jù),到了后來(lái)學(xué)習(xí)網(wǎng)絡(luò)的時(shí)候,我們知道,從網(wǎng)絡(luò)中讀取和寫(xiě)入數(shù)據(jù)也是IO,那么IO到底是什么呢?今天我們來(lái)更深刻的認(rèn)識(shí)一下IO。
就拿讀取數(shù)據(jù)來(lái)說(shuō),無(wú)論是調(diào)用read
還是recv
,在文件描述符所指向的struct file
中的接收緩沖區(qū)如果沒(méi)有數(shù)據(jù)的時(shí)候,都會(huì)阻塞等待。
當(dāng)緩沖區(qū)中有數(shù)據(jù)后,才會(huì)進(jìn)行讀取,所謂讀取,本質(zhì)就是在拷貝,就是將內(nèi)核緩沖區(qū)中的數(shù)據(jù)拷貝到用戶緩沖區(qū)中供用戶去使用。
無(wú)論是等待還是拷貝,都是讀取的過(guò)程,二者缺一不可。
- IO = 等 + 數(shù)據(jù)拷貝
那么什么是高效的IO呢?我們知道,IO過(guò)程中我們?cè)谝獾氖强截?,而不是等待,由于各種技術(shù)的發(fā)展,拷貝所花費(fèi)的時(shí)間幾乎是固定的,是由電路或者系統(tǒng)等等機(jī)制來(lái)保證的,所以拷貝的效率已經(jīng)很難再有提升了。
- 高效的IO = 減少等待的比重
IO的種類有五種,下面本喵給大家介紹一下:
- 阻塞IO:在內(nèi)核將數(shù)據(jù)準(zhǔn)備好之前,系統(tǒng)調(diào)用會(huì)一直等待,所有的套接字以及文件,默認(rèn)都是阻塞方式。
如上圖所示便是阻塞IO的示意圖,在進(jìn)程調(diào)用recvfrom
從內(nèi)核緩沖區(qū)中讀取數(shù)據(jù)時(shí),如果數(shù)據(jù)沒(méi)有準(zhǔn)備好,進(jìn)程就會(huì)阻塞在調(diào)用處等待,直到數(shù)據(jù)準(zhǔn)備好,才會(huì)將內(nèi)核緩沖區(qū)的數(shù)據(jù)拷貝到用戶緩沖區(qū),并且給進(jìn)程返回值。
- 阻塞IO是最常見(jiàn)的IO模型,也是最簡(jiǎn)單的IO模型,我們之前寫(xiě)的所有IO都是阻塞式的。
- 非阻塞IO:如果內(nèi)核還未將數(shù)據(jù)準(zhǔn)備好,系統(tǒng)調(diào)用仍然會(huì)直接返回,并且返回
EAGAN
或者EWOULDBLOCK
錯(cuò)誤碼。
如上圖所示,進(jìn)程調(diào)用recvfrom
從內(nèi)核緩沖區(qū)中讀取數(shù)據(jù)時(shí),即使數(shù)據(jù)沒(méi)有準(zhǔn)備好,仍然會(huì)給進(jìn)程一個(gè)返回一個(gè)EAGAN
或者EWOULDBLOCK
。
通常情況下使用這種IO方式時(shí),如果返回EAGAN
或者EWOULDBLOCK
,說(shuō)明數(shù)據(jù)沒(méi)有準(zhǔn)備好,就會(huì)再次調(diào)用recvfrom
去讀取數(shù)據(jù),如此反復(fù),直到數(shù)據(jù)準(zhǔn)備好并且完成拷貝,最后返回表示成的返回值。
- 非阻塞IO需要程序員以循環(huán)的方式反復(fù)嘗試讀寫(xiě)文件描述符,這個(gè)過(guò)程稱為輪詢。
- 這對(duì)CPU來(lái)說(shuō)是較大的浪費(fèi),一般只有特定場(chǎng)景下才使用。
默認(rèn)情況下,文件描述符fd指向的struct file
中的緩沖區(qū)的阻塞式IO,所以我們前面無(wú)論是在進(jìn)行文件操縱還是使用套接字的時(shí)候,都是阻塞IO。
如上圖所示系統(tǒng)調(diào)用fcntl
,可以使用它將fd所指向的文件改成非阻塞IO模式,它的參數(shù)是一個(gè)可變參數(shù)。
- int fd:要修改文件的文件描述符fd。
- 返回值fl:大于0表示當(dāng)前文件的狀態(tài),小于0表示調(diào)用失敗。
- int cmd:對(duì)fd所指向的文件要進(jìn)行的操作,可以傳遞兩個(gè)參數(shù):
F_GETFL
:用來(lái)獲取該文件當(dāng)前的狀態(tài)。F_SETFL
:用來(lái)設(shè)置該文件當(dāng)前的狀態(tài)。
- 可變參數(shù)部分:可以傳遞參數(shù)有O_RDONLY,O_WRONLY等等,最重要的是
O_NONBLOCK
非阻塞IO方式,但是在傳參的時(shí)候,必須和fl
進(jìn)行按位或,如fl | O_NONBLOCK
。
如上圖所示代碼是本喵寫(xiě)的一個(gè)工具函數(shù),作用就是將指定fd
所代表的文件設(shè)置成非阻塞IO方式。
如上圖,從內(nèi)核中的代碼可以看到,O_NONBLOCK
是一個(gè)宏,僅僅代表著一個(gè)比特位的狀態(tài),所以在傳參時(shí),需要使用fl | O_NONBLOCK
的方式,在文件原有的狀態(tài)上增加非阻塞。
再重新來(lái)看一下read
系統(tǒng)調(diào)用:
如上圖所示為man
手冊(cè)中的描述,調(diào)用失敗以后,返回-1,并且錯(cuò)誤碼被設(shè)置。
- 設(shè)置不同的錯(cuò)誤碼代表著不同的意義。
如上圖所示,可以設(shè)置的錯(cuò)誤碼有這么多,雖然返回值是-1,但是不同的錯(cuò)誤碼代表著不同的情況,其中EAGAIN
或者EWOULDBLOCK
表示的就是數(shù)據(jù)沒(méi)有準(zhǔn)備好,需要稍后再讀。
- 讀取數(shù)據(jù)時(shí),內(nèi)核中緩沖區(qū)的數(shù)據(jù)沒(méi)有準(zhǔn)備好并沒(méi)有錯(cuò),這是很正常的情況。
- 所以錯(cuò)誤碼
EAGAIN
并不表示錯(cuò)誤了,只是表示數(shù)據(jù)暫時(shí)沒(méi)有準(zhǔn)備好,需要稍后再來(lái)讀取。
如上圖所示,EAGAIN
和EWOULDBLOCK
都是宏,而且它們的值都是11,本質(zhì)上是一個(gè)東西。
如上圖代碼所示,使用本喵寫(xiě)的工具serNonBlock
將標(biāo)準(zhǔn)輸入,也就是文件描述符為0的文件設(shè)置成非阻塞IO模式,然后運(yùn)行。
如上圖所示,可以看到,輸入提示符>>>
在不停打印,在本喵從鍵盤(pán)上輸入的時(shí)候,仍然在打印,雖然輸入的內(nèi)容被>>>
分隔開(kāi)了,但是輸入完成以后,回顯echo
的內(nèi)容卻是完整的,仍然是helloworld
。
- 輸入和輸出互不影響,因?yàn)橛休斎刖彌_區(qū)和輸出緩沖區(qū),二者是相互獨(dú)立的。
如上圖所示便是默認(rèn)情況下的阻塞IO,可以看到,輸入和輸出輪替進(jìn)行著,在本喵沒(méi)有輸入的情況下,該進(jìn)程是阻塞等待的。
如上圖所示,可以在else
情況,也就是返回值s小于0的時(shí)候再進(jìn)行具體的判斷,如果錯(cuò)誤碼errno
的值是EAGAIN
說(shuō)明只是數(shù)據(jù)沒(méi)有準(zhǔn)備好,并不是出錯(cuò)了,在等待數(shù)據(jù)就緒的過(guò)程中可以執(zhí)行其他任務(wù)。
如上圖所示,此時(shí)進(jìn)程并沒(méi)有阻塞,而是在輪詢,數(shù)據(jù)沒(méi)有準(zhǔn)備好就執(zhí)行其他任務(wù)來(lái)打發(fā)時(shí)間。
- 信號(hào)驅(qū)動(dòng)IO:內(nèi)核將數(shù)據(jù)準(zhǔn)備好的時(shí)候,使用
SIGIO
信號(hào)通知應(yīng)用程序進(jìn)行IO操作。
如上圖所示信號(hào)驅(qū)動(dòng)IO模型,該模式調(diào)用recvfrom
并不是在主進(jìn)程中調(diào)用,而且使用signal
注冊(cè)信號(hào)處理函數(shù),在信號(hào)處理函數(shù)中調(diào)用recvfrom
。
進(jìn)程一直在正常運(yùn)行,執(zhí)行自己的邏輯,當(dāng)內(nèi)核接收緩沖區(qū)有數(shù)據(jù)到來(lái)時(shí),進(jìn)程會(huì)收到系統(tǒng)給發(fā)的信號(hào)SIGIO
,然后就會(huì)調(diào)用該信號(hào)注冊(cè)的處理方式,在信號(hào)處理方式中調(diào)用recvfrom
讀取緩沖區(qū)中的數(shù)據(jù)。
- 一旦進(jìn)入信號(hào)處理函數(shù)中,說(shuō)明內(nèi)核緩沖區(qū)中的數(shù)據(jù)已經(jīng)準(zhǔn)備好了,在這里只需要讀取,而不再需要等待。
- IO多路轉(zhuǎn)接:雖然從流程圖上看起來(lái)和阻塞IO類似,實(shí)際上最核心在于IO多路轉(zhuǎn)接能夠同時(shí)等待多個(gè)文件描述符的就緒狀態(tài)。
如上圖IO多路轉(zhuǎn)接模式,該模式中,將IO的等待和拷貝兩個(gè)步驟分開(kāi)了。
進(jìn)程調(diào)用select
系統(tǒng)調(diào)用來(lái)等待內(nèi)核緩沖區(qū)中數(shù)據(jù)就緒,當(dāng)數(shù)據(jù)就緒以后通知進(jìn)程調(diào)用recvfrom
來(lái)將數(shù)據(jù)拷貝到用戶緩沖區(qū)中。
這樣看起來(lái)和信號(hào)驅(qū)動(dòng)的IO模式?jīng)]有什么區(qū)別,但是多路轉(zhuǎn)接的優(yōu)勢(shì)在于可以同時(shí)等待多個(gè)文件描述符所指向的文件。
- IO多路轉(zhuǎn)接模式下,可以同時(shí)等待多個(gè)文件,當(dāng)一個(gè)或者多個(gè)文件的緩沖區(qū)中數(shù)據(jù)就緒時(shí),就會(huì)通知上層用戶來(lái)讀取。
此時(shí)雖然也有等,但是等的比重就降低了,因?yàn)槟軌蛞淮蔚却鄠€(gè)文件,進(jìn)而讓上層用戶一次來(lái)讀取多個(gè)緩沖區(qū)中的數(shù)據(jù),拷貝的比重就增加了,從而提高了IO的效率。
- 異步IO:由內(nèi)核在數(shù)據(jù)拷貝完成時(shí),通知應(yīng)用程序直接去用戶緩沖區(qū)中使用數(shù)據(jù)。
- 同步和異步關(guān)注的是消息通信機(jī)制:
- 所謂同步,就是在發(fā)出一個(gè)調(diào)用時(shí),在沒(méi)有得到結(jié)果之前,該調(diào)用就不返回,但是一旦調(diào)用返回,就得到返回值了。
- 也就是由調(diào)用者主動(dòng)等待這個(gè)調(diào)用的結(jié)果。
異步則是相反,調(diào)用在發(fā)出之后,這個(gè)調(diào)用就直接返回了,所以沒(méi)有返回結(jié)果。
- 當(dāng)一個(gè)異步過(guò)程調(diào)用發(fā)出后,調(diào)用者不會(huì)立刻得到結(jié)果,而是在調(diào)用發(fā)出后,被調(diào)用者通過(guò)狀態(tài)、信號(hào)等來(lái)通知調(diào)用者,或通過(guò)回調(diào)函數(shù)處理這個(gè)調(diào)用。
如上圖所示異步IO模式示意圖,進(jìn)程調(diào)用aio_read
,將等待數(shù)據(jù)就緒和將數(shù)據(jù)拷貝到用戶緩沖區(qū)兩個(gè)步驟的工作全部交給操作系統(tǒng)來(lái)完成。
當(dāng)操作系統(tǒng)完成兩個(gè)步驟以后,通知上層用戶直接去用戶緩沖區(qū)中使用數(shù)據(jù)即可。
信號(hào)驅(qū)動(dòng)是告訴進(jìn)程可以從內(nèi)核緩沖區(qū)中拷貝數(shù)據(jù)到用戶緩沖區(qū)了,而異步IO連拷貝這一步也不用做了,直接使用數(shù)據(jù)。
- 另外,我們回憶在學(xué)習(xí)多進(jìn)程多線程的時(shí)候,也提到同步和互斥,這里的同步通信和進(jìn)程之間的同步是完全不同的概念。
- 進(jìn)程/線程同步也是進(jìn)程/線程之間直接的制約關(guān)系,是為完成某種任務(wù)而建立的兩個(gè)或多個(gè)線程,這個(gè)線程需要在某些位置上協(xié)調(diào)他們的工作次序而等待、傳遞信息所產(chǎn)生的制約關(guān)系。尤其是在訪問(wèn)臨界資源的時(shí)候。
比較這五種IO模式,阻塞IO模式肯定是效率最低的,非阻塞以及信號(hào)驅(qū)動(dòng)的IO模式,同樣需要參加IO的等待和拷貝兩個(gè)過(guò)程,對(duì)于IO的效率是相同的。
異步IO模式也是同樣的道理,雖然等待和拷貝不是由線程去做的,而是由操作系統(tǒng)在做,但是線程也得在等待和拷貝完成后才能使用數(shù)據(jù),所以IO的效率還是沒(méi)有提高的。
而多路轉(zhuǎn)接不一樣,雖然等待和拷貝兩個(gè)過(guò)程都參與,但是等待時(shí)可以一次等待多個(gè)文件描述符,拷貝時(shí)也是可以拷貝多個(gè)文件描述符中的數(shù)據(jù)。
- 由于等待的文件描述符數(shù)量多,所以有數(shù)據(jù)就緒的概率就高,進(jìn)行拷貝也更加頻繁。
- 站在上帝視角來(lái)看,多路轉(zhuǎn)接模式下,進(jìn)程在單位時(shí)間內(nèi)進(jìn)行拷貝的次數(shù)要比其他幾種模式多。
所以多路轉(zhuǎn)接更加高效,而我們研究的重點(diǎn)也在多路轉(zhuǎn)接模式上,主要有select
,poll
,epoll
三種方式。
??select
??認(rèn)識(shí)接口
如上圖所示,該系統(tǒng)調(diào)用有5個(gè)參數(shù)。
- int nfds:要等待的所有文件描述符中的最大
fd+1
。
假設(shè)現(xiàn)在要等待的文件有3個(gè),文件描述符分別是3,4,7,則傳參時(shí)就需要傳7+1=8
給nfds。
- fd_set* reads:等待讀取就緒文件描述符的位圖。
select
等待的文件有不同的事件會(huì)就緒,比如讀取就緒,寫(xiě)就緒,錯(cuò)誤就緒等等。
如上圖所示是fd_set
類型在內(nèi)核中的定義,可以看到它就是一個(gè)位圖。它有多個(gè)比特位,每一個(gè)比特位代表一個(gè)文件描述符,比特位的狀態(tài)表示該比特位是否被監(jiān)聽(tīng)。
如上圖所示便是fd_set
位圖示意圖,其中比特位的順序由低到高從左向右,比特位的下標(biāo)就是代表文件描述符fd,比特位的內(nèi)容表示狀態(tài),這張圖中,文件描述符為1,3,1023的三個(gè)文件描述符需要被select
監(jiān)視。
如上圖所示,使用sizeof
得到fd_set
類型的大小是8字節(jié),所以有1024個(gè)比特位,這意味著使用select
最多監(jiān)視1024個(gè)文件描述符。
將接收緩沖區(qū)所在文件的文件描述符設(shè)置到readfds
中,當(dāng)該緩沖區(qū)有數(shù)據(jù)到來(lái)時(shí)(讀就緒),select
就會(huì)通知上層進(jìn)程去讀取該緩沖區(qū)中的數(shù)據(jù)。
- fd_set* writefds:等待寫(xiě)入就緒文件描述符所在位圖。
該位圖和readfds
一樣,只是操作系統(tǒng)等待的事件由讀就緒變成了寫(xiě)就緒,當(dāng)發(fā)送緩沖區(qū)空了以后(寫(xiě)就緒),操作系統(tǒng)就會(huì)通知上層進(jìn)程向該緩沖區(qū)中寫(xiě)入數(shù)據(jù)。
- fd_set* exceptfds:等待異常就緒文件描述符所在位圖。
打開(kāi)的文件,如TCP中的套接字,當(dāng)對(duì)端關(guān)閉套接字以后,自己這邊的套接字就會(huì)出現(xiàn)異常,此時(shí)操作系統(tǒng)就會(huì)通知上層進(jìn)程處理該異常。
需要操作系統(tǒng)監(jiān)視哪里事件就將對(duì)應(yīng)的位圖傳給select
,待事件就緒后就會(huì)通知上層進(jìn)程去處理,如果不需要監(jiān)視直接設(shè)置成nullptr
就行。
- struct timeval* timeout:設(shè)置等待方式。
如上圖所示struct timeval
類型的定義,有兩個(gè)成員,第一個(gè)是秒,第二個(gè)是微秒。
該參數(shù)傳入nullptr
的時(shí)候,select
是阻塞等待,只有當(dāng)一個(gè)或者多個(gè)文件描述符就緒時(shí)才會(huì)通知上層進(jìn)程去讀取數(shù)據(jù)。
該參數(shù)如果是struct timeval timeout = {0, 0}
,時(shí)間設(shè)置成0,select
是非阻塞等待,就需要使用輪詢的方式。
該參數(shù)如果設(shè)置了具體值,如struct timeval timeout = {5, 0}
,時(shí)間設(shè)置成5,select
在5秒內(nèi)阻塞等待,如果在5秒內(nèi)有文件描述符就緒,則通知上層,如果沒(méi)有文件描述符就緒則超時(shí)返回。
- 返回值:
ret > 0
表示有ret個(gè)fd就緒了,ret == 0
表示超時(shí)返回,ret < 0
表示select
調(diào)用失敗了。
為了使用方便,內(nèi)核還提供了一組宏供用戶去設(shè)置fd_set
位圖,將對(duì)應(yīng)的文件描述符設(shè)置進(jìn)去,避免了我們自己使用按位或等運(yùn)算給位圖賦值的麻煩:
如上圖所示,FD_CLR
是將位圖中指定文件描述符fd所對(duì)應(yīng)位的狀態(tài)清零,表示不用操作系統(tǒng)再等待該文件。
FD_ISSET
是用來(lái)判斷特定文件描述符fd是否被設(shè)置進(jìn)了位圖,如果設(shè)置返回1,沒(méi)有則返回0。
FD_SET
是用來(lái)將指定文件描述符fd對(duì)應(yīng)的位圖設(shè)置為1,表示需要操作系統(tǒng)等待該文件。
FD_ZERO
是用來(lái)將整個(gè)位圖清空的,此時(shí)操作系統(tǒng)不等待任何一個(gè)文件。
select
的五個(gè)參數(shù)中,后面四個(gè)都是輸入輸出型參數(shù),都是傳的指針,意味著操作系統(tǒng)和用戶共用一個(gè)參數(shù)。
調(diào)用select
傳參時(shí),表示用戶告訴內(nèi)核,需要操作系統(tǒng)等待哪個(gè)文件,以及哪種事件。操作系統(tǒng)第一時(shí)間會(huì)將傳入的位圖參數(shù)清空,每就緒一個(gè)文件,傳入位圖相應(yīng)比特位置一,然后傳返回值給用戶。
此時(shí)就是內(nèi)核在告訴用戶,你關(guān)心的多個(gè)fd,有哪些已經(jīng)就緒了。
用戶根據(jù)返回值來(lái)處理調(diào)用結(jié)果,如果是ret>0
,則判斷自己當(dāng)初設(shè)置進(jìn)位圖中的文件描述符是否被置一了,如果置一了,說(shuō)明該文件所對(duì)應(yīng)的事件就緒了,用戶就可以進(jìn)行進(jìn)一步處理。
- 由于傳入的參數(shù)是輸入輸出型的,操作系統(tǒng)等待后的結(jié)果也是在這個(gè)參數(shù)中,所以用戶必須自己再維護(hù)一個(gè)數(shù)據(jù)結(jié)構(gòu)來(lái)記錄自己最初設(shè)置要等待的文件描述符。
- 根據(jù)
select
返回狀態(tài)以及操作系統(tǒng)修改后的位圖,與自己維護(hù)的記錄結(jié)構(gòu)作對(duì)比,得出自己當(dāng)初要等待的文件是否就緒的結(jié)論。
無(wú)論是讀取數(shù)據(jù),還是寫(xiě)入數(shù)據(jù),再或者是異常事件,都是采用這樣的方式和機(jī)制。
struct timeval* timeout
表示的阻塞等待時(shí)間,操作系統(tǒng)內(nèi)部也會(huì)進(jìn)行修改,假設(shè)初值是{5, 0}
表示阻塞等待5s中,如果5s內(nèi)沒(méi)有事件就緒則select
返回0,表示超時(shí)返回,此時(shí)原本傳入的time
的值也變成了{0, 0}
。
如果5s內(nèi)有事件就緒,假設(shè)操作系統(tǒng)等待了3秒,selsect
返回值大于0,表示不是超時(shí)返回,有事件就緒,此時(shí)原本傳入的time
的值也變成了{2, 0}
,表示設(shè)置的阻塞事件還剩兩秒。
- 所以在輪詢過(guò)程中,每循環(huán)一次就需要重新設(shè)置一下時(shí)間,否則第一次超時(shí)返回后,
time
的值就成了0,之后select
就成了非阻塞IO,與初衷不符。
- 輸入輸出型參數(shù)的作用就是讓用戶和內(nèi)核之間相互溝通,互相知曉對(duì)方要關(guān)心的。
??簡(jiǎn)易select服務(wù)器
本喵將創(chuàng)建套接字的代碼封裝成了一個(gè)Sock
類,使用的時(shí)候直接調(diào)用即可,同樣可以將其作為一個(gè)小組件。
如上圖上,Sock
類中包含套接字的創(chuàng)建,綁定,監(jiān)聽(tīng),以及獲取新連接等四個(gè)方法,這些函數(shù)中還包含日志信息,具體的日志代碼本喵就不貼了,有興趣的小伙伴自行去查閱日志功能。
如上圖所示便是select
簡(jiǎn)易服務(wù)器的基本配置,成員變量包含監(jiān)聽(tīng)套接字,端口號(hào),用戶維護(hù)的用于設(shè)置等待位圖的數(shù)組_fdarry
,以及回調(diào)函數(shù)。
定義了幾個(gè)全局默認(rèn)變量,包括默認(rèn)端口號(hào),select
所能等待文件的上限1024
,以及用戶維護(hù)數(shù)組的初始默認(rèn)值。
- initServer():初始化服務(wù)器
如上圖所示,第一步便是初始化服務(wù)器,在系統(tǒng)中創(chuàng)建套接字,進(jìn)行綁定和監(jiān)聽(tīng),構(gòu)建完整的會(huì)話層。之后將用戶維護(hù)的用來(lái)設(shè)置等待位圖的數(shù)組進(jìn)行初始化,大小是1024個(gè)元素,初始值都是-1。
- 0,1,2文件描述符不用
select
等待,從listen
套接字開(kāi)始進(jìn)行等待。
- Start():?jiǎn)?dòng)服務(wù)器
如上圖所示,服務(wù)器在啟動(dòng)后,是一個(gè)while(1)
的死循環(huán),在這個(gè)循環(huán)中進(jìn)行一遍又一遍的輪詢。
由于select
的后面四個(gè)參數(shù)都是輸入輸出型參數(shù),所以每一次輪詢之前都需要重新設(shè)置一遍,否則就會(huì)因?yàn)椴僮飨到y(tǒng)的修改而出現(xiàn)問(wèn)題。需要重新設(shè)置位圖狀態(tài),重新設(shè)置等待時(shí)間等等。
- 每次輪詢的時(shí)候,都需要將用戶維護(hù)的數(shù)組中需要等待的文件描述符重新設(shè)置到
fd_set
位圖中。
在將參數(shù)設(shè)置好以后便調(diào)用select
系統(tǒng)調(diào)用,用switch
將返回的情況分別處理,當(dāng)n>0
時(shí),說(shuō)明有文件就緒,可以去讀取,所以調(diào)用HandlerReadEvent
函數(shù)去處理該事件。
事件處理函數(shù):
如上圖所示,當(dāng)執(zhí)行到該函數(shù)的時(shí)候,必有套接字就緒,此時(shí)用戶要做的就是判斷到底是哪個(gè)套接字上事件就緒。
第一次執(zhí)行該函數(shù)的時(shí)候,必然是監(jiān)聽(tīng)套接字就緒,因?yàn)楫?dāng)前select
等待的只有這一個(gè)套接字,所以判斷符合條件以后,去執(zhí)行Accepter
函數(shù)來(lái)獲取新連接。
- 監(jiān)聽(tīng)套接字就緒也是屬于讀事件就緒,因?yàn)榇藭r(shí)有新連接到來(lái),需要用戶去將新連接讀取走。
如上圖所示,當(dāng)監(jiān)聽(tīng)套接字就緒后,用戶第一時(shí)間肯定就是讀取監(jiān)聽(tīng)到的新連接,并且獲取客戶端的IP地址以及端口號(hào)。
新連接獲取成功后,由于此時(shí)沒(méi)有客戶端的數(shù)據(jù)到來(lái),所以新連接套接字沒(méi)有就緒。
- 此時(shí)必須將新連接套接字交給
select
去等待就緒,所以需要向用戶層維護(hù)的數(shù)組中添加該套接字的文件描述符。- 如果此時(shí)直接讀取新連接套接字的話,由于沒(méi)有就緒,就會(huì)阻塞在這里,服務(wù)器無(wú)法繼續(xù)執(zhí)行下去,與我們的初衷就不符合了。
為了方便,本喵還增加了一個(gè)Print
函數(shù),打印新增加的需要select
等待的文件描述符。
第一次之后調(diào)用Accepter
函數(shù)時(shí),已經(jīng)就緒的文件就不一定是監(jiān)聽(tīng)套接字了,有可能是前面獲取的新連接套接字,所以就需要進(jìn)行具體判斷到底是哪個(gè)套接字就緒了。當(dāng)新連接套接字就緒后,就調(diào)用Recver
從套接字中讀取客戶端的請(qǐng)求。
如上圖所示,當(dāng)新連接套接字就緒以后,首先要進(jìn)行的就是讀取套接字中客戶端發(fā)來(lái)的數(shù)據(jù),使用recv
獲取數(shù)據(jù)。
- 當(dāng)前這種讀取數(shù)據(jù)的方式是存在一定問(wèn)題的,因?yàn)椴荒鼙WC套接字中的數(shù)據(jù)是否被讀完了。
但是此時(shí)并不會(huì)產(chǎn)生影響,所以暫且這樣,后面本喵會(huì)制定具體的協(xié)議去處理它。
如果讀取數(shù)據(jù)出現(xiàn)了問(wèn)題,或者套接字出現(xiàn)異常,那么就需要關(guān)閉套接字,并且將文件描述符從用戶維護(hù)的數(shù)組中清除。
讀取成功后將請(qǐng)求處理,并且構(gòu)建響應(yīng)發(fā)送給客戶端,當(dāng)前的select
服務(wù)端的重點(diǎn)在于讀取,所以寫(xiě)入本喵這里就不詳細(xì)寫(xiě)了。
- selectServer.cpp
如上圖所示就是selectServer.cpp
中的代碼,服務(wù)器的回調(diào)函數(shù)中僅是將客戶端發(fā)送來(lái)的數(shù)據(jù)原封不動(dòng)的響應(yīng)給客戶端,沒(méi)有做其他處理,其他部分代碼本喵不再解釋。
如上圖所示,當(dāng)服務(wù)器開(kāi)始運(yùn)行后,由于此時(shí)沒(méi)有新連接到來(lái),所以監(jiān)聽(tīng)套接字一直處于未就緒的狀態(tài),前面在設(shè)置等待事件的時(shí)候設(shè)置了5s,所以每隔5s就超時(shí)返回一次。
- 可以看到當(dāng)前最大文件描述符是3,也就是監(jiān)聽(tīng)套接字的文件描述符。
如上圖所示,為了避免超時(shí)返回的干擾,本喵將select
的最后一個(gè)參數(shù)也設(shè)置成nullptr
此時(shí)select
就是阻塞等待,根據(jù)運(yùn)行結(jié)果也可以看成是阻塞不動(dòng)的。
如上圖所示,此時(shí)使用兩個(gè)telnet
連接客戶端,可以看到服務(wù)端提示有新事件就緒,此時(shí)是監(jiān)聽(tīng)套接字監(jiān)聽(tīng)到了兩個(gè)客戶端連接,所以顯示兩次,服務(wù)端每處理一次,用戶層維護(hù)的數(shù)組中文件名描述符就會(huì)增加一個(gè),所以最終是3,4,5
三個(gè)文件需要select
進(jìn)行等待。
兩個(gè)客戶端向服務(wù)端發(fā)送消息的時(shí)候,服務(wù)端顯示事件就緒,這是進(jìn)行數(shù)據(jù)通信的套接字上有數(shù)據(jù)到來(lái),提醒用戶層去讀取數(shù)據(jù)。
- 服務(wù)器上只有一個(gè)進(jìn)程(線程),客戶端有多個(gè),此時(shí)服務(wù)端可以同時(shí)接收多個(gè)客戶端的連接請(qǐng)求和數(shù)據(jù)。
- 多路轉(zhuǎn)接實(shí)現(xiàn)了我們之前只能通過(guò)多進(jìn)程或者多線程才能實(shí)現(xiàn)的功能,而且效率非常高。
??select的特點(diǎn)
-
select
能同時(shí)等待的文件fd是有上限的,除非重新修改內(nèi)核,否則無(wú)法解決。
fd_set
位圖大小只有1024個(gè)比特位,意味著select
同時(shí)最多只能等待1024個(gè)文件描述符。
可以看到,本喵的Linux機(jī)器上,最多可以打開(kāi)100001個(gè)文件,遠(yuǎn)大于1024,所以說(shuō)select
能夠同時(shí)等待的文件數(shù)量在高訪問(wèn)量的服務(wù)器中是遠(yuǎn)遠(yuǎn)不夠的。
- 必須借助第三方數(shù)組等結(jié)構(gòu)來(lái)維護(hù)需要
select
等待的文件描述符fd。
由于select
的后四個(gè)參數(shù)都是輸入輸出型參數(shù),操作系統(tǒng)也會(huì)修改這幾個(gè)參數(shù),這就導(dǎo)致用戶層必須自己維護(hù)一個(gè)數(shù)組來(lái)記錄自己曾經(jīng)想要讓select
等待的文件描述符。
并且每輪詢一次就需要重新設(shè)置一次fd_set
位圖,不僅繁瑣,而且對(duì)于效率相對(duì)較低。
-
select
存在遍歷成本。
在上面本喵實(shí)現(xiàn)的服務(wù)器代碼中,對(duì)于用戶層來(lái)說(shuō),存在多處遍歷所維護(hù)的數(shù)組。
在將文件描述符設(shè)置到fd_set
位圖中的時(shí)候,需要遍歷數(shù)組中的所有元素來(lái)找到合法的fd設(shè)置到位圖中。
在select
完成等待后,同樣需要遍歷一次數(shù)組,來(lái)確定是哪個(gè)fd的事件就緒了,然后再去處理。
- 內(nèi)核也要進(jìn)行遍歷
select
的第一個(gè)參數(shù)傳參時(shí)傳入的是最大文件描述符fd + 1
,這個(gè)參數(shù)就是為了讓內(nèi)核確定遍歷的范圍,這個(gè)值之前的所有文件描述符,操作系統(tǒng)都會(huì)進(jìn)行查看,看看是否有事件就緒。
- 內(nèi)核和用戶層來(lái)回拷貝的成本問(wèn)題
select
采用的是位圖來(lái)標(biāo)記哪個(gè)文件的事件就緒,無(wú)論是用戶層告訴內(nèi)核,還是內(nèi)核告訴用戶層,都需要拷貝,這樣來(lái)回進(jìn)行數(shù)據(jù)拷貝也是有很大的成本。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-685255.html
??總結(jié)
多路轉(zhuǎn)接是一個(gè)非常重要的IO模式,而select
方式只最基礎(chǔ)的一種,它主要的作用是帶領(lǐng)我們理解多路轉(zhuǎn)接,后面本喵還會(huì)講解poll
和epoll
方式。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-685255.html
到了這里,關(guān)于【網(wǎng)絡(luò)】多路轉(zhuǎn)接——五種IO模型 | select的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!