epoll 機制--epoll_create, epoll_ctl和epoll_wait

名詞解釋:man epoll之後,得到如下結果: 

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有所瞭解了,代碼的解釋都已經在註釋中: 
  1. while (TRUE)   
  2. {   
  3. //等待EPOLL事件的發生,相當於監聽,至於相關的端口,需要在初始化EPOLL的時候綁定。  
  4. int nfds = epoll_wait (m_epoll_fd, m_events, MAX_EVENTS, EPOLL_TIME_OUT);   
  5. if (nfds <= 0)   
  6.    continue;   
  7. m_bOnTimeChecking = FALSE;   
  8. G_CurTime = time(NULL);   
  9. for (int i=0; i<nfds; i++)   
  10. {   
  11.    try   
  12.    {   
  13. //如果新監測到一個HTTP用戶連接到綁定的HTTP端口,建立新的連接。由於我們新採用了SOCKET連接,所以基本沒用  
  14.     if (m_events[i].data.fd == m_listen_http_fd)。   
  15.     {   
  16.      OnAcceptHttpEpoll ();   
  17.     }   
  18.     else if (m_events[i].data.fd == m_listen_sock_fd)//如果新監測到一個SOCKET用戶連接到了綁定的SOCKET端口,建立新的連接。   
  19.     {   
  20.      OnAcceptSockEpoll ();   
  21.     }   
  22.     else if (m_events[i].events & EPOLLIN)//如果是已經連接的用戶,並且收到數據,那麼進行讀入。   
  23.     {   
  24.      OnReadEpoll (i);   
  25.     }   
  26.   
  27.     OnWriteEpoll (i);//查看當前的活動連接是否有需要寫出的數據。   
  28.    }   
  29.    catch (int)   
  30.    {   
  31.     PRINTF ("CATCH捕獲錯誤\n");   
  32.     continue;   
  33.    }   
  34. }   
  35. m_bOnTimeChecking = TRUE;   
  36. OnTimer ();//進行一些定時的操作,主要就是刪除一些斷線用戶等。   
  37. }   
其實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 

用到的數據結構 
  1. typedef union epoll_data {   
  2. void *ptr;   
  3. int fd;   
  4. __uint32_t u32;   
  5. __uint64_t u64;   
  6. } epoll_data_t;   
  7.   
  8. struct epoll_event {   
  9. __uint32_t events; /* Epoll events */   
  10. epoll_data_t data; /* User data variable */   
  11. };  
如:  
  1. struct epoll_event ev;  
//設置與要處理的事件相關的文件描述符 
  1. ev.data.fd=listenfd;   
//設置要處理的事件類型 
  1. ev.events=EPOLLIN|EPOLLET;   
//註冊epoll事件 
  1. 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即可 
返回發生事件數。 

用法如下: 
  1. /*build the epoll enent for recall */   
  2. struct epoll_event ev_read[20];   
  3. int nfds = 0; //return the events count   
  4. nfds=epoll_wait(epoll_fd,ev_read,20, -1);   
  5. for(i=0; i   
  6. {   
  7. if(ev_read[i].data.fd == sock)// the listener port hava data   
  8. ......   
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程序如下: 
  1. for(;;) {   
  2.            nfds = epoll_wait(kdpfd, events, maxevents, -1);   
  3.   
  4.            for(n = 0; n < nfds; ++n) {   
  5.                if(events[n].data.fd == listener) {   
  6.                    client = accept(listener, (struct sockaddr *) &local,   
  7.                                    &addrlen);   
  8.                    if(client < 0){   
  9.                        perror("accept");   
  10.                        continue;   
  11.                    }   
  12.                    setnonblocking(client);   
  13.                    ev.events = EPOLLIN | EPOLLET;   
  14.                    ev.data.fd = client;   
  15.                    if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, client, &ev) < 0) {   
  16.                        fprintf(stderr, "epoll set insertion error: fd=%d\n",   
  17.                                client);   
  18.                        return -1;   
  19.                    }   
  20.                }   
  21.                else   
  22.                    do_use_fd(events[n].data.fd);   
  23.            }   
  24.        }   
此時使用的是ET模式,即,邊沿觸發,類似於電平觸發,epoll中的邊沿觸發的意思是隻對新到的數據進行通知,而內核緩衝區中如果是舊數據則不進行通知,所以在do_use_fd函數中應該使用如下循環,才能將內核緩衝區中的數據讀完。       
  1. while (1) {   
  2.           len = recv(*******);   
  3.           if (len == -1) {   
  4.             if(errno == EAGAIN)   
  5.                break;   
  6.             perror("recv");   
  7.             break;   
  8.           }   
  9.           do something with the recved data........   
  10.        }   
在上面例子中沒有說明對於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確認。

轉載:http://hi.baidu.com/ym012/blog/item/5f3466037f38dce609fa93c3.html

發佈了49 篇原創文章 · 獲贊 21 · 訪問量 25萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章