epoll好文章

http://www.javashuo.com/article/p-onckbwhc-bm.htmlhtml

https://www.jianshu.com/p/aa486512e989linux

https://cloud.tencent.com/developer/article/1005481算法

 

最後看看epoll獨有的兩種模式LT和ET。不管是LT和ET模式,都適用於以上所說的流程。區別是,LT模式下,只要一個句柄上的事件一次沒有處理完,會在之後調用epoll_wait時次次返回這個句柄,而ET模式僅在第一次返回。

這件事怎麼作到的呢?

當一個socket句柄上有事件時,內核會把該句柄插入上面所說的準備就緒list鏈表,這時咱們調用epoll_wait,會把準備就緒的socket拷貝到用戶態內存,而後清空準備就緒list鏈表,

最後,epoll_wait幹了件事,就是檢查這些socket,若是不是ET模式(就是LT模式的句柄了),而且這些socket上確實有未處理的事件時,又把該句柄放回到剛剛清空的準備就緒鏈表了。

因此,非ET的句柄,只要它上面還有事件,epoll_wait每次都會返回。而ET模式的句柄,除非有新中斷到,即便socket上的事件沒有處理完,也是不會次次從epoll_wait返回的。

  

epoll爲何使用紅黑樹
由於epoll要求快速找到某個句柄,所以首先是一個Map接口,候選實現:

哈希表 O(1)
紅黑樹 O(lgn)
跳錶 近似O(lgn)
聽說老版本的內核和FreeBSD的Kqueue使用的是哈希表.
我的理解如今內核使用紅黑樹的緣由:

哈希表. 空間因素,可伸縮性.
(1)頻繁增刪. 哈希表須要預估空間大小, 這個場景下沒法作到.
間接影響響應時間,假如要resize,原來的數據還得移動.即便用了一致性哈希算法,
也難以知足非阻塞的timeout時間限制.(時間不穩定)
(2) 百萬級鏈接,哈希表有鏤空的空間,太浪費內存.
跳錶. 慢於紅黑樹. 空間也高.
紅黑樹. 經驗判斷,內核的其餘地方如防火牆也使用紅黑樹,實踐上看性能最優.

  

 

fd數量受限於內核內存大小,理論上無限

  

epoll 跟mmap不要緊 沒用到mmap

  

http://www.javashuo.com/article/p-wsiwtuxy-m.html編程

後面的ETLT區別寫得好數組

 

LT與ET模式

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

      話很少說,直接上代碼。併發

程序一:app

複製代碼
#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行作以下修改:socket

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

編譯並運行,結果以下:ide

 

      程序陷入死循環,由於用戶輸入任意數據後,數據被送入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 }
複製代碼

編譯並運行,結果以下:

 

      本程序依然使用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 }
複製代碼

編譯並運行,結果以下:

 

 

     程序依然使用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 }
複製代碼

編譯並運行,結果以下:

 

      這個程序的功能是隻要標準輸出寫就緒,就輸出「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 }
複製代碼

編譯並運行,結果以下:

 

      與程序四相比,程序五隻是將輸出語句的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 }
複製代碼

編譯並運行,結果以下:

 

       程序六相對程序五僅僅是修改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 }
複製代碼

編譯並運行,結果以下:

 

      程序七相對於程序五在每次向標準輸出的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模型是經過減小系統調用來達到提升並行效率的。

相關文章
相關標籤/搜索