網(wǎng)絡(luò)IO模型優(yōu)化
最開(kāi)始的阻塞式IO,它在每一個(gè)連接創(chuàng)建時(shí),都需要一個(gè)用戶(hù)線(xiàn)程來(lái)處理,并且在IO操作沒(méi)有就緒或者結(jié)束時(shí),線(xiàn)程被掛起,進(jìn)入阻塞等待狀態(tài),阻塞式IO就成為導(dǎo)致性能瓶頸的根本原因。

阻塞式發(fā)生在那些環(huán)節(jié)呢?
- 首先,應(yīng)用程序通過(guò)系統(tǒng)調(diào)用socket創(chuàng)建一個(gè)套接字,他是分配給應(yīng)用程序的一個(gè)文件描述符
- 其次,應(yīng)用程序會(huì)通過(guò)系統(tǒng)調(diào)用bind,綁定地址和端口號(hào),給套接字命名一個(gè)名稱(chēng)
- 然后,系統(tǒng)會(huì)調(diào)用listen創(chuàng)建一個(gè)隊(duì)列用于存放客戶(hù)端進(jìn)來(lái)的請(qǐng)求
- 最后,應(yīng)用服務(wù)會(huì)通過(guò)系統(tǒng)嗲用accept來(lái)監(jiān)聽(tīng)客戶(hù)端的連接請(qǐng)求
當(dāng)有一個(gè)客戶(hù)端連接到服務(wù)端之后,服務(wù)端就會(huì)調(diào)用fork創(chuàng)建一個(gè)子進(jìn)程,通過(guò)系統(tǒng)調(diào)用read監(jiān)聽(tīng)客戶(hù)端發(fā)來(lái)的消息,再通過(guò)write向客戶(hù)端返回信息。
1. 阻塞式IO
在整個(gè)socket通信工作流程中,socket的默認(rèn)狀態(tài)是阻塞的。也就是說(shuō),當(dāng)發(fā)出一個(gè)不能立即完成的套接字調(diào)用時(shí),其進(jìn)程將被阻塞,被系統(tǒng)掛起,進(jìn)入睡眠狀態(tài),一直等待響應(yīng)的操作響應(yīng)。
connect阻塞:
當(dāng)客戶(hù)端發(fā)起TCP請(qǐng)求,通過(guò)系統(tǒng)調(diào)用connect函數(shù),TCP連接的建立需要完成三次握手過(guò)程,客戶(hù)端需要等待服務(wù)端發(fā)送過(guò)來(lái)的ACK以及SYN信號(hào),同樣服務(wù)端也需要阻塞等待客戶(hù)端連接的ACK信號(hào),這就意味著會(huì)阻塞等待,直到確認(rèn)連接。

accept阻塞:
一個(gè)阻塞的socket通信的服務(wù)端接收外來(lái)連接,會(huì)調(diào)用accept函數(shù),如果沒(méi)有新的連接到達(dá),調(diào)用進(jìn)程將被掛起,進(jìn)入阻塞狀態(tài)。

read、write阻塞
當(dāng)一個(gè)socket連接創(chuàng)建成功之后,服務(wù)端用fork函數(shù)創(chuàng)建一個(gè)子進(jìn)程,調(diào)用read函數(shù)等待客戶(hù)端的數(shù)據(jù)寫(xiě)入,如果沒(méi)有數(shù)據(jù)寫(xiě)入,調(diào)用子進(jìn)程將被掛起,進(jìn)入阻塞狀態(tài)。

2. 非阻塞式IO
- 使用fcntl可以把以上三個(gè)操作都設(shè)置為非阻塞操作。如果沒(méi)有數(shù)據(jù)返回,就會(huì)直接返回一個(gè)EWOULDBLOCK或EAGAIN錯(cuò)誤,此時(shí)進(jìn)程將不會(huì)一直被阻塞。
-
當(dāng)我們把以上操作設(shè)置為了非阻塞狀態(tài),我們需要設(shè)置一個(gè)線(xiàn)程對(duì)該操作進(jìn)行輪詢(xún)檢查,這是最傳統(tǒng)的非阻塞IO模型。
3. IO復(fù)用
- 如果使用用戶(hù)線(xiàn)程輪詢(xún)查看一個(gè)IO操作的狀態(tài),在大量請(qǐng)求的情況下,這對(duì)于CPU的使用率是災(zāi)難。
-
linux提供了IO復(fù)用函數(shù)select/poll/epoll,進(jìn)程將一個(gè)或多個(gè)讀操作通過(guò)系統(tǒng)調(diào)用函數(shù),阻塞在函數(shù)操作上。這樣,系統(tǒng)內(nèi)核就可以幫助我們偵測(cè)多個(gè)讀操作是否處于就緒狀態(tài)。
select()函數(shù)
- 在超時(shí)時(shí)間內(nèi),監(jiān)聽(tīng)用戶(hù)感興趣的文件描述符上的可讀可寫(xiě)和異常事件的發(fā)生。
- linux操作系統(tǒng)的內(nèi)核將所有外部設(shè)備都看做一個(gè)文件來(lái)操作,對(duì)一個(gè)文件的讀寫(xiě)操作會(huì)調(diào)用內(nèi)核提供的系統(tǒng)命令,返回一個(gè)文件描述符fd。
- select() 函數(shù)監(jiān)視的文件描述符分 3 類(lèi),分別是 writefds(寫(xiě)文件描述符)、readfds(讀文件描述符)以及 exceptfds(異常事件文件描述符)。
- 調(diào)用后select()函數(shù)會(huì)阻塞,直到有描述符就緒或者超時(shí),函數(shù)返回。當(dāng)select函數(shù)返回后,可以通過(guò)函數(shù)FD_ISSET比那里fdset,來(lái)找到就緒的描述符。
- fd_set可以理解而我一個(gè)集合,這個(gè)集合中存放的是文件描述符。
epoll()函數(shù)
- select是順序描述fd是否就緒,而且支持fd數(shù)量不宜過(guò)大。
- epoll使用事件驅(qū)動(dòng)的方式代替輪詢(xún)掃描fd。
- epoll實(shí)現(xiàn)通過(guò)epoll_ctl來(lái)注冊(cè)一個(gè)文件描述符,將文件描述符存放到內(nèi)核的一個(gè)事件表中,這個(gè)事件是基于紅黑樹(shù)實(shí)現(xiàn)的,所以在大量IO請(qǐng)求的場(chǎng)景下,
插入和刪除的性能比select/poll的數(shù)組fd_set要好,因此epoll的性能更勝一籌,而且不會(huì)受到fd數(shù)量的限制。 -
一旦某個(gè)文件描述符就緒時(shí),內(nèi)核會(huì)采用類(lèi)似 callback 的回調(diào)機(jī)制,迅速激活這個(gè)文件描述符,當(dāng)進(jìn)程調(diào)用 epoll_wait() 時(shí)便得到通知,之后進(jìn)程將完成相關(guān) I/O 操作。
4. 信號(hào)驅(qū)動(dòng)式IO
信號(hào)驅(qū)動(dòng)式IO類(lèi)似于觀察者模式,內(nèi)核就是一個(gè)觀察者,信號(hào)回調(diào)則是通知。用戶(hù)進(jìn)程發(fā)起一個(gè)IO請(qǐng)求操作,會(huì)通過(guò)系統(tǒng)調(diào)用sigaction函數(shù),給對(duì)應(yīng)的套接字注冊(cè)一個(gè)信號(hào)回調(diào),
此時(shí)不阻塞用戶(hù)進(jìn)程,進(jìn)程會(huì)繼續(xù)工作。當(dāng)內(nèi)核數(shù)據(jù)就緒時(shí),內(nèi)核就為該進(jìn)程生成一個(gè)SIGIO信號(hào),通過(guò)信號(hào)回調(diào)通知進(jìn)行相關(guān)IO操作。

5. 異步IO

在 NIO 服務(wù)端通信編程中,首先會(huì)創(chuàng)建一個(gè) Channel,用于監(jiān)聽(tīng)客戶(hù)端連接;接著,創(chuàng)建多路復(fù)用器 Selector,并將 Channel 注冊(cè)到 Selector,程序會(huì)通過(guò) Selector 來(lái)輪詢(xún)注冊(cè)在其上的 Channel,當(dāng)發(fā)現(xiàn)一個(gè)或多個(gè) Channel 處于就緒狀態(tài)時(shí),返回就緒的監(jiān)聽(tīng)事件,最后程序匹配到監(jiān)聽(tīng)事件,進(jìn)行相關(guān)的 I/O 操作。



