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區別寫得好數組
在這裏,筆者強烈推薦《完全學會使用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"); } } }
接下來咱們將上面程序的第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 #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模式下的讀寫問題,咱們必須實現:
請思考如下一種場景:在某一時刻,有多個鏈接同時到達,服務器的 TCP 就緒隊列瞬間積累多個就緒鏈接,因爲是邊緣觸發模式,epoll 只會通知一次,accept 只處理一個鏈接,致使 TCP 就緒隊列中剩下的鏈接都得不處處理。在這種情形下,咱們應該如何有效的處理呢?
解決的方法是:解決辦法是用 while 循環抱住 accept 調用,處理完 TCP 就緒隊列中的全部鏈接後再退出循環。如何知道是否處理完就緒隊列中的全部鏈接呢? accept 返回 -1 而且 errno 設置爲 EAGAIN 就表示全部鏈接都處理完。
關於ET的accept問題,這篇博文的參考價值很高,若是有興趣,能夠連接過去圍觀一下。
由於ET模式下的讀寫須要一直讀或寫直到出錯(對於讀,當讀到的實際字節數小於請求字節數時就能夠中止),而若是你的文件描述符若是不是非阻塞的,那這個一直讀或一直寫勢必會在最後一次阻塞。這樣就不能在阻塞在epoll_wait上了,形成其餘文件描述符的任務飢餓。
這樣的實例,網上已經有不少了(包括參考連接),筆者這裏就略過了。
LT:水平觸發,效率會低於ET觸發,尤爲在大併發,大流量的狀況下。可是LT對代碼編寫要求比較低,不容易出現問題。LT模式服務編寫上的表現是:只要有數據沒有被獲取,內核就不斷通知你,所以不用擔憂事件丟失的狀況。
ET:邊緣觸發,效率很是高,在併發,大流量的狀況下,會比LT少不少epoll的系統調用,所以效率高。可是對編程要求高,須要細緻的處理每一個請求,不然容易發生丟失事件的狀況。
從本質上講:與LT相比,ET模型是經過減小系統調用來達到提升並行效率的。