I/O多路複用

        I/O多路複用是這樣一種機制:通過一個進程去監視多個文件描述符,一旦其中某個描述符就緒(通常是讀就緒或者寫就緒),就去通知程序進行相應的讀或寫操作,如果始終沒有描述符就緒,則一直阻塞直到超時。

        目前支持I/O多路複用的常見系統調用有select、poll和epoll。注意,這三者本質上還是屬於同步I/O。

一、select

        select函數監視的文件描述符有三類,分別是readset、writeset和exceptset。調用該函數後,函數就處於阻塞狀態,直到有描述符就緒(有數據可讀、可寫、異常),或者超時,這時函數返回後,就可以通過遍歷數據結構fd_set來找到已就緒的文件描述符。函數原型如下:

//若有就緒描述符則返回其數目,若超時則返回0,若出錯則返回-1
int select( int maxfdpl, fd_set *readset, fd_set *writeset,
            fd_set *exceptset, const struct timeval *timeout);

        在連接的文件描述符數量不大時,select函數性能是可以的,但是一旦描述符數量過大,而實際活躍的描述符數量又極少時,性能就有問題了。所以select函數一般有以下幾個缺點:

  1. 每次調用select都需要把所有的fd從用戶態拷貝到內核態,當fd很多時開銷比較大;
  2. 每次調用select都需要在內核線性掃描所有的socket,不管這個socket是否是活躍的,這在幅度很多時開銷也比較大;
  3. select一次可以監視的fd數目是有限制的,默認值FD_SETSIZE是1024。

二、poll

        poll本質上和select沒什麼區別,它將文件描述符數組傳到內核態,然後查詢每個fd對應的socket狀態,如果某個文件描述符就緒,則將該描述符添加到該函數開始時初始化的等待隊列中,並繼續遍歷,或者超時。之後就可以通過遍歷數據結構pollfd來找到就緒的文件描述符了。函數原型如下:

//若有就緒描述符則返回其數目,若超時之前沒有任何描述符就緒則返回0,若出錯則返回1
int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);

        poll相對select改進的一點是取消了文件描述符最大連接數的限制,不再侷限於1024,但同時又多了一個缺點,就是如果報告了就緒的fd後卻沒有處理,在下次調用poll時會再次報告這個fd。

        以上,select和poll返回後,都需要通過遍歷所有連接着的文件描述符來獲取已經就緒的socket。而事實上,同一時間連接的socket一般只有很少數目的描述符處於就緒狀態,因此隨着監視的描述符數量的增長,效率會直線下降。

三、epoll

epoll在linux內核中申請了一個簡易的文件系統,把原先的select調用或者poll調用分成了三個部分:

  1. 調用epoll_create建立一個epoll對象,返回的文件描述符將用作其它所有epoll系統調用的第一個參數,指定要訪問的內核事件表;
  2. 調用epoll_ctl向內核事件表中添加、刪除或修改文件描述符;
  3. 調用epoll_wait收集就緒的文件描述符的個數。

因此在實際獲取就緒的文件描述符時,只需要調用epoll_wait就可以,無需遍歷所有的連接。函數原型如下:

//size參數目前並不起作用,只是給內核一個提示,告訴它事件表需要多大
int epoll_create(int size);

//epfd是epoll_create函數返回的文件描述符,fd參數是要操作的文件描述符,op參數指定操作類型:
//EPOLL_CTL_ADD:往事件表中添加fd上的事件
//EPOLL_CTL_MOD:修改事件表中fd上註冊事件
//EPOLL_CTL_DEL:刪除事件表中fd上註冊事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

//成功時返回就緒的文件描述符個數,失敗時返回-1並設置errno
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

        從數據結構角度講,每一個epoll對象都有一個獨立的eventpoll結構體,這個結構體會在內核空間中創造獨立的內存,用於存儲epoll_ctl方法向epoll對象中添加進來的事件。這些事件都會掛到rbr紅黑樹中,這樣如果有重複的事件添加進來就可以通過紅黑樹而高效地識別出來。另外所有添加到epoll中的事件都會與設備(如網卡)驅動程序建立回調關係,也就是說當相應的事件發生時會調用這裏的回調方法,回調方法會把這樣的事件放到雙向鏈表rdllist中。

        當調用epoll_wait檢查是否有發生事件的連接時,就只是檢查eventpoll對象中的雙向鏈表rdllist是否有epitem元素而已,如果rdllist鏈表不爲null,則把這裏的事件複製到用戶態內存,並將事件數量返回給用戶。同時當epoll_ctl調用向epoll對象中添加、修改、刪除事件時,從紅黑樹rbr中查找事件也非常快。也就是說,epoll是非常高效的,可以輕易地處理百萬級別的併發連接。

        epoll對文件描述符的操作有兩種模式:LT(Level Trigger,水平觸發)模式和ET(Edge Trigger,邊緣觸發)模式。默認情況下,epoll採用LT模式。這時當epoll_wait檢測到有事件發生並將此事件通知應用程序後,應用程序如果這次不立即處理該事件,當應用程序下一次調用epoll_wait時,epoll_wait會再次將該事件通知給應用程序,直到該事件被處理爲止。若爲ET模式,當epoll_wait檢測到有就緒事件並通知應用程序時,應用程序會立即處理該事件,因爲如果這次不處理,後續的epoll_wait又不再會報告這一事件,該事件就永遠不會再處理了。因此ET模式很大程度上降低了同一個epoll事件被重複觸發的次數,效率相比LT模式要高。

發佈了68 篇原創文章 · 獲贊 7 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章