Linux下的I/O復用與epoll詳解(ET與LT)

前言

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

為什么會是EPOLL

select的缺陷

? ? ? 高并發的核心解決方案是1個線程處理所有連接的“等待消息準備好”,這一點上epoll和select是無爭議的。但select預估錯誤了一件事,當數十萬并發連接存在時,可能每一毫秒只有數百個活躍的連接,同時其余數十萬連接在這一毫秒是非活躍的。select的使用方法是這樣的:

? ? ? 返回的活躍連接 ==select(全部待監控的連接)。

? ? ? 什么時候會調用select方法呢?在你認為需要找出有報文到達的活躍連接時,就應該調用。所以,調用select在高并發時是會被頻繁調用的。這樣,這個頻繁調用的方法就很有必要看看它是否有效率,因為,它的輕微效率損失都會被“頻繁”二字所放大。它有效率損失嗎?顯而易見,全部待監控連接是數以十萬計的,返回的只是數百個活躍連接,這本身就是無效率的表現。被放大后就會發現,處理并發上萬個連接時,select就完全力不從心了。

? ? ? 此外,在Linux內核中,select所用到的FD_SET是有限的,即內核中有個參數__FD_SETSIZE定義了每個FD_SET的句柄個數。
? ? ? ?

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方法要做的事:

  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、紅黑樹、鏈表

? ? ??epoll是通過內核與用戶空間mmap同一塊內存實現的。mmap將用戶空間的一塊地址和內核空間的一塊地址同時映射到相同的一塊物理內存地址(不管是用戶空間還是內核空間都是虛擬地址,最終要通過地址映射映射到物理地址),使得這塊物理內存對內核和對用戶均可見,減少用戶態和內核態之間的數據交換。內核可以直接看到epoll監聽的句柄,效率高。

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

? ? ??

? ? ? 下面幾個關鍵數據結構的定義? ?

復制代碼

 1 struct epitem2 {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  }

復制代碼

? ? ??

復制代碼

 1 struct eventpoll2 {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 }

復制代碼

? ? ??添加以及返回事件

? ? ??通過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高性能服務器編程》)

系統調用

select

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 );

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

? 注冊監控事件

1 #include <sys/epoll.h>
2 int epoll_ctl ( int epfd, int op, int fd, struct epoll_event *event );

函數說明:

? ? ?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 };

結構體說明:

? ? ?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;

復制代碼

? epoll_wait函數

1 #include <sys/epoll.h>
2 int epoll_wait ( int epfd, struct epoll_event* events, int maxevents, int timeout );

函數說明:

? ? ?返回:成功時返回就緒的文件描述符的個數,失敗時返回-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模式

編譯并運行,結果如下:

?

? ? ? 程序陷入死循環,因為用戶輸入任意數據后,數據被送入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模型是通過減少系統調用來達到提高并行效率的。

總結

? ? ? epoll使用的梳理與總結到這里就告一段落了。限于篇幅原因,很多細節都被略過了。后面參考給出的鏈接,強烈推薦閱讀。疏謬之處,萬望斧正!? ?

備注

? ? ?本文有相當份量的內容參考借鑒了網絡上各位網友的熱心分享,特別是一些帶有完全參考的文章,其后附帶的鏈接內容更直接、更豐富,筆者只是做了一下歸納&轉述,在此一并表示感謝。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/384937.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/384937.shtml
英文地址,請注明出處:http://en.pswp.cn/news/384937.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

徹底學會使用epoll(一)——ET模式實現分析

注&#xff1a;之前寫過兩篇關于epoll實現的文章&#xff0c;但是感覺懂得了實現原理并不一定會使用&#xff0c;所以又決定寫這一系列文章&#xff0c;希望能夠對epoll有比較清楚的認識。是請大家轉載務必注明出處&#xff0c;算是對我勞動成果的一點點尊重吧。另外&#xff0…

OPENSSL X509證書驗證

openssl實現了標準的x509v3數字證書&#xff0c;其源碼在crypto/x509和crypto/x509v3中。其中x509目錄實現了數字證書以及證書申請相關的各種函數&#xff0c;包括了X509和X509_REQ結構的設置、讀取、打印和比較&#xff1b;數字證書的驗證、摘要&#xff1b;各種公鑰的導入導出…

linux網絡編程九:splice函數,高效的零拷貝

1. splice函數 #include <fcntl.h> ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags); splice用于在兩個文件描述符之間移動數據&#xff0c; 也是零拷貝。 fd_in參數是待輸入描述符。如果它是一個管道文件…

sys/queue.h

概述 sys/queue.h是LINUX/UNIX系統下面的一個標準頭文件&#xff0c;用一系列的數據結構定義了一隊列。包括singly-lined list, list, simple queue(Singly-linked Tail queue), tail queue, circle queue五種。 引用此頭文件對這五種數據結構的描述&#xff1a; A singly-lin…

sys/queue.h分析(圖片復制不過來,查看原文)

這兩天有興趣學習使用了下系統頭文件sys/queue.h中的鏈表/隊列的實現&#xff0c;感覺實現的很是優美&#xff0c;關鍵是以后再也不需要自己實現這些基本的數據結構了&#xff0c;哈哈&#xff01; 我的系統環境是 正好需要使用隊列&#xff0c;那么本篇就以其中的尾隊列&…

線程池原理及C語言實現線程池

備注&#xff1a;該線程池源碼參考自傳直播客培訓視頻配套資料&#xff1b; 源碼&#xff1a;https://pan.baidu.com/s/1zWuoE3q0KT5TUjmPKTb1lw 密碼&#xff1a;pp42 引言&#xff1a;線程池是一種多線程處理形式&#xff0c;大多用于高并發服務器上&#xff0c;它能合理有效…

iptables 的mangle表

mangle表的主要功能是根據規則修改數據包的一些標志位&#xff0c;以便其他規則或程序可以利用這種標志對數據包進行過濾或策略路由。 內網的客戶機通過Linux主機連入Internet&#xff0c;而Linux主機與Internet連接時有兩條線路&#xff0c;它們的網關如圖所示。現要求對內網進…

Linux常用命令(一)

history 查看歷史命令 ctrlp 向上翻歷史紀錄 ctrln 向下翻歷史紀錄 ctrlb 光標向左移動 ctrlf 光標向右移動 ctrla 光標移動到行首 ctrle 光標移動到行尾 ctrlh 刪除光標前一個 ctrld 刪除光標后一個 ctrlu 刪除光標前所有 ctrlL clear命令 清屏 tab鍵可以補全命令/填充路徑…

ip route / ip rule /iptables 配置策略路由

Linux 使用 ip route , ip rule , iptables 配置策略路由 要求192.168.0.100以內的使用 10.0.0.1 網關上網&#xff0c;其他IP使用 20.0.0.1 上網。 首先要在網關服務器上添加一個默認路由&#xff0c;當然這個指向是絕大多數的IP的出口網關。 ip route add default gw 20.0.0.…

iptables:tproxy做透明代理

什么是透明代理 客戶端向真實服務器發起連接&#xff0c;代理機冒充服務器與客戶端建立連接&#xff0c;并以客戶端ip與真實服務器建立連接進行代理轉發。因此對于客戶端與服務器來說&#xff0c;代理機都是透明的。 如何建立透明代理 本地socket捕獲數據包 nat方式 iptables…

編譯參數(-D)

程序中可以使用#ifdef來控制輸出信息 #include<stdio.h> #define DEBUGint main() {int a 10;int b 20;int sum a b; #ifdef DEBUGprintf("%d %d %d\n",a,b,sum); #endifreturn 0; } 這樣在有宏定義DEBGU的時候就會有信息輸出 如果注銷掉宏定義就不會有輸…

libpcap講解與API接口函數講解

ibpcap&#xff08;Packet Capture Library&#xff09;&#xff0c;即數據包捕獲函數庫&#xff0c;是Unix/Linux平臺下的網絡數據包捕獲函數庫。它是一個獨立于系統的用戶層包捕獲的API接口&#xff0c;為底層網絡監測提供了一個可移植的框架。 一、libpcap工作原理 libpcap…

Linux常用命令(三)

man 查看幫助文檔 alias ls : 查看命令是否被封裝 echo &#xff1a; 顯示字符串到屏幕終端 echo $PATH : 將環境變量打印出來 poweroff&#xff1a;關機 rebot&#xff1a;重啟 需要管理員權限 vim是從vi發展過來的文本編輯器 命令模式&#xff1a;打開文件之后默認進入命令模…

淺談iptables防SYN Flood攻擊和CC攻擊

何為syn flood攻擊&#xff1a; SYN Flood是一種廣為人知的DoS&#xff08;拒絕服務攻擊&#xff09;是DDoS&#xff08;分布式拒絕服務攻擊&#xff09;的方式之一&#xff0c;這是一種利用TCP協議缺陷&#xff0c;發送大量偽造的TCP連接請求&#xff0c;從而使得被攻擊方資源…

Linux之靜態庫

命名規則&#xff1a; lib 庫的名字 .a 制作步驟 生成對應.o文件 .c .o 將生成的.o文件打包 ar rcs 靜態庫的名字&#xff08;libMytest.a&#xff09; 生成的所有的.o 發布和使用靜態庫&#xff1a; 1&#xff09; 發布靜態 2&#xff09; 頭文件 文件如下圖所示&…

iptables詳解和練習

防火墻&#xff0c;其實說白了講&#xff0c;就是用于實現Linux下訪問控制的功能的&#xff0c;它分為硬件的或者軟件的防火墻兩種。無論是在哪個網絡中&#xff0c;防火墻工作的地方一定是在網絡的邊緣。而我們的任務就是需要去定義到底防火墻如何工作&#xff0c;這就是防火墻…

Linux之動態庫

命令規則 lib 名字 .so 制作步驟 1&#xff09;生成與位置無關的代碼&#xff08;生成與位置無關的代碼&#xff09; 2&#xff09;將.o打包成共享庫&#xff08;動態庫&#xff09; 發布和使用共享庫 動態庫運行原理&#xff1a; 生成動態庫&#xff1a; gcc -fPIC -c *.c -…

linux下源碼安裝vsftpd-3.0.2

1&#xff09;在http://vsftpd.beasts.org/網站中查找并下載 vsftpd-3.0.2.tar.gz源碼包 2)如果自己的機器上安裝有yum可以用yum grouplist | less指令查看以下開發環境&#xff0c;當然這一步不做也行 3&#xff09;拆解源碼包 4&#xff09;查看源碼包 5&#xff09;編輯…

Linux之GDB調試命令

gdb啟動 gdb 程序名 l 查看源代碼&#xff08;默認顯示十行&#xff09; l 文件名&#xff1a;行數 l 文件名&#xff1a;函數名 添加斷點 break 行數 &#xff08;b 也行&#xff09; b 15 if i 15 條件斷點 i b 查看斷點信息 start 程序執行一步 n 單步調試 s 單步&#xf…

Gdb 調試core文件詳解

一&#xff0c;什么是coredump 我們經常聽到大家說到程序core掉了&#xff0c;需要定位解決&#xff0c;這里說的大部分是指對應程序由于各種異常或者bug導致在運行過程中異常退出或者中止&#xff0c;并且在滿足一定條件下&#xff08;這里為什么說需要滿足一定的條件呢&#…