Linux下的I/O复用与epoll详解
前言
? ? ? I/O多路復(fù)用有很多種實(shí)現(xiàn)。在linux上,2.4內(nèi)核前主要是select和poll,自Linux 2.6內(nèi)核正式引入epoll以來,epoll已經(jīng)成為了目前實(shí)現(xiàn)高性能網(wǎng)絡(luò)服務(wù)器的必備技術(shù)。盡管他們的使用方法不盡相同,但是本質(zhì)上卻沒有什么區(qū)別。本文將重點(diǎn)探討將放在EPOLL的實(shí)現(xiàn)與使用詳解。
為什么會(huì)是EPOLL
select的缺陷
? ? ? 高并發(fā)的核心解決方案是1個(gè)線程處理所有連接的“等待消息準(zhǔn)備好”,這一點(diǎn)上epoll和select是無爭議的。但select預(yù)估錯(cuò)誤了一件事,當(dāng)數(shù)十萬并發(fā)連接存在時(shí),可能每一毫秒只有數(shù)百個(gè)活躍的連接,同時(shí)其余數(shù)十萬連接在這一毫秒是非活躍的。select的使用方法是這樣的: ? ? ? 返回的活躍連接 ==select(全部待監(jiān)控的連接)。 ? ? ? 什么時(shí)候會(huì)調(diào)用select方法呢?在你認(rèn)為需要找出有報(bào)文到達(dá)的活躍連接時(shí),就應(yīng)該調(diào)用。所以,調(diào)用select在高并發(fā)時(shí)是會(huì)被頻繁調(diào)用的。這樣,這個(gè)頻繁調(diào)用的方法就很有必要看看它是否有效率,因?yàn)?#xff0c;它的輕微效率損失都會(huì)被“頻繁”二字所放大。它有效率損失嗎?顯而易見,全部待監(jiān)控連接是數(shù)以十萬計(jì)的,返回的只是數(shù)百個(gè)活躍連接,這本身就是無效率的表現(xiàn)。被放大后就會(huì)發(fā)現(xiàn),處理并發(fā)上萬個(gè)連接時(shí),select就完全力不從心了。 ? ? ? 此外,在 Linux內(nèi)核中,select所用到的FD_SET是有限的,即內(nèi)核中有個(gè)參數(shù)__FD_SETSIZE定義了每個(gè)FD_SET的句柄個(gè)數(shù)。view sourceprint? 1.1/linux/posix_types.h: 2.2 3.3#define __FD_SETSIZE???????? 1024 View Code ? ? ? 其次,內(nèi)核中實(shí)現(xiàn)?select是用 輪詢方法,即每次檢測(cè)都會(huì)遍歷所有FD_SET中的句柄,顯然,select函數(shù)執(zhí)行時(shí)間與FD_SET中的句柄個(gè)數(shù)有一個(gè)比例關(guān)系,即?select要檢測(cè)的句柄數(shù)越多就會(huì)越費(fèi)時(shí)。看到這里,您可能要要問了,你為什么不提poll?筆者認(rèn)為select與poll在內(nèi)部機(jī)制方面并沒有太大的差異。相比于select機(jī)制,poll只是取消了最大監(jiān)控文件描述符數(shù)限制,并沒有從根本上解決select存在的問題。 ? ? ? ? ? ? ? 接下來我們看張圖,當(dāng)并發(fā)連接為較小時(shí),select與epoll似乎并無多少差距。可是當(dāng)并發(fā)連接上來以后,select就顯得力不從心了。
? ? ? ? 圖 1.主流I/O復(fù)用機(jī)制的benchmark
?epoll高效的奧秘
? ? ? epoll精巧的使用了3個(gè)方法來實(shí)現(xiàn)select方法要做的事:
新建epoll描述符==epoll_create() epoll_ctrl(epoll描述符,添加或者刪除所有待監(jiān)控的連接) 返回的活躍連接 ==epoll_wait( epoll描述符?) ? ? ? 與select相比,epoll分清了頻繁調(diào)用和不頻繁調(diào)用的操作。例如,epoll_ctrl是不太頻繁調(diào)用的,而epoll_wait是非常頻繁調(diào)用的。這時(shí),epoll_wait卻幾乎沒有入?yún)?#xff0c;這比select的效率高出一大截,而且,它也不會(huì)隨著并發(fā)連接的增加使得入?yún)⒃桨l(fā)多起來,導(dǎo)致內(nèi)核執(zhí)行效率下降。 ? ? ?? ? ? ? 筆者在這里不想過多貼出epoll的代碼片段。如果大家有興趣,可以參考文末貼出的博文鏈接和Linux相關(guān)源碼。? ? ??要深刻理解epoll,首先得了解epoll的三大關(guān)鍵要素:mmap、紅黑樹、鏈表。
? ? ??epoll是通過內(nèi)核與用戶空間mmap同一塊內(nèi)存實(shí)現(xiàn)的。mmap將用戶空間的一塊地址和內(nèi)核空間的一塊地址同時(shí)映射到相同的一塊物理內(nèi)存地址(不管是用戶空間還是內(nèi)核空間都是虛擬地址,最終要通過地址映射映射到物理地址),使得這塊物理內(nèi)存對(duì)內(nèi)核和對(duì)用戶均可見,減少用戶態(tài)和內(nèi)核態(tài)之間的數(shù)據(jù)交換。
? ? ? epoll上就是相當(dāng)減少了epoll監(jiān)聽的句柄從用戶態(tài)copy到內(nèi)核態(tài),內(nèi)核可以直接看到epoll監(jiān)聽的句柄,效率高。
? ? ? 紅黑樹將存儲(chǔ)epoll所監(jiān)聽的套接字。上面mmap出來的內(nèi)存如何保存epoll所監(jiān)聽的套接字,必然也得有一套數(shù)據(jù)結(jié)構(gòu),epoll在實(shí)現(xiàn)上采用紅黑樹去存儲(chǔ)所有套接字,當(dāng)添加或者刪除一個(gè)套接字時(shí)(epoll_ctl),都在紅黑樹上去處理,紅黑樹本身插入和刪除性能比較好,時(shí)間復(fù)雜度O(lgN)。
? ? ??
? ? ? 下面幾個(gè)關(guān)鍵數(shù)據(jù)結(jié)構(gòu)的定義? ?
view sourceprint? 01.1struct epitem 02.2{ 03.3????struct rb_node rbn;??????????? //用于主結(jié)構(gòu)管理的紅黑樹 04.4????struct list_head rdllink;?????? //事件就緒隊(duì)列 05.5????struct epitem *next;?????????? //用于主結(jié)構(gòu)體中的鏈表 06.6????struct epoll_filefd ffd;???????? //每個(gè)fd生成的一個(gè)結(jié)構(gòu) 07.7????int nwait;???????????????? 08.8????struct list_head pwqlist;???? //poll等待隊(duì)列 09.9????struct eventpoll *ep;????????? //該項(xiàng)屬于哪個(gè)主結(jié)構(gòu)體 10.10????struct list_head fllink;???????? //鏈接fd對(duì)應(yīng)的file鏈表 11.11????struct epoll_event event;? //注冊(cè)的感興趣的事件,也就是用戶空間的epoll_event 12.12?}
? ? ??
view sourceprint? 01.1struct eventpoll 02.2{ 03.3????spin_lock_t lock;??????????? //對(duì)本數(shù)據(jù)結(jié)構(gòu)的訪問 04.4????struct mutex mtx;??????????? //防止使用時(shí)被刪除 05.5????wait_queue_head_t wq;??????? //sys_epoll_wait() 使用的等待隊(duì)列 06.6????wait_queue_head_t poll_wait; //file->poll()使用的等待隊(duì)列 07.7????struct list_head rdllist;??? //事件滿足條件的鏈表 08.8????struct rb_root rbr;????????? //用于管理所有fd的紅黑樹 09.9????struct epitem *ovflist;????? //將事件到達(dá)的fd進(jìn)行鏈接起來發(fā)送至用戶空間 10.10}
? ? ??添加以及返回事件
? ? ??通過epoll_ctl函數(shù)添加進(jìn)來的事件都會(huì)被放在紅黑樹的某個(gè)節(jié)點(diǎn)內(nèi),所以,重復(fù)添加是沒有用的。當(dāng)把事件添加進(jìn)來的時(shí)候時(shí)候會(huì)完成關(guān)鍵的一步,那就是該事件都會(huì)與相應(yīng)的設(shè)備(網(wǎng)卡)驅(qū)動(dòng)程序建立回調(diào)關(guān)系,當(dāng)相應(yīng)的事件發(fā)生后,就會(huì)調(diào)用這個(gè)回調(diào)函數(shù),該回調(diào)函數(shù)在內(nèi)核中被稱為:ep_poll_callback,這個(gè)回調(diào)函數(shù)其實(shí)就所把這個(gè)事件添加到rdlist這個(gè)雙向鏈表中。一旦有事件發(fā)生,epoll就會(huì)將該事件添加到雙向鏈表中。那么當(dāng)我們調(diào)用epoll_wait時(shí),epoll_wait只需要檢查rdlist雙向鏈表中是否有存在注冊(cè)的事件,效率非??捎^。這里也需要將發(fā)生了的事件復(fù)制到用戶態(tài)內(nèi)存中即可。
? ? ? epoll_wait的工作流程:
epoll_wait調(diào)用ep_poll,當(dāng)rdlist為空(無就緒fd)時(shí)掛起當(dāng)前進(jìn)程,直到rdlist不空時(shí)進(jìn)程才被喚醒。 文件fd狀態(tài)改變(buffer由不可讀變?yōu)榭勺x或由不可寫變?yōu)榭蓪?#xff09;,導(dǎo)致相應(yīng)fd上的回調(diào)函數(shù)ep_poll_callback()被調(diào)用。 ep_poll_callback將相應(yīng)fd對(duì)應(yīng)epitem加入rdlist,導(dǎo)致rdlist不空,進(jìn)程被喚醒,epoll_wait得以繼續(xù)執(zhí)行。 ep_events_transfer函數(shù)將rdlist中的epitem拷貝到txlist中,并將rdlist清空。 ep_send_events函數(shù)(很關(guān)鍵),它掃描txlist中的每個(gè)epitem,調(diào)用其關(guān)聯(lián)fd對(duì)用的poll方法。此時(shí)對(duì)poll的調(diào)用僅僅是取得fd上較新的events(防止之前events被更新),之后將取得的events和相應(yīng)的fd發(fā)送到用戶空間(封裝在struct?epoll_event,從epoll_wait返回)。? ???小結(jié)
? ? ? ?表 1. select、poll和epoll三種I/O復(fù)用模式的比較( 摘錄自《linux高性能服務(wù)器編程》)
| 系統(tǒng)調(diào)用 | select | poll | epoll |
| 事件集合 | 用哦過戶通過3個(gè)參數(shù)分別傳入感興趣的可讀,可寫及異常等事件 內(nèi)核通過對(duì)這些參數(shù)的在線修改來反饋其中的就緒事件 這使得用戶每次調(diào)用select都要重置這3個(gè)參數(shù) | 統(tǒng)一處理所有事件類型,因此只需要一個(gè)事件集參數(shù)。 用戶通過pollfd.events傳入感興趣的事件,內(nèi)核通過 修改pollfd.revents反饋其中就緒的事件 | 內(nèi)核通過一個(gè)事件表直接管理用戶感興趣的所有事件。 因此每次調(diào)用epoll_wait時(shí),無需反復(fù)傳入用戶感興趣 的事件。epoll_wait系統(tǒng)調(diào)用的參數(shù)events僅用來反饋就緒的事件 |
| 應(yīng)用程序索引就緒文件 描述符的時(shí)間復(fù)雜度 | O(n) | O(n) | O(1) |
| 最大支持文件描述符數(shù) | 一般有最大值限制 | 65535 | 65535 |
| 工作模式 | LT | LT | 支持ET高效模式 |
| 內(nèi)核實(shí)現(xiàn)和工作效率 | 采用輪詢方式檢測(cè)就緒事件,時(shí)間復(fù)雜度:O(n) | 采用輪詢方式檢測(cè)就緒事件,時(shí)間復(fù)雜度:O(n) | 采用回調(diào)方式檢測(cè)就緒事件,時(shí)間復(fù)雜度:O(1) |
? ? ? 行文至此,想必各位都應(yīng)該已經(jīng)明了為什么epoll會(huì)成為Linux平臺(tái)下實(shí)現(xiàn)高性能網(wǎng)絡(luò)服務(wù)器的首選I/O復(fù)用調(diào)用。
? ? ??需要注意的是:epoll并不是在所有的應(yīng)用場(chǎng)景都會(huì)比select和poll高很多。尤其是當(dāng)活動(dòng)連接比較多的時(shí)候,回調(diào)函數(shù)被觸發(fā)得過于頻繁的時(shí)候,epoll的效率也會(huì)受到顯著影響!所以,epoll特別適用于連接數(shù)量多,但活動(dòng)連接較少的情況。
? ? ? 接下來,筆者將介紹一下epoll使用方式的注意點(diǎn)。
?EPOLL的使用?
?文件描述符的創(chuàng)建?
view sourceprint? 1.1#include <sys/epoll.h> 2.2int epoll_create ( intsize );
? ? ? 在epoll早期的實(shí)現(xiàn)中,對(duì)于監(jiān)控文件描述符的組織并不是使用紅黑樹,而是hash表。這里的size實(shí)際上已經(jīng)沒有意義。
? 注冊(cè)監(jiān)控事件
view sourceprint? 1.1#include <sys/epoll.h> 2.2int epoll_ctl ( intepfd, int op, int fd, struct epoll_event *event ); ?函數(shù)說明: ? ? ?fd:要操作的文件描述符 ? ? ?op:指定操作類型 操作類型: ? ? ?EPOLL_CTL_ADD:往事件表中注冊(cè)fd上的事件 ? ? ?EPOLL_CTL_MOD:修改fd上的注冊(cè)事件 ? ? ?EPOLL_CTL_DEL:刪除fd上的注冊(cè)事件 ? ? ?event:指定事件,它是epoll_event結(jié)構(gòu)指針類型 ? ? ?epoll_event定義: view sourceprint? 1.1struct epoll_event 2.2{ 3.3????__unit32_t events;??? // epoll事件 4.4????epoll_data_t data;???? // 用戶數(shù)據(jù) 5.5}; ?結(jié)構(gòu)體說明: ? ? ?events:描述事件類型,和poll支持的事件類型基本相同(兩個(gè)額外的事件:EPOLLET和EPOLLONESHOT,高效運(yùn)作的關(guān)鍵) ? ? ?data成員:存儲(chǔ)用戶數(shù)據(jù) view sourceprint? 1.1typedef union epoll_data 2.2{ 3.3????void* ptr;????????????? //指定與fd相關(guān)的用戶數(shù)據(jù) 4.4????int fd;???????????????? //指定事件所從屬的目標(biāo)文件描述符 5.5????uint32_t u32; 6.6????uint64_t u64; 7.7} epoll_data_t;
? epoll_wait函數(shù)
view sourceprint? 1.1#include <sys/epoll.h> 2.2int epoll_wait ( intepfd, struct epoll_event* events, intmaxevents, int timeout ); View Code 函數(shù)說明: ? ? ?返回:成功時(shí)返回就緒的文件描述符的個(gè)數(shù),失敗時(shí)返回-1并設(shè)置errno ? ? ?timeout:指定epoll的超時(shí)時(shí)間,單位是毫秒。當(dāng)timeout為-1是,epoll_wait調(diào)用將永遠(yuǎn)阻塞,直到某個(gè)時(shí)間發(fā)生。當(dāng)timeout為0時(shí),epoll_wait調(diào)用將立即返回。 ? ? ?maxevents:指定最多監(jiān)聽多少個(gè)事件 ? ? ?events:檢測(cè)到事件,將所有就緒的事件從內(nèi)核事件表中復(fù)制到它的第二個(gè)參數(shù)events指向的數(shù)組中。
?EPOLLONESHOT事件
使用場(chǎng)合: ? ? ? 一個(gè)線程在讀取完某個(gè)socket上的數(shù)據(jù)后開始處理這些數(shù)據(jù),而數(shù)據(jù)的處理過程中該socket又有新數(shù)據(jù)可讀,此時(shí)另外一個(gè)線程被喚醒來讀取這些新的數(shù)據(jù)。 ? ? ? 于是,就出現(xiàn)了兩個(gè)線程同時(shí)操作一個(gè)socket的局面。可以使用epoll的EPOLLONESHOT事件實(shí)現(xiàn)一個(gè)socket連接在任一時(shí)刻都被一個(gè)線程處理。 作用: ? ? ? 對(duì)于注冊(cè)了EPOLLONESHOT事件的文件描述符,操作系統(tǒng)最多出發(fā)其上注冊(cè)的一個(gè)可讀,可寫或異常事件,且只能觸發(fā)一次。 使用: ? ? ? 注冊(cè)了EPOLLONESHOT事件的socket一旦被某個(gè)線程處理完畢,該線程就應(yīng)該立即重置這個(gè)socket上的EPOLLONESHOT事件,以確保這個(gè)socket下一次可讀時(shí),其EPOLLIN事件能被觸發(fā),進(jìn)而讓其他工作線程有機(jī)會(huì)繼續(xù)處理這個(gè)sockt。 效果: ? ? ? 盡管一個(gè)socket在不同事件可能被不同的線程處理,但同一時(shí)刻肯定只有一個(gè)線程在為它服務(wù),這就保證了連接的完整性,從而避免了很多可能的競(jìng)態(tài)條件。?LT與ET模式
? ? ? 在這里,筆者強(qiáng)烈推薦《徹底學(xué)會(huì)使用epoll》系列博文,這是筆者看過的,對(duì)epoll的ET和LT模式講解最為詳盡和易懂的博文。下面的實(shí)例均來自該系列博文。限于篇幅原因,很多關(guān)鍵的細(xì)節(jié),不能完全摘錄。
? ? ? 話不多說,直接上代碼。
程序一:
view sourceprint? 01.1#include <stdio.h> 02.2#include <unistd.h> 03.3#include <sys/epoll.h> 04.4 05.5int main(void) 06.6{ 07.7????int epfd,nfds; 08.8????struct epoll_event ev,events[5];???????????????????//ev用于注冊(cè)事件,數(shù)組用于返回要處理的事件 09.9????epfd = epoll_create(1);???????????????????????????????//只需要監(jiān)聽一個(gè)描述符——標(biāo)準(zhǔn)輸入 10.10????ev.data.fd = STDIN_FILENO; 11.11????ev.events = EPOLLIN|EPOLLET;??????????????????????? //監(jiān)聽讀狀態(tài)同時(shí)設(shè)置ET模式 12.12????epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev);??? //注冊(cè)epoll事件 13.13????for(;;) 14.14????{ 15.15????????nfds = epoll_wait(epfd, events, 5, -1); 16.16????????for(inti = 0; i < nfds; i++) 17.17????????{ 18.18????????????if(events[i].data.fd==STDIN_FILENO) 19.19????????????????printf('Something happened with stdin! 20.'); 21.20????????} 22.21????} 23.22}
編譯并運(yùn)行,結(jié)果如下:
?
當(dāng)用戶輸入一組字符,這組字符被送入buffer,字符停留在buffer中,又因?yàn)閎uffer由空變?yōu)椴豢?#xff0c;所以ET返回讀就緒,輸出”welcome to epoll's world!”。 之后程序再次執(zhí)行epoll_wait,此時(shí)雖然buffer中有內(nèi)容可讀,但是根據(jù)我們上節(jié)的分析,ET并不返回就緒,導(dǎo)致epoll_wait阻塞。(底層原因是ET下就緒fd的epitem只被放入rdlist一次)。 用戶再次輸入一組字符,導(dǎo)致buffer中的內(nèi)容增多,根據(jù)我們上節(jié)的分析這將導(dǎo)致fd狀態(tài)的改變,是對(duì)應(yīng)的epitem再次加入rdlist,從而使epoll_wait返回讀就緒,再次輸出“Welcome to epoll's world!”。接下來我們將上面程序的第11行做如下修改:
view sourceprint? 1.1?ev.events=EPOLLIN;??? //默認(rèn)使用LT模式
編譯并運(yùn)行,結(jié)果如下:
?
? ? ? 程序陷入死循環(huán),因?yàn)橛脩糨斎肴我鈹?shù)據(jù)后,數(shù)據(jù)被送入buffer且沒有被讀出,所以LT模式下每次epoll_wait都認(rèn)為buffer可讀返回讀就緒。導(dǎo)致每次都會(huì)輸出”welcome to epoll's world!”。
程序二:
view sourceprint? 01.1#include <stdio.h> 02.2#include <unistd.h> 03.3#include <sys/epoll.h> 04.4 05.5int main(void) 06.6{ 07.7????int epfd,nfds; 08.8????struct epoll_event ev,events[5];???????????????????//ev用于注冊(cè)事件,數(shù)組用于返回要處理的事件 09.9????epfd = epoll_create(1);???????????????????????????????//只需要監(jiān)聽一個(gè)描述符——標(biāo)準(zhǔn)輸入 10.10????ev.data.fd = STDIN_FILENO; 11.11????ev.events = EPOLLIN;??????????????????????????????? //監(jiān)聽讀狀態(tài)同時(shí)設(shè)置LT模式 12.12????epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev);??? //注冊(cè)epoll事件 13.13????for(;;) 14.14????{ 15.15????????nfds = epoll_wait(epfd, events, 5, -1); 16.16????????for(inti = 0; i < nfds; i++) 17.17????????{ 18.18????????????if(events[i].data.fd==STDIN_FILENO) 19.19????????????{ 20.20????????????????char buf[1024] = {0}; 21.21????????????????read(STDIN_FILENO, buf, sizeof(buf)); 22.22????????????????printf('welcome to epoll's <a href="http://www.it165.net/edu/ebg/"target="_blank" class="keylink">word</a>! 23.'); 24.23????????????}??????????? 25.24????????} 26.25????} 27.26}
編譯并運(yùn)行,結(jié)果如下:
?
? ? ? 本程序依然使用LT模式,但是每次epoll_wait返回讀就緒的時(shí)候我們都將buffer(緩沖)中的內(nèi)容read出來,所以導(dǎo)致buffer再次清空,下次調(diào)用epoll_wait就會(huì)阻塞。所以能夠?qū)崿F(xiàn)我們所想要的功能——當(dāng)用戶從控制臺(tái)有任何輸入操作時(shí),輸出”welcome to epoll's world!”
程序三:
view sourceprint? 01.1#include <stdio.h> 02.2#include <unistd.h> 03.3#include <sys/epoll.h> 04.4 05.5int main(void) 06.6{ 07.7????int epfd,nfds; 08.8????struct epoll_event ev,events[5];???????????????????//ev用于注冊(cè)事件,數(shù)組用于返回要處理的事件 09.9????epfd = epoll_create(1);???????????????????????????????//只需要監(jiān)聽一個(gè)描述符——標(biāo)準(zhǔn)輸入 10.10????ev.data.fd = STDIN_FILENO; 11.11????ev.events = EPOLLIN|EPOLLET;??????????????????????? //監(jiān)聽讀狀態(tài)同時(shí)設(shè)置ET模式 12.12????epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev);??? //注冊(cè)epoll事件 13.13????for(;;) 14.14????{ 15.15????????nfds = epoll_wait(epfd, events, 5, -1); 16.16????????for(inti = 0; i < nfds; i++) 17.17????????{ 18.18????????????if(events[i].data.fd==STDIN_FILENO) 19.19????????????{ 20.20????????????????printf('welcome to epoll's <a href="http://www.it165.net/edu/ebg/"target="_blank" class="keylink">word</a>! 21.'); 22.21????????????????ev.data.fd = STDIN_FILENO; 23.22????????????????ev.events = EPOLLIN|EPOLLET;??????????????????????? //設(shè)置ET模式 24.23????????????????epoll_ctl(epfd, EPOLL_CTL_MOD, STDIN_FILENO, &ev);??? //重置epoll事件(ADD無效) 25.24????????????}??????????? 26.25????????} 27.26????} 28.27}
編譯并運(yùn)行,結(jié)果如下:
?
? ? ?程序依然使用ET,但是每次讀就緒后都主動(dòng)的再次MOD?IN事件,我們發(fā)現(xiàn)程序再次出現(xiàn)死循環(huán),也就是每次返回讀就緒。但是注意,如果我們將MOD改為ADD,將不會(huì)產(chǎn)生任何影響。別忘了每次ADD一個(gè)描述符都會(huì)在epitem組成的紅黑樹中添加一個(gè)項(xiàng),我們之前已經(jīng)ADD過一次,再次ADD將阻止添加,所以在次調(diào)用ADD?IN事件不會(huì)有任何影響。
程序四:
view sourceprint? 01.1#include <stdio.h> 02.2#include <unistd.h> 03.3#include <sys/epoll.h> 04.4 05.5int main(void) 06.6{ 07.7????int epfd,nfds; 08.8????struct epoll_event ev,events[5];???????????????????//ev用于注冊(cè)事件,數(shù)組用于返回要處理的事件 09.9????epfd = epoll_create(1);???????????????????????????????//只需要監(jiān)聽一個(gè)描述符——標(biāo)準(zhǔn)輸入 10.10????ev.data.fd = STDOUT_FILENO; 11.11????ev.events = EPOLLOUT|EPOLLET;??????????????????????? //監(jiān)聽讀狀態(tài)同時(shí)設(shè)置ET模式 12.12????epoll_ctl(epfd, EPOLL_CTL_ADD, STDOUT_FILENO, &ev);??? //注冊(cè)epoll事件 13.13????for(;;) 14.14????{ 15.15????????nfds = epoll_wait(epfd, events, 5, -1); 16.16????????for(inti = 0; i < nfds; i++) 17.17????????{ 18.18????????????if(events[i].data.fd==STDOUT_FILENO) 19.19????????????{ 20.20????????????????printf('welcome to epoll's word! 21.'); 22.21????????????}??????????? 23.22????????} 24.23????} 25.24}
編譯并運(yùn)行,結(jié)果如下:
?
? ? ? 這個(gè)程序的功能是只要標(biāo)準(zhǔn)輸出寫就緒,就輸出“welcome to epoll's world”。我們發(fā)現(xiàn)這將是一個(gè)死循環(huán)。下面具體分析一下這個(gè)程序的執(zhí)行過程:
首先初始buffer為空,buffer中有空間可寫,這時(shí)無論是ET還是LT都會(huì)將對(duì)應(yīng)的epitem加入rdlist,導(dǎo)致epoll_wait就返回寫就緒。 程序想標(biāo)準(zhǔn)輸出輸出”welcome to epoll's world”和換行符,因?yàn)闃?biāo)準(zhǔn)輸出為控制臺(tái)的時(shí)候緩沖是“行緩沖”,所以換行符導(dǎo)致buffer中的內(nèi)容清空,這就對(duì)應(yīng)第二節(jié)中ET模式下寫就緒的第二種情況——當(dāng)有舊數(shù)據(jù)被發(fā)送走時(shí),即buffer中待寫的內(nèi)容變少得時(shí)候會(huì)觸發(fā)fd狀態(tài)的改變。所以下次epoll_wait會(huì)返回寫就緒。如此循環(huán)往復(fù)。程序五:
view sourceprint? 01.1#include <stdio.h> 02.2#include <unistd.h> 03.3#include <sys/epoll.h> 04.4 05.5int main(void) 06.6{ 07.7????int epfd,nfds; 08.8????struct epoll_event ev,events[5];???????????????????//ev用于注冊(cè)事件,數(shù)組用于返回要處理的事件 09.9????epfd = epoll_create(1);???????????????????????????????//只需要監(jiān)聽一個(gè)描述符——標(biāo)準(zhǔn)輸入 10.10????ev.data.fd = STDOUT_FILENO; 11.11????ev.events = EPOLLOUT|EPOLLET;??????????????????????? //監(jiān)聽讀狀態(tài)同時(shí)設(shè)置ET模式 12.12????epoll_ctl(epfd, EPOLL_CTL_ADD, STDOUT_FILENO, &ev);??? //注冊(cè)epoll事件 13.13????for(;;) 14.14????{ 15.15????????nfds = epoll_wait(epfd, events, 5, -1); 16.16????????for(inti = 0; i < nfds; i++) 17.17????????{ 18.18????????????if(events[i].data.fd==STDOUT_FILENO) 19.19????????????{ 20.20????????????????printf('welcome to epoll's word!'); 21.21????????????}??????????? 22.22????????} 23.23????} 24.24}
編譯并運(yùn)行,結(jié)果如下:
?
? ? ? 與程序四相比,程序五只是將輸出語句的printf的換行符移除。我們看到程序成掛起狀態(tài)。因?yàn)榈谝淮蝒poll_wait返回寫就緒后,程序向標(biāo)準(zhǔn)輸出的buffer中寫入“welcome to epoll's world!”,但是因?yàn)闆]有輸出換行,所以buffer中的內(nèi)容一直存在,下次epoll_wait的時(shí)候,雖然有寫空間但是ET模式下不再返回寫就緒?;貞浀谝还?jié)關(guān)于ET的實(shí)現(xiàn),這種情況原因就是第一次buffer為空,導(dǎo)致epitem加入rdlist,返回一次就緒后移除此epitem,之后雖然buffer仍然可寫,但是由于對(duì)應(yīng)epitem已經(jīng)不再rdlist中,就不會(huì)對(duì)其就緒fd的events的在檢測(cè)了。
程序六:
view sourceprint? 01.1#include <stdio.h> 02.2#include <unistd.h> 03.3#include <sys/epoll.h> 04.4 05.5int main(void) 06.6{ 07.7????int epfd,nfds; 08.8????struct epoll_event ev,events[5];???????????????????//ev用于注冊(cè)事件,數(shù)組用于返回要處理的事件 09.9????epfd = epoll_create(1);???????????????????????????????//只需要監(jiān)聽一個(gè)描述符——標(biāo)準(zhǔn)輸入 10.10????ev.data.fd = STDOUT_FILENO; 11.11????ev.events = EPOLLOUT;??????????????????????????????? //監(jiān)聽讀狀態(tài)同時(shí)設(shè)置LT模式 12.12????epoll_ctl(epfd, EPOLL_CTL_ADD, STDOUT_FILENO, &ev);??? //注冊(cè)epoll事件 13.13????for(;;) 14.14????{ 15.15????????nfds = epoll_wait(epfd, events, 5, -1); 16.16????????for(inti = 0; i < nfds; i++) 17.17????????{ 18.18????????????if(events[i].data.fd==STDOUT_FILENO) 19.19????????????{ 20.20????????????????printf('welcome to epoll's word!'); 21.21????????????}??????????? 22.22????????} 23.23????} 24.24}
編譯并運(yùn)行,結(jié)果如下:
?
? ? ? ?程序六相對(duì)程序五僅僅是修改ET模式為默認(rèn)的LT模式,我們發(fā)現(xiàn)程序再次死循環(huán)。這時(shí)候原因已經(jīng)很清楚了,因?yàn)楫?dāng)向buffer寫入”welcome to epoll's world!”后,雖然buffer沒有輸出清空,但是LT模式下只有buffer有寫空間就返回寫就緒,所以會(huì)一直輸出”welcome to epoll's world!”,當(dāng)buffer滿的時(shí)候,buffer會(huì)自動(dòng)刷清輸出,同樣會(huì)造成epoll_wait返回寫就緒。
程序七:
view sourceprint? 01.1#include <stdio.h> 02.2#include <unistd.h> 03.3#include <sys/epoll.h> 04.4 05.5int main(void) 06.6{ 07.7????int epfd,nfds; 08.8????struct epoll_event ev,events[5];???????????????????//ev用于注冊(cè)事件,數(shù)組用于返回要處理的事件 09.9????epfd = epoll_create(1);???????????????????????????????//只需要監(jiān)聽一個(gè)描述符——標(biāo)準(zhǔn)輸入 10.10????ev.data.fd = STDOUT_FILENO; 11.11????ev.events = EPOLLOUT|EPOLLET;??????????????????????????????? //監(jiān)聽讀狀態(tài)同時(shí)設(shè)置LT模式 12.12????epoll_ctl(epfd, EPOLL_CTL_ADD, STDOUT_FILENO, &ev);??? //注冊(cè)epoll事件 13.13????for(;;) 14.14????{ 15.15????????nfds = epoll_wait(epfd, events, 5, -1); 16.16????????for(inti = 0; i < nfds; i++) 17.17????????{ 18.18????????????if(events[i].data.fd==STDOUT_FILENO) 19.19????????????{ 20.20????????????????printf('welcome to epoll's word!'); 21.21????????????????ev.data.fd = STDOUT_FILENO; 22.22????????????????ev.events = EPOLLOUT|EPOLLET;??????????????????????? //設(shè)置ET模式 23.23????????????????epoll_ctl(epfd, EPOLL_CTL_MOD, STDOUT_FILENO, &ev);??? //重置epoll事件(ADD無效) 24.24????????????}??????????? 25.25????????} 26.26????} 27.27}
編譯并運(yùn)行,結(jié)果如下:
?
? ? ? 程序七相對(duì)于程序五在每次向標(biāo)準(zhǔn)輸出的buffer輸出”welcome to epoll's world!”后,重新MOD?OUT事件。所以相當(dāng)于每次都會(huì)返回就緒,導(dǎo)致程序循環(huán)輸出。
? ? ? 經(jīng)過前面的案例分析,我們已經(jīng)了解到,當(dāng)epoll工作在ET模式下時(shí),對(duì)于讀操作,如果read一次沒有讀盡buffer中的數(shù)據(jù),那么下次將得不到讀就緒的通知,造成buffer中已有的數(shù)據(jù)無機(jī)會(huì)讀出,除非有新的數(shù)據(jù)再次到達(dá)。對(duì)于寫操作,主要是因?yàn)镋T模式下fd通常為非阻塞造成的一個(gè)問題——如何保證將用戶要求寫的數(shù)據(jù)寫完。
? ? ? 要解決上述兩個(gè)ET模式下的讀寫問題,我們必須實(shí)現(xiàn):
對(duì)于讀,只要buffer中還有數(shù)據(jù)就一直讀; 對(duì)于寫,只要buffer還有空間且用戶請(qǐng)求寫的數(shù)據(jù)還未寫完,就一直寫。?ET模式下的accept問題
? ? ? 請(qǐng)思考以下一種場(chǎng)景:在某一時(shí)刻,有多個(gè)連接同時(shí)到達(dá),服務(wù)器的?TCP?就緒隊(duì)列瞬間積累多個(gè)就緒連接,由于是邊緣觸發(fā)模式,epoll?只會(huì)通知一次,accept?只處理一個(gè)連接,導(dǎo)致?TCP?就緒隊(duì)列中剩下的連接都得不到處理。在這種情形下,我們應(yīng)該如何有效的處理呢?
? ? ? 解決的方法是:解決辦法是用?while?循環(huán)抱住?accept?調(diào)用,處理完?TCP?就緒隊(duì)列中的所有連接后再退出循環(huán)。如何知道是否處理完就緒隊(duì)列中的所有連接呢??accept??返回?-1?并且?errno?設(shè)置為?EAGAIN?就表示所有連接都處理完。?
? ? ? 關(guān)于ET的accept問題,這篇博文的參考價(jià)值很高,如果有興趣,可以鏈接過去圍觀一下。
ET模式為什么要設(shè)置在非阻塞模式下工作
? ? ? 因?yàn)镋T模式下的讀寫需要一直讀或?qū)懼钡匠鲥e(cuò)(對(duì)于讀,當(dāng)讀到的實(shí)際字節(jié)數(shù)小于請(qǐng)求字節(jié)數(shù)時(shí)就可以停止),而如果你的文件描述符如果不是非阻塞的,那這個(gè)一直讀或一直寫勢(shì)必會(huì)在最后一次阻塞。這樣就不能在阻塞在epoll_wait上了,造成其他文件描述符的任務(wù)餓死。
epoll的使用實(shí)例
? ? ? 這樣的實(shí)例,網(wǎng)上已經(jīng)有很多了(包括參考鏈接),筆者這里就略過了。
小結(jié)
? ? ? ?LT:水平觸發(fā),效率會(huì)低于ET觸發(fā),尤其在大并發(fā),大流量的情況下。但是LT對(duì)代碼編寫要求比較低,不容易出現(xiàn)問題。LT模式服務(wù)編寫上的表現(xiàn)是:只要有數(shù)據(jù)沒有被獲取,內(nèi)核就不斷通知你,因此不用擔(dān)心事件丟失的情況。
? ? ? ?ET:邊緣觸發(fā),效率非常高,在并發(fā),大流量的情況下,會(huì)比LT少很多epoll的系統(tǒng)調(diào)用,因此效率高。但是對(duì)編程要求高,需要細(xì)致的處理每個(gè)請(qǐng)求,否則容易發(fā)生丟失事件的情況。
? ? ??從本質(zhì)上講:與LT相比,ET模型是通過減少系統(tǒng)調(diào)用來達(dá)到提高并行效率的。
總結(jié)
? ? ? epoll使用的梳理與總結(jié)到這里就告一段落了。限于篇幅原因,很多細(xì)節(jié)都被略過了。后面參考給出的鏈接,強(qiáng)烈推薦閱讀。疏謬之處,萬望斧正!
總結(jié)
以上是生活随笔為你收集整理的Linux下的I/O复用与epoll详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux epoll事件模型详解
- 下一篇: 浅析Linux Native AIO的实