http://blog.csdn.net/majianfei1023/article/details/45772269
歡迎轉載,轉載請注明原文地址:http://blog.csdn.net/majianfei1023/article/details/45772269
一.基本概念:
1.epoll是什么:epoll是Linux內核為處理大批量文件描述符而作了改進的poll,是Linux下多路復用IO接口select/poll的增強版本,它能顯著提高程序在大量并發連接中只有少量活躍的情況下的系統CPU利用率。另一點原因就是獲取事件的時候,它無須遍歷整個被偵聽的描述符集,只要遍歷那些被內核IO事件異步喚醒而加入就緒隊列(Ready)的描述符集合就行了。epoll除了提供select/poll那種IO事件的水平觸發(Level Triggered)外,還提供了邊緣觸發(Edge Triggered),這就使得用戶空間程序有可能緩存IO狀態,減少epoll_wait的調用,提高應用程序效率。
2.epoll,select,poll的區別:
select,poll,epoll都是IO多路復用的機制。I/O多路復用就通過一種機制,可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應的讀寫操作。
select和poll都需要在返回后,通過遍歷文件描述符來獲取已經就緒的socket。事實上,同時連接的大量客戶端在一時刻可能只有很少的處于就緒狀態,因此隨著監視的描述符數量的增長,其效率也會線性下降。
select的幾大缺點:
(1)單個進程能夠監視的文件描述符的數量存在最大限制,在Linux上一般為1024,不過可以通過修改宏定義甚至重新編譯內核的方式提升這一限制。但是隨著文件描述符數量的增大,其復制的開銷也線性增長。同時,由于網絡響應時間的延遲使得大量TCP連接處于非活躍狀態,但調用select()會對所有socket進行一次線性掃描,所以這也浪費了一定的開銷。
(2)每次調用select,都需要把fd集合從用戶態拷貝到內核態,這個開銷在fd很多時會很大
(3)同時每次調用select都需要在內核進行線性遍歷,時間復雜度為O(N),這個開銷在fd很多時也很大
poll的缺點:
它和select在本質上沒有多大差別,但是poll沒有最大文件描述符數量的限制(也就是沒有select的缺點1,卻有它的缺點2和3)。
poll和select同樣存在一個缺點就是,包含大量文件描述符的數組被整體復制于用戶態和內核的地址空間之間,而不論這些文件描述符是否就緒,它的開銷隨著文件描述符數量的增加而線性增大。
epoll的優點主要是以下幾個方面:
1. 監視的描述符數量不受限制,它所支持的FD上限是最大可以打開文件的數目,這個數字一般遠大于2048,舉個例子,在1GB內存的機器上大約是10萬左 右,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個數目和系統內存關系很大。 (我2G內存的機器上是239545)。select的最大缺點就是進程打開的fd是有數量限制的。
2. IO的效率不會隨著監視fd的數量的增長而下降。epoll不同于select和poll輪詢的方式,而是通過每個fd定義的回調函數來實現的。只有就緒的fd才會執行回調函數,時間復雜度為O(1)。
3. 支持電平觸發和邊沿觸發兩種方式(具體區別,看下面的2.2epoll詳解-工作模式),理論上邊緣觸發的性能要更高一些,但是代碼實現相當復雜。
4. mmap加速內核與用戶空間的信息傳遞。epoll是通過內核于用戶空間mmap同一塊內存,避免了無謂的內存拷貝。
通過表格來看一下更清楚。
select | poll | epoll | |
支持最大連接數 | 1024(2048) | 無上限 | 無上限 |
IO效率 | 每次調用進行線性遍歷,時間復雜度為O(N) | 每次調用進行線性遍歷,時間復雜度為O(N) | 使用“事件”通知方式,每當fd就緒,系統注冊的回調函數就會被調用,將就緒fd放到rdllist里面,這樣epoll_wait返回的時候我們就拿到了就緒的fd。時間發復雜度O(1) |
fd拷貝 | 每次select都拷貝 | 每次poll都拷貝 | 調用epoll_ctl時拷貝進內核并由內核保存,之后每次epoll_wait不拷貝 |
二.epoll詳解:
既然了解了epoll的基本概念和優點,那我們就來看看epoll怎么用。
1.epoll的接口:
epoll操作過程需要三個接口,分別如下:
- #include?<sys/epoll.h>??
- int?epoll_create(int?size);??
- int?epoll_ctl(int?epfd,?int?op,?int?fd,?struct?epoll_event?*event);??
- int?epoll_wait(int?epfd,?struct?epoll_event?*?events,?int?maxevents,?int?timeout);??
(1)?int epoll_create(int size);
創建一個epoll的句柄,size用來告訴內核這個監聽的數目一共有多大。在linux-2.4.32內核中根據size大小初始化哈希表的大小,在linux2.6.10內核中該參數無用,使用紅黑樹管理所有的文件描述符,而不是hash。需要注意的是,當創建好epoll句柄后,它就是會占用一個fd值,在linux下如果查看/proc/進程id/fd/,是能夠看到這個fd的,所以在使用完epoll后,必須調用close()關閉,否則可能導致fd被耗盡。
(2)int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注冊函數,它不同于select()在監聽事件時告訴內核要監聽什么類型的事件,而是在這里先注冊要監聽的事件類型。
第一個參數epfd是epoll_create()的返回值;
第二個參數op表示動作,用三個宏來表示:
??EPOLL_CTL_ADD:注冊新的fd到epfd中,??EPOLL_CTL_MOD:修改已經注冊的fd的監聽事件,
??EPOLL_CTL_DEL:從epfd中刪除一個fd;
第三個參數fd是需要監聽的fd;
第四個參數*event是告訴內核需要監聽什么事,struct epoll_event結構如下:
- struct?epoll_event?{??
- ??__uint32_t?events;??/*?Epoll?events?*/??
- ??epoll_data_t?data;??/*?User?data?variable?*/??
- };??
- ??
- ??
- typedef?union?epoll_data?{????
- void?*ptr;????
- int?fd;????
- __uint32_t?u32;????
- __uint64_t?u64;????
- }?epoll_data_t;??
events可以是以下幾個宏的集合:
??EPOLLIN?:表示對應的文件描述符可以讀(包括對端SOCKET正常關閉);
??EPOLLOUT:表示對應的文件描述符可以寫;
??EPOLLPRI:表示對應的文件描述符有緊急的數據可讀(這里應該表示有帶外數據到來);
??EPOLLERR:表示對應的文件描述符發生錯誤;
??EPOLLHUP:表示對應的文件描述符被掛斷;
??EPOLLET: 將EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對于水平觸發(Level Triggered)來說的。
??EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之后,如果還需要繼續監聽這個socket(也就是第三個參數fd還是epoll_data.fd)的話,需要再次把這個socket加入到EPOLL隊列里
(3)?int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的產生,類似于select()調用。參數events用來從內核得到事件的集合,maxevents告之內核這個events有多大,這個maxevents的值不能大于創建epoll_create()時的size,參數timeout是超時時間(毫秒,0會立即返回,-1將不確定,也有說法說是永久阻塞)。該函數返回需要處理的事件數目,如返回0表示已超時。
epoll_wait運行的原理是
等侍注冊在epfd上的socket fd的事件的發生,如果發生則將發生的sokct fd和事件類型放入到events數組中。
并 且將注冊在epfd上的socket fd的事件類型給清空,所以如果下一個循環你還要關注這個socket fd的話,則需要用epoll_ctl(epfd,EPOLL_CTL_MOD,listenfd,&ev)來重新設置socket fd的事件類型。這時不用EPOLL_CTL_ADD,因為socket fd并未清空,只是事件類型清空。這一步非常重要。
2.epoll工作模式:
EPOLL事件有兩種模型:
LT(level triggered-電平觸發)是缺省的工作方式,并且同時支持block和no-block socket.在這種做法中,內核告訴你一個文件描述符是否就緒了,然后你可以對這個就緒的fd進行IO操作。如果你不作任何操作或者未處理完,內核還是會繼續通知你的,所以,這種模式編程出錯誤可能性要小一點。傳統的select/poll都是這種模型的代表。
ET(edge triggered-邊緣觸發)是高速工作方式,只支持no-block socket。在這種模式下,當描述符從未就緒變為就緒時,內核通過epoll告訴你。然后它會假設你知道文件描述符已經就緒,并且不會再為那個文件描述符發送更多的就緒通知,直到你做了某些操作導致那個文件描述符不再為就緒狀態了(比如,你在發送,接收或者接收請求,或者發送接收的數據少于一定量時導致了一個EWOULDBLOCK 錯誤),邊緣觸發的意思就是在兩個狀態的臨界值進行改變的時候觸發。但是請注意,如果一直不對這個fd作IO操作(從而導致它再次變成未就緒),內核不會發送更多的通知(only once),不過在TCP協議中,ET模式的加速效用仍需要更多的benchmark確認。
? ET和LT的區別就在這里體現,LT事件不會丟棄,而是只要讀buffer里面有數據可以讓用戶讀,則不斷的通知你。而ET則只在事件發生之時通知。可以簡單理解為LT是水平觸發,而ET則為邊緣觸發。LT模式只要有事件未處理就會觸發,而ET則只在高低電平變換時(即狀態從1到0或者0到1)觸發。
? ET模式在很大程度上減少了epoll事件被重復觸發的次數,因此效率要比LT模式高。epoll工作在ET模式的時候,必須使用非阻塞套接口,以避免由于一個文件句柄的阻塞讀/阻塞寫操作把處理多個文件描述符的任務餓死。
三.epoll的例子:
- #include?<stdio.h>??
- #include?<unistd.h>??
- #include?<stdlib.h>??
- #include?<string.h>??
- #include?<sys/types.h>??
- #include?<errno.h>??
- #include?<sys/socket.h>??
- #include?<netinet/in.h>??
- #include?<sys/epoll.h>??
- #include?<fcntl.h>??
- ??
- #define?MAX_EPOLL???????1000??
- #define?BUF_SIZE????????1024??
- #define?PORT????????????6000??
- ??
- ??
- int?setnonblocking(?int?fd?)??
- {??
- ????if(?fcntl(?fd,?F_SETFL,?fcntl(?fd,?F_GETFD,?0?)|O_NONBLOCK?)?==?-1?)??
- ????{??
- ????????printf("Set?blocking?error?:?%d\n",?errno);??
- ????????return?-1;??
- ????}??
- ????return?0;??
- }??
- ??
- int?main(?int?argc,?char?**?argv?)??
- {??
- ????int?????????listen_fd;??
- ????int?????????conn_fd;??
- ????int?????????epoll_fd;??
- ????int?????????nread;??
- ????int?????i;??
- ????struct?sockaddr_in?servaddr;??
- ????struct?sockaddr_in?cliaddr;??
- ????struct??epoll_event?ev;??//監聽fd??
- ????struct??epoll_event?events[MAX_EPOLL];??
- ????char????buf[BUF_SIZE];??
- ????socklen_t???len?=?sizeof(?struct?sockaddr_in?);??
- ??????
- ????bzero(?&servaddr,?sizeof(?servaddr?)?);??
- ????servaddr.sin_family?=?AF_INET;??
- ????servaddr.sin_addr.s_addr?=?htonl(?INADDR_ANY?);??
- ????servaddr.sin_port?=?htons(?PORT?);??
- ??????
- ????if(?(?listen_fd?=?socket(?AF_INET,?SOCK_STREAM,?0?)?)?==?-1?)??
- ????{??
- ????????printf("socket?error...\n"?,?errno?);??
- ????????exit(?EXIT_FAILURE?);??
- ????}??
- ??????
- ????if(?setnonblocking(?listen_fd?)?==?-1?)??
- ????{??
- ????????printf("setnonblocking?error?:?%d\n",?errno);??
- ????????exit(?EXIT_FAILURE?);??
- ????}??
- ??????
- ????if(?bind(?listen_fd,?(?struct?sockaddr?*)&servaddr,?sizeof(?struct?sockaddr?)?)?==?-1?)??
- ????{??
- ????????printf("bind?error?:?%d\n",?errno);??
- ????????exit(?EXIT_FAILURE?);??
- ????}??
- ??
- ????if(?listen(?listen_fd,?10?)?==?-1?)??
- ????{??
- ????????printf("Listen?Error?:?%d\n",?errno);??
- ????????exit(?EXIT_FAILURE?);??
- ????}??
- ??????
- ????epoll_fd?=?epoll_create(?MAX_EPOLL?);???//1.epoll_create??
- ????ev.events?=?EPOLLIN?|?EPOLLET;??
- ????ev.data.fd?=?listen_fd;??
- ????if(?epoll_ctl(?epoll_fd,?EPOLL_CTL_ADD,?listen_fd,?&ev?)?<?0?)???????//2.epoll_ctl??
- ????{??
- ????????printf("Epoll?Error?:?%d\n",?errno);??
- ????????exit(?EXIT_FAILURE?);??
- ????}??
- ??????
- ????while(?1?)??
- ????{??
- ????????int?ready_counts?=?0;??
- ????????if(?(?ready_counts?=?epoll_wait(?epoll_fd,?events,?MAX_EPOLL,?-1?)?)?==?-1?)????????//3.epoll_wait,就緒的event在events里面??
- ????????{??
- ????????????printf(?"Epoll?Wait?Error?:?%d\n",?errno?);??
- ????????????exit(?EXIT_FAILURE?);??
- ????????}??
- ??
- ????????for(?i?=?0;?i?<?ready_counts;?i++?)??
- ????????{??
- ????????????if(?events[i].data.fd?==?listen_fd)?????//監聽端口有就緒事件??
- ????????????{??
- ????????????????if(?(?conn_fd?=?accept(?listen_fd,?(struct?sockaddr?*)&cliaddr,?&len?)?)?==?-1?)??
- ????????????????{??
- ????????????????????printf("Accept?Error?:?%d\n",?errno);??
- ????????????????????exit(?EXIT_FAILURE?);??
- ????????????????}??
- ??????????????????
- ????????????????printf(?"Server?get?from?client?!\n"/*,??inet_ntoa(cliaddr.sin_addr),?cliaddr.sin_port?*/);??
- ??????????????????
- ????????????????ev.events?=?EPOLLIN?|?EPOLLET;??
- ????????????????ev.data.fd?=?conn_fd;??
- ????????????????if(?epoll_ctl(?epoll_fd,?EPOLL_CTL_ADD,?conn_fd,?&ev?)?<?0?)??
- ????????????????{??
- ????????????????????printf("Epoll?Error?:?%d\n",?errno);??
- ????????????????????exit(?EXIT_FAILURE?);??
- ????????????????}??
- ????????????}??
- ????????????else??
- ????????????{??
- ????????????????nread?=?read(?events[i].data.fd,?buf,?sizeof(?buf?)?);??
- ????????????????if(?nread?<=?0?)??
- ????????????????{??
- ????????????????????close(?events[i].data.fd?);??
- ????????????????????epoll_ctl(?epoll_fd,?EPOLL_CTL_DEL,?events[i].data.fd,?&ev?);??
- ????????????????????continue;??
- ????????????????}??
- ??????????????????
- ????????????????write(?events[i].data.fd,?buf,?nread?);??
- ????????????}??
- ????????}??
- ????}??
- ??????
- ????close(?listen_fd?);??
- ????return?0;??
- }??
四:總結:
既然epoll有這么多優點,是不是可以取代select和poll。
什么情況下用select/poll而不用epoll呢?
1.epoll更適合于處理大量的fd ,且活躍fd不是很多的情況。
反之如果處理的fd量不大,且基本都是活躍的,epoll并不比select/poll有什么效率。相反,過多使用epoll_ctl,效率相比還有稍微的下降。這時候使用select既簡單又方便,就沒必要用epoll這么復雜的寫法了。
2.select目前幾乎在所有的平臺上支持,其良好跨平臺支持也是它的一個優點