原文地址::http://apps.hi.baidu.com/share/detail/31300135
NAME
epoll - I/O event notification facility
SYNOPSIS
#include <sys/epoll.h>
DEscrīptION
epoll is a variant of poll(2) that can be used either as Edge or Level
Triggered interface and scales well to large numbers of watched fds.
Three system calls are provided to set up and control an epoll set:
epoll_create(2), epoll_ctl(2), epoll_wait(2).
An epoll set is connected to a file descrīptor created by epoll_create(2). Interest for certain file descrīptors is then registered via
epoll_ctl(2). Finally, the actual wait is started by epoll_wait(2).
其實,一切的解釋都是多餘的,按照我目前的瞭解,EPOLL模型似乎只有一種格式,所以大家只要參考我下面的代碼,就能夠對EPOLL有所瞭解了,代碼的解釋都已經在注釋中:
while (TRUE)
{
int nfds = epoll_wait (m_epoll_fd, m_events, MAX_EVENTS, EPOLL_TIME_OUT);//等待EPOLL事件的發生,相當於監聽,至於相關的連接埠,需要在初始化EPOLL的時候綁定。
if (nfds <= 0)
continue;
m_bOnTimeChecking = FALSE;
G_CurTime = time(NULL);
for (int i=0; i<nfds; i++)
{
try
{
if (m_events[i].data.fd == m_listen_http_fd)//如果新監測到一個HTTP使用者串連到綁定的HTTP連接埠,建立新的串連。由於我們新採用了SOCKET串連,所以基本沒用。
{
OnAcceptHttpEpoll ();
}
else if (m_events[i].data.fd == m_listen_sock_fd)//如果新監測到一個SOCKET使用者串連到了綁定的SOCKET連接埠,建立新的串連。
{
OnAcceptSockEpoll ();
}
else if (m_events[i].events & EPOLLIN)//如果是已經串連的使用者,並且收到資料,那麼進行讀入。
{
OnReadEpoll (i);
}
OnWriteEpoll (i);//查看當前的活動串連是否有需要寫出的資料。
}
catch (int)
{
PRINTF ("CATCH捕獲錯誤\n");
continue;
}
}
m_bOnTimeChecking = TRUE;
OnTimer ();//進行一些定時的操作,主要就是刪除一些斷線使用者等。
}
其實EPOLL的精華,按照我目前的理解,也就是上述的幾段短短的代碼,看來時代真的不同了,以前如何接受大量使用者串連的問題,現在卻被如此輕鬆的搞定,真是讓人不得不感歎。
今天搞了一天的epoll,想做一個高並發的代理程式。剛開始真是鬱悶,一直搞不通,網上也有幾篇介紹epoll的文章。但都不深入,沒有將一些注意的地方講明。以至於走了很多彎路,現將自己的一些理解共用給大家,以少走彎路。
epoll用到的所有函數都是在標頭檔sys/epoll.h中聲明,有什麼地方不明白或函數忘記了可以去看一下。
epoll和select相比,最大不同在於:
1epoll返回時已經明確的知道哪個sokcet fd發生了事件,不用再一個個比對。這樣就提高了效率。
2select的FD_SETSIZE是有限止的,而epoll是沒有限止的只與系統資源有關。
1、epoll_create函數
函式宣告:int epoll_create(int size)
該 函數產生一個epoll專用的檔案描述符。它其實是在核心申請一空間,用來存放你想關注的socket fd上是否發生以及發生了什麼事件。size就是你在這個epoll fd上能關注的最大socket fd數。隨你定好了。只要你有空間。可參見上面與select之不同2.
22、epoll_ctl函數
函式宣告:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
該函數用於控制某個epoll檔案描述符上的事件,可以註冊事件,修改事件,刪除事件。
參數:
epfd:由 epoll_create 產生的epoll專用的檔案描述符;
op:要進行的操作例如註冊事件,可能的取值EPOLL_CTL_ADD 註冊、EPOLL_CTL_MOD 修 改、EPOLL_CTL_DEL 刪除
fd:關聯的檔案描述符;
event:指向epoll_event的指標;
如果調用成功返回0,不成功返回-1
用到的資料結構
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
如:
struct epoll_event ev;
//設定與要處理的事件相關的檔案描述符
ev.data.fd=listenfd;
//設定要處理的事件類型
ev.events=EPOLLIN|EPOLLET;
//註冊epoll事件
epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
常用的事件類型:
EPOLLIN :表示對應的檔案描述符可以讀;
EPOLLOUT:表示對應的檔案描述符可以寫;
EPOLLPRI:表示對應的檔案描述符有緊急的資料可讀
EPOLLERR:表示對應的檔案描述符發生錯誤;
EPOLLHUP:表示對應的檔案描述符被掛斷;
EPOLLET:表示對應的檔案描述符有事件發生;
3、epoll_wait函數
函式宣告:int epoll_wait(int epfd,struct epoll_event * events,int maxevents,int timeout)
該函數用於輪詢I/O事件的發生;
參數:
epfd:由epoll_create 產生的epoll專用的檔案描述符;
epoll_event:用於回傳代處理事件的數組;
maxevents:每次能處理的事件數目;
timeout:等待I/O事件發生的逾時值(單位我也不太清楚);-1相當於阻塞,0相當於非阻塞。一般用-1即可
返回傳生事件數目。
用法如下:
/*build the epoll enent for recall */
struct epoll_event ev_read[20];
int nfds = 0; //return the events count
nfds=epoll_wait(epoll_fd,ev_read,20, -1);
for(i=0; i
{
if(ev_read[i].data.fd == sock)// the listener port hava data
......
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並未清空,只是事件類型清空。這一步非常重要。
俺最開始就是沒有加這個,白搞了一個上午。
4單個epoll並不能解決所有問題,特別是你的每個操作都比較費時的時候,因為epoll是串列處理的。
所以你還是有必要建立線程池來發揮更大的效能。
//////////////////////////////////////////////////////////////////////////////
man中給出了epoll的用法,example程式如下:
for(;;) {
nfds = epoll_wait(kdpfd, events, maxevents, -1);
for(n = 0; n < nfds; ++n) {
if(events[n].data.fd == listener) {
client = accept(listener, (struct sockaddr *) &local,
&addrlen);
if(client < 0){
perror("accept");
continue;
}
setnonblocking(client);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = client;
if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, client, &ev) < 0) {
fprintf(stderr, "epoll set insertion error: fd=%d\n",
client);
return -1;
}
}
else
do_use_fd(events[n].data.fd);
}
}
此時使用的是ET模式,即,邊沿觸發,類似於電平觸發,epoll中的邊沿觸發的意思是只對新到的資料進行通知,而核心緩衝區中如果是舊資料則不進行通知,所以在do_use_fd函數中應該使用如下迴圈,才能將核心緩衝區中的資料讀完。
while (1) {
len = recv(*******);
if (len == -1) {
if(errno == EAGAIN)
break;
perror("recv");
break;
}
do something with the recved data........
}
在上面例子中沒有說明對於listen socket fd該如何處理,有的時候會使用兩個線程,一個用來監聽accept另一個用來監聽epoll_wait,如果是這樣使用的話,則listen socket fd使用預設的阻塞方式就行了,而如果epoll_wait和accept處於一個線程中,即,全部由epoll_wait進行監聽,則,需將listen socket fd也設定成非阻塞的,這樣,對accept也應該使用while包起來(類似於上面的recv),因為,epoll_wait返回時只是說有串連到來了,並沒有說有幾個串連,而且在ET模式下epoll_wait不會再因為上一次的串連還沒讀完而返回,這種情況確實存在,我因為這個問題而耗費了一天多的時間,這裡需要說明的是,每調用一次accept將從核心中的已串連隊列中的隊頭讀取一個串連,因為在並發訪問的環境下,有可能有多個串連“同時”到達,而epoll_wait只返回了一次。
唯一有點麻煩是epoll有2種工作方式:LT和ET。
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確認。