高性能跨平台網絡IO(Reactor、epoll、iocp)總結


今天聽了公司內部的講座,對於之前關於IO一些模模糊糊的地方有了一些新的感想以及體會,故此總結一下。

一、IO模型:Reactor和Proactor

 

 

 

 Reactor框架工作模式為:用戶注冊事件,而后Reactor框架監聽該事件,當數據到達后,通知用戶,而后用戶自己完成事件處理。因此用戶只需向Reactor提供fd即可。

Proactor框架工作模式為:用戶注冊事件,而后Proactor框架監聽,數據到達后,Proactor完成事件處理,而后返回給用戶通知以及處理完成的數據。用戶需向Proactor提供fd以及buf。

Reactor的buf由用戶操控,因此可以復用,最多只需用戶線程數*buf_size即可;而Proactor的buf由Proactor操控,因此需要請求數*buf_size,內存占用較大,同時提供給Proactor的buf用戶在其返回前不能觸碰,增大了編寫代碼難度。

二、Select和Poll

Select和Poll都是Posix規定的IO接口,這里主要介紹poll。

struct pollfd {  
 int fd;        //文件描述符  
 short events;  //要求查詢的事件掩碼  
 short revents; //返回的事件掩碼  
};  
int poll(struct pollfd *ufds, unsigned int nfds, int timeout); 

poll是實現監聽fd的工具。ufds數組是需要監聽的fd,nfds是ufds的長度,timeout是超時時間,且poll是水平觸發的。當用戶調用poll時,內核會copy ufds(這樣在poll監聽的同時,用戶仍然可以去修改本地的ufds),而后去監聽這些fd,當有fd可讀或可寫,則poll返回觸發的fd個數,同時用戶可以去遍歷ufds查找觸發的fd。這樣導致,如果注冊的fd過多,poll的性能會下降,因為copy ufds是O(n),同時遍歷ufds也是O(n)。

當使用poll來建立Reactor框架時,其結構為:poller線程池監控poll,當獲取到fd時,將fd的任務分發給處理線程池worker。

三、EPoll、kQueue、iocp

由於poll性能下降的問題不能滿足需求,同時posix並沒有出台新的標准,因此各大server廠商實現了自己的高性能poll。

EPoll Linux 2002
kqueue FreeBsd(MAC) 2000
iocp Windows 1993

首先介紹EPoll。

epoll_create 創建一個epoll對象,一般epollfd = epoll_create()
epoll_ctl (epoll_add/epoll_del的合體),往epoll對象中增加/刪除某一個流的某一個事件
比如
epoll_ctl(epollfd, EPOLL_CTL_ADD, socket, EPOLLIN);//有緩沖區內有數據時epoll_wait返回
epoll_ctl(epollfd, EPOLL_CTL_DEL, socket, EPOLLOUT);//緩沖區可寫入時epoll_wait返回
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);等待直到注冊的事件發生

epoll相比於poll來說,具有了自己的對象。poll上一次調用與下一次調用是毫無關系的,而epoll則是首先創建一個epoll對象,而后對這個對象進行注冊、刪除、修改,然后監聽。當創建一個epoll對象時,其在內核中創建了一個紅黑樹和一個完成事件鏈表。紅黑樹存儲注冊事件,完成事件鏈表則保持返回的觸發事件。這里使用紅黑樹,肯定需要一把鎖,同時linux最大fd為65536,所以紅黑樹的性能在這個數量級很好。而由於其返回的是觸發事件鏈表,因此用戶只需遍歷該鏈表即可獲取到觸發fd,不會有性能問題。

並且epoll可以使用ET(edge trigger邊緣觸發)和LT(level trigger水平觸發)兩種方式,使用ET內核會監控更少,但對編程要求更高。

LT模式:當epoll_wait檢測到描述符事件發生並將此事件通知應用程序,應用程序可以不立即處理該事件。下次調用epoll_wait時,會再次響應應用程序並通知此事件。
ET模式:當epoll_wait檢測到描述符事件發生並將此事件通知應用程序,應用程序必須立即處理該事件。如果不處理,下次調用epoll_wait時,不會再次響應應用程序並通知此事件。

epoll_wait調用后,會返回觸發事件數int,以及事件鏈表events。這里maxevents參數代表該epoll_wait最多返回的enents數,如果返回fd過多,如10000,而有10個epoller線程監聽同一個epfd,則設置maxevents為1000,每個線程最多獲得1000個觸發events,充分利用CPU。

而后介紹iocp(input output completion port)

 

 iocp在內核實現了poll、poller、task_queue,用戶worker可以直接使用GetQueuesCompletionStatus獲取到一個觸發的事件任務, 而不需要管理poller以及fd封裝任務分發等。同時,iocp只能監聽windows的api觸發事件。

其使用方法為:CreateIoCompletionPort函數創建一個新的IOCP;CreateIoCompletionPort把socket或文件句柄與一個已存在的IOCP關聯起來;線程調用GetQueuedCompletionStatus函數等待放入IOCP的I/O完成包。

同時,線程可以用PostQueuedCompletionStatus函數在IOCP上投寄一個完成包。也有CancelEx可以殺掉某個監聽事件,且操作為原子的,調用CancelEx后,只會出現監聽成功的Task、監聽失敗Cancel成功的Task、監聽失敗Cancel失敗的Task之一。

四、io框架

開源網絡庫:ace、boost.asio、libevent

五、監聽事件隊列超時處理方法:

不好的方法:

維護一個超時隊列,epoll_wait超時時間最小的事件。

好的方法:

超時器數據結構:

  • 超時=0,不進入超時器
  • 超時!=0,進入超時器的超時隊列
  • 超時隊列支持增刪改查,支持有序遍歷

超時器工作模式:

  • 單獨開一個LInux temerfd,掛在epoll上,監聽讀事件
  • 任何時刻提交的新IO請求,插入后如果位於隊列頭,重新設置timerfd
  • 當epoll告知timerfd可讀時,從頭遍歷超時隊列,對超時的socket
  1. 喚醒worker,告知用戶超時了
  2. 調用epoll_ctrl、刪除對應的sockerfd
  3. epoll刪除完成時關閉socketfd
  • 如果要保證原子性,需要1把互斥鎖、鎖粒度略大

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM