I/O多路復(fù)用到底是怎么一回事?一文讀懂

高性能是每個工程師的追求,無論寫一行代碼還是做一個系統(tǒng),都希望能夠達到高性能的效果。

高性能架構(gòu)設(shè)計主要集中在兩方面:

  • 盡量提升單服務(wù)器的性能,將單服務(wù)器的性能發(fā)揮到極致

  • 如果單服務(wù)器無法支撐性能,設(shè)計服務(wù)器集群方案


單服務(wù)器高性能的關(guān)鍵之一就是服務(wù)器采取的網(wǎng)絡(luò)編程模型。服務(wù)器如何管理連接,如何處理請求等。這兩個設(shè)計點最終都和操作系統(tǒng)的I/O模型及進程模型相關(guān)。

  • I/O模型:阻塞、非阻塞、同步、異步。

  • 進程模型:單進程、多進程、多線程。


我們所說的I/O模型是指網(wǎng)絡(luò)I/O模型,就是服務(wù)端如何管理連接,如何請求連接的措施,是用一個進程管理一個連接(PPC),還是一個線程管理一個連接(TPC),亦或者一個進程管理多個連接(Reactor)。


因此,IO多路復(fù)用中多路就是多個TCP連接(或多個Channel),復(fù)用就是指復(fù)用一個或少量線程,理解起來就是多個網(wǎng)路IO復(fù)用一個或少量線程來處理這些連接



什么是IO操作


我們都知道在Unix環(huán)境下,一切皆文件,而文件就是一串二進制流。在信息 交換的過程中,我們都是對這些流進行數(shù)據(jù)的收發(fā)操作,簡稱為I/O操作(input and output),往流中讀出數(shù)據(jù),系統(tǒng)調(diào)用read,寫入數(shù)據(jù),系統(tǒng)調(diào)用write。




文件描述符


可是在實際操作過程中,計算機里面有那么多流,我們怎么知道該造作的是哪個流呢?于是我們就要依賴文件描述符。


文件描述符就是一個整數(shù),這就有點像我們剛出生的時候,會有一個身份 證編號,通過這個編號,我們能夠確定一個具體的人。那么同理,我們在Unix系統(tǒng)下,如果打開一個文件,會得到一個文件描述符,以后我們通過這個文件描述符就可以找到這個需要操作的文件,進行操作、




什么叫阻塞


假如你今天在家里做飯,你決定我一定要先燒魚,可是去買魚的人就是沒有回來,于是你后面的事情都不干了,卡在那里等魚,這就是阻塞


你在Unix系統(tǒng)中,如果一定要等一條信息,那條信息不來,你就什么都不干,停在那里了,這就是阻塞。



常見IO模型


  • 同步阻塞IO(Blocking IO):即傳統(tǒng)IO模型。

  • 同步非阻塞IO(Non-blocking IO):默認(rèn)常見的socket都是阻塞的,非阻塞IO要求socket被設(shè)置成NONBLOCK。

  • IO多路復(fù)用(IO Multiplexing):即經(jīng)典的Reactor設(shè)計模式,也被稱為異步阻塞IO,Java中的selector和linux中的epoll都是這種模型。

  • 異步IO(Asychronous IO):即Proactor設(shè)計模式,也被稱為異步非阻塞IO。


同步和異步的概念描述的是用戶線程與內(nèi)核的交互方式,這里所說的用戶進程/線程和內(nèi)核是以傳輸層為分割線的,傳輸層以上是指用戶進程,傳輸層以下(包括傳輸層)是指內(nèi)核(處理所有通信細(xì)節(jié),發(fā)送數(shù)據(jù),等待確認(rèn),給無序到達的數(shù)據(jù)排序等,這四層是操作系統(tǒng)內(nèi)核的一部分)。同步是指用戶線程發(fā)起IO請求后需要等待或者輪詢內(nèi)核IO操作,完成后才能繼續(xù)執(zhí)行。異步是指用戶線程發(fā)起IO請求后仍繼續(xù)執(zhí)行,當(dāng)內(nèi)核IO操作完成后回通知用戶線程,或者調(diào)用用戶線程注冊的回調(diào)函數(shù)。


阻塞和非阻塞的概念描述的是用戶線程調(diào)用內(nèi)核IO操作的方式,阻塞時指IO操作需要徹底完成后才能返回用戶空間,非阻塞時指IO操作被調(diào)用后立即返回給用戶一個狀態(tài)值,無需等待IO操作徹底完成。


1
同步阻塞IO

同步阻塞IO是最簡單的IO模型,用戶線程在內(nèi)核進行IO操作時被阻塞。用戶線程通過調(diào)用系統(tǒng)調(diào)用read發(fā)起IO讀操作,由用戶空間轉(zhuǎn)到內(nèi)核空間。內(nèi)核等到數(shù)據(jù)包到達后,然后將接受的數(shù)據(jù)拷貝到用戶空間,完成read操作。整個IO請求過程,用戶線程都是被阻塞的,對CPU利用率不夠。

I/O多路復(fù)用到底是怎么一回事?一文讀懂的圖1


2
同步非阻塞IO

在同步基礎(chǔ)上,將socket設(shè)置為NONBLOCK,這樣用戶線程可以在發(fā)起IO請求后立即返回。雖說可以立即返回,但并未讀到任何數(shù)據(jù),用戶線程需要不斷的發(fā)起IO請求,直到數(shù)據(jù)到達后才能真正讀到數(shù)據(jù),然后去處理。


整個IO請求中,雖然可以立即返回,但是因為是同步的,為了等到數(shù)據(jù),需要不斷的輪詢、重復(fù)請求,消耗了大量的CPU資源。因此,這種模型很少使用,實際用處不大。


I/O多路復(fù)用到底是怎么一回事?一文讀懂的圖2

3
IO多路復(fù)用

不管是同步阻塞還是同步非阻塞,對系統(tǒng)性能的提升都是很小的。而通過復(fù)用可以使一個或一組線程(線程池)處理多個TCP連接。IO多路復(fù)用使用兩個系統(tǒng)調(diào)用(select/poll/epoll和recvfrom),blocking IO只調(diào)用了recvfrom。select/poll/epoll核心是可以同時處理多個connection,而不是更快,所以連接數(shù)不高的話,性能不一定比多線程+阻塞IO好。


select是內(nèi)核提供的多路分離函數(shù),使用它可以避免同步非阻塞IO中輪詢等待問題。


I/O多路復(fù)用到底是怎么一回事?一文讀懂的圖3


用戶首先將需要進行IO操作的socket添加到select中,然后阻塞等待select系統(tǒng)調(diào)用返回。當(dāng)數(shù)據(jù)到達時,socket被激活,select函數(shù)返回,用戶線程正式發(fā)起read請求,讀取數(shù)據(jù)并繼續(xù)執(zhí)行。


這么一看,這種方式和同步阻塞IO并沒有太大區(qū)別,甚至還多了添加監(jiān)視socket以及調(diào)用select函數(shù)的額外操作,效率更差。但是使用select以后,用戶可以在一個線程內(nèi)同時處理多個socket的IO請求,這就是它的最大優(yōu)勢。用戶可以注冊多個socket,然后不斷調(diào)用select讀取被激活的socket,即可達到同一個線程同時處理多個IO請求的目的。而在同步阻塞模型中,必須通過多線程方式才能達到這個目的。所以IO多路復(fù)用設(shè)計目的其實不是為了快,而是為了解決線程/進程數(shù)量過多對服務(wù)器開銷造成的壓力。

select(socket); #向select注冊socketwhile(true){   sockets = select(); #獲取被激活的socket   for(socket in sockets){      if(can_read(socket)){        #socket可讀,調(diào)用read讀取數(shù)據(jù)         read(socket,buffer);         process(buffer);   }}}<code></code>


雖然這種方式允許單線程內(nèi)處理多個IO請求,但是每個IO請求的過程還是阻塞的(在select函數(shù)上阻塞),平均時間甚至比同步阻塞IO模型還要長。如果用戶線程只注冊自己感興趣的socket,然后去做自己的事情,等到數(shù)據(jù)到來時在進行處理,則可以提高CPU利用率。


I/O多路復(fù)用到底是怎么一回事?一文讀懂的圖4


通過Reactor方式,用戶線程輪詢IO操作狀態(tài)的工作統(tǒng)一交給handle_events事件循環(huán)處理。用戶線程注冊事件處理器之后可以繼續(xù)執(zhí)行做其他的工作(異步),而Reactor線程負(fù)責(zé)調(diào)用內(nèi)核的select函數(shù)檢查socket狀態(tài)。當(dāng)有socket被激活時,則通知相應(yīng)的用戶線程(或執(zhí)行用戶線程的回調(diào)函數(shù)),執(zhí)行handel_envent進行數(shù)據(jù)的讀取、處理工作。


由于select函數(shù)是阻塞的,因此多路IO復(fù)用模型就被稱為異步阻塞IO模型,這里阻塞不是指socket。因為使用IO多路復(fù)用時,socket都設(shè)置NONBLOCK,不過不影響,因為用戶發(fā)起IO請求時,數(shù)據(jù)已經(jīng)到達了,用戶線程一定不會被阻塞。


IO多路復(fù)用是最常用的IO模型,但其異步程度還不徹底,因為它使用了回阻塞線程的select系統(tǒng)調(diào)用。因此IO多路復(fù)用只能稱為異步阻塞IO,而非真正的異步IO。

附:Reactor設(shè)計模式

I/O多路復(fù)用到底是怎么一回事?一文讀懂的圖5

4
異步非阻塞IO

在IO多路復(fù)用模型中,事件循環(huán)文件句柄的狀態(tài)事件通知給用戶線程,由用戶線程自行讀取數(shù)據(jù)、處理數(shù)據(jù)。而異步IO中,當(dāng)用戶線程收到通知時候,數(shù)據(jù)已經(jīng)被內(nèi)核讀取完畢,并放在了用戶線程指定的緩沖區(qū)內(nèi),內(nèi)核在IO完成后通知用戶線程直接使用就行了。因此這種模型需要操作系統(tǒng)更強的支持,把read操作從用戶線程轉(zhuǎn)移到了內(nèi)核。


相比于IO多路復(fù)用模型,異步IO并不十分常用,不少高性能并發(fā)服務(wù)程序使用IO多路復(fù)用+多線程任務(wù)處理的架構(gòu)基本可以滿足需求。不過最主要原因還是操作系統(tǒng)對異步IO的支持并非特別完善,更多的采用IO多路復(fù)用模擬異步IO方式(IO事件觸發(fā)時不直接通知用戶線程,而是將數(shù)據(jù)讀寫完畢后放到用戶指定的緩沖區(qū))。





select、poll、epoll詳解


select,poll,epoll都是IO多路復(fù)用的機制。


I/O多路復(fù)用就是通過一種機制,一個進程可以監(jiān)視多個描述符(socket),一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應(yīng)的讀寫操作。


雖說IO多路復(fù)用被稱為異步阻塞IO,但select,poll,epoll本質(zhì)上都是同步IO,因為它們都需要在續(xù)寫事件就緒后自己負(fù)責(zé)進行讀寫,也就是說這個讀寫過程是阻塞的,而真正意義上的異步IO無需自己負(fù)責(zé)進行讀寫。


1)select


int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);<code></code>


select函數(shù)監(jiān)視的文件描述符有三類,readfds,writefds,exceptfds。調(diào)用后函數(shù)會阻塞,直到有描述符就緒(有數(shù)據(jù)讀、寫、或者有except),或者超時(timeout指定時間,如果立即返回設(shè)置null),函數(shù)返回。當(dāng)select函數(shù)返回后,可以通過便利fdset,來找到就緒的描述符。


優(yōu)點:良好的跨平臺性。


缺點:單個進程能夠監(jiān)視的文件描述符的數(shù)量存在最大限制,在Linux上為1024,可以通過修改宏定義甚至重新編譯內(nèi)核的方式提升這一限制,但這樣會造成效率的降低。


2)poll

int poll(struct poll *fds, unsigned int nfds, int timeout);struct pollfd{   int fd;   short events;   short revents;};<code></code>


與select使用三個位圖來表示fdset,poll使用一個pollfd的指針實現(xiàn)。pollfd結(jié)構(gòu)包含了要監(jiān)視的event和發(fā)生的event,不在使用select參數(shù)傳值的方式。同時pollfd并沒有最大數(shù)量的限制(但數(shù)量過大性能也會下降)。和select一樣,poll返回后,需要輪詢pollfd來或許就緒的描述符。


3)epoll


epoll是select和poll的增強版本,相比于前兩者,它更加的靈活,沒有描述符的限制。epoll使用一個文件描述符管理多個描述符,將用戶關(guān)系的文件描述符的事件存放到內(nèi)核的一個事件表中,這樣在用戶空間和內(nèi)核空間的copy只需要一次。


*本文系網(wǎng)絡(luò)轉(zhuǎn)載,版權(quán)歸原作者所有,如有侵權(quán)請聯(lián)系刪除

登錄后免費查看全文
立即登錄
App下載
技術(shù)鄰APP
工程師必備
  • 項目客服
  • 培訓(xùn)客服
  • 平臺客服

TOP