Linux下的I/O複用與epoll詳解

前言

      I/O多路複用有不少種實現。在linux上,2.4內核前主要是select和poll,自Linux 2.6內核正式引入epoll以來,epoll已經成爲了目前實現高性能網絡服務器的必備技術。儘管他們的使用方法不盡相同,可是本質上卻沒有什麼區別。本文將重點探討將放在EPOLL的實現與使用詳解。html

爲何會是EPOLL

select的缺陷

      高併發的核心解決方案是1個線程處理全部鏈接的「等待消息準備好」,這一點上epoll和select是無爭議的。但select預估錯誤了一件事,當數十萬併發鏈接存在時,可能每一毫秒只有數百個活躍的鏈接,同時其他數十萬鏈接在這一毫秒是非活躍的。select的使用方法是這樣的:
      返回的活躍鏈接 ==select(所有待監控的鏈接)。
      何時會調用select方法呢?在你認爲須要找出有報文到達的活躍鏈接時,就應該調用。因此,調用select在高併發時是會被頻繁調用的。這樣,這個頻繁調用的方法就頗有必要看看它是否有效率,由於,它的輕微效率損失都會被「頻繁」二字所放大。它有效率損失嗎?顯而易見,所有待監控鏈接是數以十萬計的,返回的只是數百個活躍鏈接,這自己就是無效率的表現。被放大後就會發現,處理併發上萬個鏈接時,select就徹底力不從心了。
      此外,在Linux內核中,select所用到的FD_SET是有限的,即內核中有個參數__FD_SETSIZE定義了每一個FD_SET的句柄個數。
       
1 /linux/posix_types.h:
2 
3 #define __FD_SETSIZE         1024
View Code
      其次,內核中實現 select是用 輪詢方法,即每次檢測都會遍歷全部FD_SET中的句柄,顯然,select函數執行時間與FD_SET中的句柄個數有一個比例關係,即 select要檢測的句柄數越多就會越費時。看到這裏,您可能要要問了,你爲何不提poll?筆者認爲select與poll在內部機制方面並無太大的差別。相比於select機制,poll只是取消了最大監控文件描述符數限制,並無從根本上解決select存在的問題。
       
      接下來咱們看張圖,當併發鏈接爲較小時,select與epoll彷佛並沒有多少差距。但是當併發鏈接上來之後,select就顯得力不從心了。
        圖 1.主流I/O複用機制的benchmark

 epoll高效的奧祕

      epoll精巧的使用了3個方法來實現select方法要作的事:node

  1. 新建epoll描述符==epoll_create()
  2. epoll_ctrl(epoll描述符,添加或者刪除全部待監控的鏈接)
  3. 返回的活躍鏈接 ==epoll_wait( epoll描述符 )
      與select相比,epoll分清了頻繁調用和不頻繁調用的操做。例如,epoll_ctrl是不太頻繁調用的,而epoll_wait是很是頻繁調用的。這時,epoll_wait卻幾乎沒有入參,這比select的效率高出一大截,並且,它也不會隨着併發鏈接的增長使得入參愈加多起來,致使內核執行效率降低。
       
      筆者在這裏不想過多貼出epoll的代碼片斷。若是你們有興趣,能夠參考文末貼出的博文連接和Linux相關源碼。

      要深入理解epoll,首先得了解epoll的三大關鍵要素:mmap、紅黑樹、鏈表linux

      epoll是經過內核與用戶空間mmap同一塊內存實現的。mmap將用戶空間的一塊地址和內核空間的一塊地址同時映射到相同的一塊物理內存地址(無論是用戶空間仍是內核空間都是虛擬地址,最終要經過地址映射映射到物理地址),使得這塊物理內存對內核和對用戶都可見,減小用戶態和內核態之間的數據交換。內核能夠直接看到epoll監聽的句柄,效率高。編程

      紅黑樹將存儲epoll所監聽的套接字。上面mmap出來的內存如何保存epoll所監聽的套接字,必然也得有一套數據結構,epoll在實現上採用紅黑樹去存儲全部套接字,當添加或者刪除一個套接字時(epoll_ctl),都在紅黑樹上去處理,紅黑樹自己插入和刪除性能比較好,時間複雜度O(logN)。數組

      

      下面幾個關鍵數據結構的定義   服務器

 1 struct epitem
 2 {
 3     struct rb_node rbn;            //用於主結構管理的紅黑樹
 4     struct list_head rdllink;       //事件就緒隊列
 5     struct epitem *next;           //用於主結構體中的鏈表
 6     struct epoll_filefd ffd;         //每一個fd生成的一個結構
 7     int nwait;                 
 8     struct list_head pwqlist;     //poll等待隊列
 9     struct eventpoll *ep;          //該項屬於哪一個主結構體
10     struct list_head fllink;         //連接fd對應的file鏈表
11     struct epoll_event event;  //註冊的感興趣的事件,也就是用戶空間的epoll_event
12  }
View Code

      

 1 struct eventpoll
 2 {
 3     spin_lock_t lock;            //對本數據結構的訪問
 4     struct mutex mtx;            //防止使用時被刪除
 5     wait_queue_head_t wq;        //sys_epoll_wait() 使用的等待隊列
 6     wait_queue_head_t poll_wait; //file->poll()使用的等待隊列
 7     struct list_head rdllist;    //事件知足條件的鏈表
 8     struct rb_root rbr;          //用於管理全部fd的紅黑樹
 9     struct epitem *ovflist;      //將事件到達的fd進行連接起來發送至用戶空間
10 }
View Code

      添加以及返回事件

      經過epoll_ctl函數添加進來的事件都會被放在紅黑樹的某個節點內,因此,重複添加是沒有用的。當把事件添加進來的時候時候會完成關鍵的一步,那就是該事件都會與相應的設備(網卡)驅動程序創建回調關係,當相應的事件發生後,就會調用這個回調函數,該回調函數在內核中被稱爲:ep_poll_callback,這個回調函數其實就所把這個事件添加到rdllist這個雙向鏈表中。一旦有事件發生,epoll就會將該事件添加到雙向鏈表中。那麼當咱們調用epoll_wait時,epoll_wait只須要檢查rdlist雙向鏈表中是否有存在註冊的事件,效率很是可觀。這裏也須要將發生了的事件複製到用戶態內存中便可。網絡

     

      epoll_wait的工做流程:數據結構

  1. epoll_wait調用ep_poll,當rdlist爲空(無就緒fd)時掛起當前進程,直到rdlist不空時進程才被喚醒。
  2. 文件fd狀態改變(buffer由不可讀變爲可讀或由不可寫變爲可寫),致使相應fd上的回調函數ep_poll_callback()被調用。
  3. ep_poll_callback將相應fd對應epitem加入rdlist,致使rdlist不空,進程被喚醒,epoll_wait得以繼續執行。
  4. ep_events_transfer函數將rdlist中的epitem拷貝到txlist中,並將rdlist清空。
  5. ep_send_events函數(很關鍵),它掃描txlist中的每一個epitem,調用其關聯fd對用的poll方法。此時對poll的調用僅僅是取得fd上較新的events(防止以前events被更新),以後將取得的events和相應的fd發送到用戶空間(封裝在struct epoll_event,從epoll_wait返回)。     

小結

       表 1. select、poll和epoll三種I/O複用模式的比較( 摘錄自《linux高性能服務器編程》)

系統調用併發

selectsocket

poll

epoll

 

事件集合

用哦過戶經過3個參數分別傳入感興趣的可讀,可寫及異常等事件

內核經過對這些參數的在線修改來反饋其中的就緒事件

這使得用戶每次調用select都要重置這3個參數

統一處理全部事件類型,所以只須要一個事件集參數。

用戶經過pollfd.events傳入感興趣的事件,內核經過

修改pollfd.revents反饋其中就緒的事件

內核經過一個事件表直接管理用戶感興趣的全部事件。

所以每次調用epoll_wait時,無需反覆傳入用戶感興趣

的事件。epoll_wait系統調用的參數events僅用來反饋就緒的事件

應用程序索引就緒文件

描述符的時間複雜度

O(n)

O(n)

O(1)

最大支持文件描述符數

通常有最大值限制

65535

65535

工做模式

LT

LT

支持ET高效模式

內核實現和工做效率 採用輪詢方式檢測就緒事件,時間複雜度:O(n)

採用輪詢方式檢測就緒事件,時間複雜度:O(n)

採用回調方式檢測就緒事件,時間複雜度:O(1)

      行文至此,想必各位都應該已經明瞭爲何epoll會成爲Linux平臺下實現高性能網絡服務器的首選I/O複用調用。

      須要注意的是:epoll並非在全部的應用場景都會比select和poll高不少。尤爲是當活動鏈接比較多的時候,回調函數被觸發得過於頻繁的時候,epoll的效率也會受到顯著影響!因此,epoll特別適用於鏈接數量多,但活動鏈接較少的狀況。

      接下來,筆者將介紹一下epoll使用方式的注意點。

 EPOLL的使用 

 文件描述符的建立 

1 #include <sys/epoll.h>
2 int epoll_create ( int size );
View Code

      在epoll早期的實現中,對於監控文件描述符的組織並非使用紅黑樹,而是hash表。這裏的size實際上已經沒有意義。

  註冊監控事件

1 #include <sys/epoll.h>
2 int epoll_ctl ( int epfd, int op, int fd, struct epoll_event *event );
View Code
函數說明:
     fd:要操做的文件描述符
     op:指定操做類型
操做類型:
     EPOLL_CTL_ADD:往事件表中註冊fd上的事件
     EPOLL_CTL_MOD:修改fd上的註冊事件
     EPOLL_CTL_DEL:刪除fd上的註冊事件
     event:指定事件,它是epoll_event結構指針類型
     epoll_event定義:
1 struct epoll_event
2 {
3     __unit32_t events;    // epoll事件
4     epoll_data_t data;     // 用戶數據 
5 };
View Code
結構體說明:
     events:描述事件類型,和poll支持的事件類型基本相同(兩個額外的事件:EPOLLET和EPOLLONESHOT,高效運做的關鍵)
     data成員:存儲用戶數據
1 typedef union epoll_data
2 {
3     void* ptr;              //指定與fd相關的用戶數據 
4     int fd;                 //指定事件所從屬的目標文件描述符 
5     uint32_t u32;
6     uint64_t u64;
7 } epoll_data_t;
View Code

  epoll_wait函數

1 #include <sys/epoll.h>
2 int epoll_wait ( int epfd, struct epoll_event* events, int maxevents, int timeout );
View Code
函數說明:
     返回:成功時返回就緒的文件描述符的個數,失敗時返回-1並設置errno
     timeout:指定epoll的超時時間,單位是毫秒。當timeout爲-1是,epoll_wait調用將永遠阻塞,直到某個時間發生。當timeout爲0時,epoll_wait調用將當即返回。
     maxevents:指定最多監聽多少個事件
     events:檢測到事件,將全部就緒的事件從內核事件表中複製到它的第二個參數events指向的數組中。

 EPOLLONESHOT事件

使用場合:
      一個線程在讀取完某個socket上的數據後開始處理這些數據,而數據的處理過程當中該socket又有新數據可讀,此時另一個線程被喚醒來讀取這些新的數據。
      因而,就出現了兩個線程同時操做一個socket的局面。可使用epoll的EPOLLONESHOT事件實現一個socket鏈接在任一時刻都被一個線程處理。
做用:
      對於註冊了EPOLLONESHOT事件的文件描述符,操做系統最多出發其上註冊的一個可讀,可寫或異常事件,且只能觸發一次。
使用:
      註冊了EPOLLONESHOT事件的socket一旦被某個線程處理完畢,該線程就應該當即重置這個socket上的EPOLLONESHOT事件,以確保這個socket下一次可讀時,其EPOLLIN事件能被觸發,進而讓其餘工做線程有機會繼續處理這個sockt。
效果:
      儘管一個socket在不一樣事件可能被不一樣的線程處理,但同一時刻確定只有一個線程在爲它服務,這就保證了鏈接的完整性,從而避免了不少可能的競態條件。

 LT與ET模式

      在這裏,筆者強烈推薦《完全學會使用epoll》系列博文,這是筆者看過的,對epoll的ET和LT模式講解最爲詳盡和易懂的博文。下面的實例均來自該系列博文。限於篇幅緣由,不少關鍵的細節,不能徹底摘錄。

      話很少說,直接上代碼。

程序一:

#include <stdio.h>
#include <unistd.h>
#include <sys/epoll.h>

int main(void)
{
  int epfd,nfds;
  struct epoll_event ev,events[5]; //ev用於註冊事件,數組用於返回要處理的事件
  epfd = epoll_create(1); //只須要監聽一個描述符——標準輸入
  ev.data.fd = STDIN_FILENO;
  ev.events = EPOLLIN|EPOLLET; //監聽讀狀態同時設置ET模式
  epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev); //註冊epoll事件
  for(;;)
  {
    nfds = epoll_wait(epfd, events, 5, -1);
    for(int i = 0; i < nfds; i++)
    {
      if(events[i].data.fd==STDIN_FILENO)
        printf("welcome to epoll's word!\n");

    }
  }
}

 

編譯並運行,結果以下:

 

  1. 當用戶輸入一組字符,這組字符被送入buffer,字符停留在buffer中,又由於buffer由空變爲不空,因此ET返回讀就緒,輸出」welcome to epoll's world!」。
  2. 以後程序再次執行epoll_wait,此時雖然buffer中有內容可讀,可是根據咱們上節的分析,ET並不返回就緒,致使epoll_wait阻塞。(底層緣由是ET下就緒fd的epitem只被放入rdlist一次)。
  3. 用戶再次輸入一組字符,致使buffer中的內容增多,根據咱們上節的分析這將致使fd狀態的改變,是對應的epitem再次加入rdlist,從而使epoll_wait返回讀就緒,再次輸出「Welcome to epoll's world!」。

接下來咱們將上面程序的第11行作以下修改:

1  ev.events=EPOLLIN;    //默認使用LT模式
View Code

編譯並運行,結果以下:

 

      程序陷入死循環,由於用戶輸入任意數據後,數據被送入buffer且沒有被讀出,因此LT模式下每次epoll_wait都認爲buffer可讀返回讀就緒。致使每次都會輸出」welcome to epoll's world!」。

程序二:

 1 #include <stdio.h>
 2 #include <unistd.h>
 3 #include <sys/epoll.h>
 4 
 5 int main(void)
 6 {
 7     int epfd,nfds;
 8     struct epoll_event ev,events[5];                    //ev用於註冊事件,數組用於返回要處理的事件
 9     epfd = epoll_create(1);                                //只須要監聽一個描述符——標準輸入
10     ev.data.fd = STDIN_FILENO;
11     ev.events = EPOLLIN;                                //監聽讀狀態同時設置LT模式
12     epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev);    //註冊epoll事件
13     for(;;)
14     {
15         nfds = epoll_wait(epfd, events, 5, -1);
16         for(int i = 0; i < nfds; i++)
17         {
18             if(events[i].data.fd==STDIN_FILENO)
19             {
20                 char buf[1024] = {0};
21                 read(STDIN_FILENO, buf, sizeof(buf));
22                 printf("welcome to epoll's word!\n");
23             }            
24         }
25     }
26 }
View Code

編譯並運行,結果以下:

 

      本程序依然使用LT模式,可是每次epoll_wait返回讀就緒的時候咱們都將buffer(緩衝)中的內容read出來,因此致使buffer再次清空,下次調用epoll_wait就會阻塞。因此可以實現咱們所想要的功能——當用戶從控制檯有任何輸入操做時,輸出」welcome to epoll's world!」

程序三:

 1 #include <stdio.h>
 2 #include <unistd.h>
 3 #include <sys/epoll.h>
 4 
 5 int main(void)
 6 {
 7     int epfd,nfds;
 8     struct epoll_event ev,events[5];                    //ev用於註冊事件,數組用於返回要處理的事件
 9     epfd = epoll_create(1);                                //只須要監聽一個描述符——標準輸入
10     ev.data.fd = STDIN_FILENO;
11     ev.events = EPOLLIN|EPOLLET;                        //監聽讀狀態同時設置ET模式
12     epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev);    //註冊epoll事件
13     for(;;)
14     {
15         nfds = epoll_wait(epfd, events, 5, -1);
16         for(int i = 0; i < nfds; i++)
17         {
18             if(events[i].data.fd==STDIN_FILENO)
19             {
20                 printf("welcome to epoll's word!\n");
21                 ev.data.fd = STDIN_FILENO;
22                 ev.events = EPOLLIN|EPOLLET;                        //設置ET模式
23                 epoll_ctl(epfd, EPOLL_CTL_MOD, STDIN_FILENO, &ev);    //重置epoll事件(ADD無效)
24             }            
25         }
26     }
27 }
View Code

編譯並運行,結果以下:

 

 

     程序依然使用ET,可是每次讀就緒後都主動的再次MOD IN事件,咱們發現程序再次出現死循環,也就是每次返回讀就緒。可是注意,若是咱們將MOD改成ADD,將不會產生任何影響。別忘了每次ADD一個描述符都會在epitem組成的紅黑樹中添加一個項,咱們以前已經ADD過一次,再次ADD將阻止添加,因此在次調用ADD IN事件不會有任何影響。

程序四:

 1 #include <stdio.h>
 2 #include <unistd.h>
 3 #include <sys/epoll.h>
 4 
 5 int main(void)
 6 {
 7     int epfd,nfds;
 8     struct epoll_event ev,events[5];                    //ev用於註冊事件,數組用於返回要處理的事件
 9     epfd = epoll_create(1);                                //只須要監聽一個描述符——標準輸入
10     ev.data.fd = STDOUT_FILENO;
11     ev.events = EPOLLOUT|EPOLLET;                        //監聽讀狀態同時設置ET模式
12     epoll_ctl(epfd, EPOLL_CTL_ADD, STDOUT_FILENO, &ev);    //註冊epoll事件
13     for(;;)
14     {
15         nfds = epoll_wait(epfd, events, 5, -1);
16         for(int i = 0; i < nfds; i++)
17         {
18             if(events[i].data.fd==STDOUT_FILENO)
19             {
20                 printf("welcome to epoll's word!\n");
21             }            
22         }
23     }
24 }
View Code

編譯並運行,結果以下:

 

      這個程序的功能是隻要標準輸出寫就緒,就輸出「welcome to epoll's world」。咱們發現這將是一個死循環。下面具體分析一下這個程序的執行過程:

  1. 首先初始buffer爲空,buffer中有空間可寫,這時不管是ET仍是LT都會將對應的epitem加入rdlist,致使epoll_wait就返回寫就緒。
  2. 程序想標準輸出輸出」welcome to epoll's world」和換行符,由於標準輸出爲控制檯的時候緩衝是「行緩衝」,因此換行符致使buffer中的內容清空,這就對應第二節中ET模式下寫就緒的第二種狀況——當有舊數據被髮送走時,即buffer中待寫的內容變少得時候會觸發fd狀態的改變。因此下次epoll_wait會返回寫就緒。如此循環往復。

程序五:

 1 #include <stdio.h>
 2 #include <unistd.h>
 3 #include <sys/epoll.h>
 4 
 5 int main(void)
 6 {
 7     int epfd,nfds;
 8     struct epoll_event ev,events[5];                    //ev用於註冊事件,數組用於返回要處理的事件
 9     epfd = epoll_create(1);                                //只須要監聽一個描述符——標準輸入
10     ev.data.fd = STDOUT_FILENO;
11     ev.events = EPOLLOUT|EPOLLET;                        //監聽讀狀態同時設置ET模式
12     epoll_ctl(epfd, EPOLL_CTL_ADD, STDOUT_FILENO, &ev);    //註冊epoll事件
13     for(;;)
14     {
15         nfds = epoll_wait(epfd, events, 5, -1);
16         for(int i = 0; i < nfds; i++)
17         {
18             if(events[i].data.fd==STDOUT_FILENO)
19             {
20                 printf("welcome to epoll's word!");
21             }            
22         }
23     }
24 }
View Code

編譯並運行,結果以下:

 

      與程序四相比,程序五隻是將輸出語句的printf的換行符移除。咱們看到程序成掛起狀態。由於第一次epoll_wait返回寫就緒後,程序向標準輸出的buffer中寫入「welcome to epoll's world!」,可是由於沒有輸出換行,因此buffer中的內容一直存在,下次epoll_wait的時候,雖然有寫空間可是ET模式下再也不返回寫就緒。回憶第一節關於ET的實現,這種狀況緣由就是第一次buffer爲空,致使epitem加入rdlist,返回一次就緒後移除此epitem,以後雖然buffer仍然可寫,可是因爲對應epitem已經再也不rdlist中,就不會對其就緒fd的events的在檢測了。

程序六:

 1 #include <stdio.h>
 2 #include <unistd.h>
 3 #include <sys/epoll.h>
 4 
 5 int main(void)
 6 {
 7     int epfd,nfds;
 8     struct epoll_event ev,events[5];                    //ev用於註冊事件,數組用於返回要處理的事件
 9     epfd = epoll_create(1);                                //只須要監聽一個描述符——標準輸入
10     ev.data.fd = STDOUT_FILENO;
11     ev.events = EPOLLOUT;                                //監聽讀狀態同時設置LT模式
12     epoll_ctl(epfd, EPOLL_CTL_ADD, STDOUT_FILENO, &ev);    //註冊epoll事件
13     for(;;)
14     {
15         nfds = epoll_wait(epfd, events, 5, -1);
16         for(int i = 0; i < nfds; i++)
17         {
18             if(events[i].data.fd==STDOUT_FILENO)
19             {
20                 printf("welcome to epoll's word!");
21             }            
22         }
23     }
24 }
View Code

編譯並運行,結果以下:

 

       程序六相對程序五僅僅是修改ET模式爲默認的LT模式,咱們發現程序再次死循環。這時候緣由已經很清楚了,由於當向buffer寫入」welcome to epoll's world!」後,雖然buffer沒有輸出清空,可是LT模式下只有buffer有寫空間就返回寫就緒,因此會一直輸出」welcome to epoll's world!」,當buffer滿的時候,buffer會自動刷清輸出,一樣會形成epoll_wait返回寫就緒。

程序七:

 1 #include <stdio.h>
 2 #include <unistd.h>
 3 #include <sys/epoll.h>
 4 
 5 int main(void)
 6 {
 7     int epfd,nfds;
 8     struct epoll_event ev,events[5];                    //ev用於註冊事件,數組用於返回要處理的事件
 9     epfd = epoll_create(1);                                //只須要監聽一個描述符——標準輸入
10     ev.data.fd = STDOUT_FILENO;
11     ev.events = EPOLLOUT|EPOLLET;                                //監聽讀狀態同時設置LT模式
12     epoll_ctl(epfd, EPOLL_CTL_ADD, STDOUT_FILENO, &ev);    //註冊epoll事件
13     for(;;)
14     {
15         nfds = epoll_wait(epfd, events, 5, -1);
16         for(int i = 0; i < nfds; i++)
17         {
18             if(events[i].data.fd==STDOUT_FILENO)
19             {
20                 printf("welcome to epoll's word!");
21                 ev.data.fd = STDOUT_FILENO;
22                 ev.events = EPOLLOUT|EPOLLET;                        //設置ET模式
23                 epoll_ctl(epfd, EPOLL_CTL_MOD, STDOUT_FILENO, &ev);    //重置epoll事件(ADD無效)
24             }            
25         }
26     }
27 }
View Code

編譯並運行,結果以下:

 

      程序七相對於程序五在每次向標準輸出的buffer輸出」welcome to epoll's world!」後,從新MOD OUT事件。因此至關於每次都會返回就緒,致使程序循環輸出。

      通過前面的案例分析,咱們已經瞭解到,當epoll工做在ET模式下時,對於讀操做,若是read一次沒有讀盡buffer中的數據,那麼下次將得不到讀就緒的通知,形成buffer中已有的數據無機會讀出,除非有新的數據再次到達。對於寫操做,主要是由於ET模式下fd一般爲非阻塞形成的一個問題——如何保證將用戶要求寫的數據寫完。

      要解決上述兩個ET模式下的讀寫問題,咱們必須實現:

  1. 對於讀,只要buffer中還有數據就一直讀;
  2. 對於寫,只要buffer還有空間且用戶請求寫的數據還未寫完,就一直寫。

 ET模式下的accept問題

      請思考如下一種場景:在某一時刻,有多個鏈接同時到達,服務器的 TCP 就緒隊列瞬間積累多個就緒鏈接,因爲是邊緣觸發模式,epoll 只會通知一次,accept 只處理一個鏈接,致使 TCP 就緒隊列中剩下的鏈接都得不處處理。在這種情形下,咱們應該如何有效的處理呢?

      解決的方法是:解決辦法是用 while 循環抱住 accept 調用,處理完 TCP 就緒隊列中的全部鏈接後再退出循環。如何知道是否處理完就緒隊列中的全部鏈接呢? accept  返回 -1 而且 errno 設置爲 EAGAIN 就表示全部鏈接都處理完。 

      關於ET的accept問題,這篇博文的參考價值很高,若是有興趣,能夠連接過去圍觀一下。

ET模式爲何要設置在非阻塞模式下工做

      由於ET模式下的讀寫須要一直讀或寫直到出錯(對於讀,當讀到的實際字節數小於請求字節數時就能夠中止),而若是你的文件描述符若是不是非阻塞的,那這個一直讀或一直寫勢必會在最後一次阻塞。這樣就不能在阻塞在epoll_wait上了,形成其餘文件描述符的任務飢餓。

epoll的使用實例

      這樣的實例,網上已經有不少了(包括參考連接),筆者這裏就略過了。

小結

       LT:水平觸發,效率會低於ET觸發,尤爲在大併發,大流量的狀況下。可是LT對代碼編寫要求比較低,不容易出現問題。LT模式服務編寫上的表現是:只要有數據沒有被獲取,內核就不斷通知你,所以不用擔憂事件丟失的狀況。

       ET:邊緣觸發,效率很是高,在併發,大流量的狀況下,會比LT少不少epoll的系統調用,所以效率高。可是對編程要求高,須要細緻的處理每一個請求,不然容易發生丟失事件的狀況。

      從本質上講:與LT相比,ET模型是經過減小系統調用來達到提升並行效率的。

總結

      epoll使用的梳理與總結到這裏就告一段落了。限於篇幅緣由,不少細節都被略過了。後面參考給出的連接,強烈推薦閱讀。疏謬之處,萬望斧正!   

備註

     本文有至關分量的內容參考借鑑了網絡上各位網友的熱心分享,特別是一些帶有徹底參考的文章,其後附帶的連接內容更直接、更豐富,筆者只是作了一下概括&轉述,在此一併表示感謝。

參考

      《Linux高性能服務器編程》

      《完全學會使用epoll》(系列博文)

      《epoll源碼分析(全) 》

      《linux kernel中epoll的設計和實現

      《poll&&epoll實現分析(二)——epoll實現

相關文章
相關標籤/搜索