epoll的兩種工作模式


epoll有兩種模式,Edge Triggered(簡稱ET) 和 Level Triggered(簡稱LT).在採用這兩種模式時要注意的是,假設採用ET模式,那么僅當狀態發生變化時才會通知,而採用LT模式類似於原來的select/poll操作,僅僅要還有沒有處理的事件就會一直通知. 

以代碼來說明問題: 
首先給出server的代碼,須要說明的是每次accept的連接,增加可讀集的時候採用的都是ET模式,並且接收緩沖區是5字節的,也就是每次僅僅接收5字節的數據: 
Java代碼   收藏代碼
  1. #include <iostream>  
  2. #include <sys/socket.h>  
  3. #include <sys/epoll.h>  
  4. #include <netinet/in.h>  
  5. #include <arpa/inet.h>  
  6. #include <fcntl.h>  
  7. #include <unistd.h>  
  8. #include <stdio.h>  
  9. #include <errno.h>  
  10.   
  11. using namespace std;  
  12.   
  13. #define MAXLINE 5  
  14. #define OPEN_MAX 100  
  15. #define LISTENQ 20  
  16. #define SERV_PORT 5000  
  17. #define INFTIM 1000  
  18.   
  19. void setnonblocking(int sock)  
  20. {  
  21.     int opts;  
  22.     opts=fcntl(sock,F_GETFL);  
  23.     if(opts<0)  
  24.     {  
  25.         perror("fcntl(sock,GETFL)");  
  26.         exit(1);  
  27.     }  
  28.     opts = opts|O_NONBLOCK;  
  29.     if(fcntl(sock,F_SETFL,opts)<0)  
  30.     {  
  31.         perror("fcntl(sock,SETFL,opts)");  
  32.         exit(1);  
  33.     }     
  34. }  
  35.   
  36. int main()  
  37. {  
  38.     int i, maxi, listenfd, connfd, sockfd,epfd,nfds;  
  39.     ssize_t n;  
  40.     char line[MAXLINE];  
  41.     socklen_t clilen;  
  42.     //聲明epoll_event結構體的變量,ev用於注冊事件,數組用於回傳要處理的事件  
  43.     struct epoll_event ev,events[20];  
  44.     //生成用於處理accept的epoll專用的文件描寫敘述符  
  45.     epfd=epoll_create(256);  
  46.     struct sockaddr_in clientaddr;  
  47.     struct sockaddr_in serveraddr;  
  48.     listenfd = socket(AF_INET, SOCK_STREAM, 0);  
  49.     //把socket設置為非堵塞方式  
  50.     //setnonblocking(listenfd);  
  51.     //設置與要處理的事件相關的文件描寫敘述符  
  52.     ev.data.fd=listenfd;  
  53.     //設置要處理的事件類型  
  54.     ev.events=EPOLLIN|EPOLLET;  
  55.     //ev.events=EPOLLIN;  
  56.     //注冊epoll事件  
  57.     epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);  
  58.     bzero(&serveraddr, sizeof(serveraddr));  
  59.     serveraddr.sin_family = AF_INET;  
  60.     char *local_addr="127.0.0.1";  
  61.     inet_aton(local_addr,&(serveraddr.sin_addr));//htons(SERV_PORT);  
  62.     serveraddr.sin_port=htons(SERV_PORT);  
  63.     bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));  
  64.     listen(listenfd, LISTENQ);  
  65.     maxi = 0;  
  66.     for ( ; ; ) {  
  67.         //等待epoll事件的發生  
  68.         nfds=epoll_wait(epfd,events,20,500);  
  69.         //處理所發生的全部事件       
  70.         for(i=0;i<nfds;++i)  
  71.         {  
  72.             if(events[i].data.fd==listenfd)  
  73.             {  
  74.                 connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen);  
  75.                 if(connfd<0){  
  76.                     perror("connfd<0");  
  77.                     exit(1);  
  78.                 }  
  79.                 //setnonblocking(connfd);  
  80.                 char *str = inet_ntoa(clientaddr.sin_addr);  
  81.                 cout << "accapt a connection from " << str << endl;  
  82.                 //設置用於讀操作的文件描寫敘述符  
  83.                 ev.data.fd=connfd;  
  84.                 //設置用於注測的讀操作事件  
  85.                 ev.events=EPOLLIN|EPOLLET;  
  86.                 //ev.events=EPOLLIN;  
  87.                 //注冊ev  
  88.                 epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);  
  89.             }  
  90.             else if(events[i].events&EPOLLIN)  
  91.             {  
  92.                 cout << "EPOLLIN" << endl;  
  93.                 if ( (sockfd = events[i].data.fd) < 0)   
  94.                     continue;  
  95.                 if ( (n = read(sockfd, line, MAXLINE)) < 0) {  
  96.                     if (errno == ECONNRESET) {  
  97.                         close(sockfd);  
  98.                         events[i].data.fd = -1;  
  99.                     } else  
  100.                         std::cout<<"readline error"<<std::endl;  
  101.                 } else if (n == 0) {  
  102.                     close(sockfd);  
  103.                     events[i].data.fd = -1;  
  104.                 }  
  105.                 line[n] = '\0';  
  106.                 cout << "read " << line << endl;  
  107.                 //設置用於寫操作的文件描寫敘述符  
  108.                 ev.data.fd=sockfd;  
  109.                 //設置用於注測的寫操作事件  
  110.                 ev.events=EPOLLOUT|EPOLLET;  
  111.                 //改動sockfd上要處理的事件為EPOLLOUT  
  112.                 //epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);  
  113.             }  
  114.             else if(events[i].events&EPOLLOUT)  
  115.             {     
  116.                 sockfd = events[i].data.fd;  
  117.                 write(sockfd, line, n);  
  118.                 //設置用於讀操作的文件描寫敘述符  
  119.                 ev.data.fd=sockfd;  
  120.                 //設置用於注測的讀操作事件  
  121.                 ev.events=EPOLLIN|EPOLLET;  
  122.                 //改動sockfd上要處理的事件為EPOLIN  
  123.                 epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);  
  124.             }  
  125.         }  
  126.     }  
  127.     return 0;  
  128. }  


以下給出測試所用的Perl寫的client端,在client中發送10字節的數據,同一時候讓client在發送完數據之后進入死循環, 也就是在發送完之后連接的狀態不發生改變--既不再發送數據, 也不關閉連接,這樣才干觀察出server的狀態: 
Java代碼   收藏代碼
  1. #!/usr/bin/perl  
  2.   
  3. use IO::Socket;  
  4.   
  5. my $host = "127.0.0.1";  
  6. my $port = 5000;  
  7.   
  8. my $socket = IO::Socket::INET->new("$host:$port") or die "create socket error $@";  
  9. my $msg_out = "1234567890";  
  10. print $socket $msg_out;  
  11. print "now send over, go to sleep \n";  
  12.   
  13. while (1)  
  14. {  
  15.     sleep(1);  
  16. }  

執行server和client發現,server只讀取了5字節的數據,而client事實上發送了10字節的數據,也就是說,server僅當第一次監聽到了EPOLLIN事件,因為沒有讀取完數據,並且採用的是ET模式,狀態在此之后不發生變化,因此server再也接收不到EPOLLIN事件了. 
(友情提示:上面的這個測試client,當你關閉它的時候會再次出發IO可讀事件給server,此時server就會去讀取剩下的5字節數據了,可是這一事件與前面描寫敘述的ET性質並不矛盾.) 

假設我們把client改為這樣: 
Java代碼   收藏代碼
  1. #!/usr/bin/perl  
  2.   
  3. use IO::Socket;  
  4.   
  5. my $host = "127.0.0.1";  
  6. my $port = 5000;  
  7.   
  8. my $socket = IO::Socket::INET->new("$host:$port") or die "create socket error $@";  
  9. my $msg_out = "1234567890";  
  10. print $socket $msg_out;  
  11. print "now send over, go to sleep \n";  
  12. sleep(5);  
  13. print "5 second gone send another line\n";  
  14. print $socket $msg_out;  
  15.   
  16. while (1)  
  17. {  
  18.     sleep(1);  
  19. }  


能夠發現,在server接收完5字節的數據之后一直監聽不到client的事件,而當client休眠5秒之后又一次發送數據,server再次監聽到了變化,僅僅只是由於僅僅是讀取了5個字節,仍然有10個字節的數據(client第二次發送的數據)沒有接收完. 

假設上面的實驗中,對accept的socket都採用的是LT模式,那么僅僅要還有數據留在buffer中,server就會繼續得到通知,讀者能夠自行修改代碼進行實驗. 

基於這兩個實驗,能夠得出這種結論:ET模式僅當狀態發生變化的時候才獲得通知,這里所謂的狀態的變化並不包含緩沖區中還有未處理的數據,也就是說,假設要採用ET模式,須要一直read/write直到出錯為止,非常多人反映為什么採用ET模式僅僅接收了一部分數據就再也得不到通知了,大多由於這樣;而LT模式是僅僅要有數據沒有處理就會一直通知下去的. 
補充說明一下這里一直強調的"狀態變化"是什么: 

1)對於監聽可讀事件時,假設是socket是監聽socket,那么當有新的主動連接到來為狀態發生變化;對一般的socket而言,協議棧中相應的緩沖區有新的數據為狀態發生變化.可是,假設在一個時間同一時候接收了N個連接(N>1),可是監聽socket僅僅accept了一個連接,那么其他未 accept的連接將不會在ET模式下給監聽socket發出通知,此時狀態不發生變化;對於一般的socket,就如樣例中而言,假設相應的緩沖區本身已經有了N字節的數據,而僅僅取出了小於N字節的數據,那么殘存的數據不會造成狀態發生變化. 

2)對於監聽可寫事件時,同理可推,不再詳述. 

而不論是監聽可讀還是可寫,對方關閉socket連接都將造成狀態發生變化,比方在樣例中,假設強行中斷client腳本,也就是主動中斷了socket連接,那么都將造成server端發生狀態的變化,從而server得到通知,將已經在本方緩沖區中的數據讀出. 

把前面的描寫敘述能夠總結例如以下:僅當對方的動作(發出數據,關閉連接等)造成的事件才干導致狀態發生變化,而本方協議棧中已經處理的事件(包含接收了對方的數據,接收了對方的主動連接請求)並非造成狀態發生變化的必要條件,狀態變化一定是對方造成的.所以在ET模式下的,必須一直處理到出錯或者全然處理完成,才干進行下一個動作,否則可能會錯誤發生. 

另外,從這個樣例中,也能夠闡述一些主要的網絡編程概念.首先,連接的兩端中,一端發送成功並不代表着對方上層應用程序接收成功, 就拿上面的client測試程序來說,10字節的數據已經發送成功,可是上層的server並沒有調用read讀取數據,因此發送成功只說明了數據被對方的協議棧接收存放在了相應的buffer中,而上層的應用程序是否接收了這部分數據不得而知;相同的,讀取數據時也只代表着本方協議棧的相應buffer中有數據可讀,而此時時候在對端是否在發送數據也不得而知. 




epoll精髓 
在linux的網絡編程中,非常長的時間都在使用select來做事件觸發。在linux新的內核中,有了一種替換它的機制,就是epoll。

 
相比於select。epoll最大的優點在於它不會隨着監聽fd數目的增長而減少效率。由於在內核中的select實現中,它是採用輪詢來處理的,輪詢的fd數目越多,自然耗時越多。而且。在linux/posix_types.h頭文件有這種聲明: 
#define __FD_SETSIZE    1024 
表示select最多同一時候監聽1024個fd,當然,能夠通過改動頭文件再重編譯內核來擴大這個數目,但這似乎並不治本。 

epoll的接口很easy。一共就三個函數: 
1. int epoll_create(int size); 
創建一個epoll的句柄。size用來告訴內核這個監聽的數目一共同擁有多大。這個參數不同於select()中的第一個參數,給出最大監聽的fd+1的值。

須要注意的是,當創建好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()是在監聽事件時告訴內核要監聽什么類型的事件,而是在這里先注冊要監聽的事件類型。

第一個參數是epoll_create()的返回值。第二個參數表示動作,用三個宏來表示: 
EPOLL_CTL_ADD:注冊新的fd到epfd中; 
EPOLL_CTL_MOD:改動已經注冊的fd的監聽事件; 
EPOLL_CTL_DEL:從epfd中刪除一個fd; 
第三個參數是須要監聽的fd。第四個參數是告訴內核須要監聽什么事,struct epoll_event結構例如以下: 

Java代碼   收藏代碼
  1. struct epoll_event {  
  2.   __uint32_t events;  /* Epoll events */  
  3.   epoll_data_t data;  /* User data variable */  
  4. };  


events能夠是下面幾個宏的集合: 
EPOLLIN :表示相應的文件描寫敘述符能夠讀(包含對端SOCKET正常關閉); 
EPOLLOUT:表示相應的文件描寫敘述符能夠寫; 
EPOLLPRI:表示相應的文件描寫敘述符有緊急的數據可讀(這里應該表示有帶外數據到來); 
EPOLLERR:表示相應的文件描寫敘述符錯誤發生; 
EPOLLHUP:表示相應的文件描寫敘述符被掛斷; 
EPOLLET: 將EPOLL設為邊緣觸發(Edge Triggered)模式。這是相對於水平觸發(Level Triggered)來說的。 
EPOLLONESHOT:僅僅監聽一次事件。當監聽完這次事件之后,假設還須要繼續監聽這個socket的話,須要再次把這個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表示已超時。 

-------------------------------------------------------------------------------------------- 

從man手冊中。得到ET和LT的詳細描寫敘述例如以下 

EPOLL事件有兩種模型: 
Edge Triggered (ET) 
Level Triggered (LT) 

假如有這樣一個樣例: 
1. 我們已經把一個用來從管道中讀取數據的文件句柄(RFD)加入到epoll描寫敘述符 
2. 這個時候從管道的還有一端被寫入了2KB的數據 
3. 調用epoll_wait(2),而且它會返回RFD,說明它已經准備好讀取操作 
4. 然后我們讀取了1KB的數據 
5. 調用epoll_wait(2)...... 

Edge Triggered 工作模式: 
假設我們在第1步將RFD加入到epoll描寫敘述符的時候使用了EPOLLET標志。那么在第5步調用epoll_wait(2)之后將有可能會掛起,由於剩余的數據還存在於文件的輸入緩沖區內。並且數據發出端還在等待一個針對已經發出數據的反饋信息。

僅僅有在監視的文件句柄上發生了某個事件的時候 ET 工作模式才會匯報事件。因此在第5步的時候,調用者可能會放棄等待仍在存在於文件輸入緩沖區內的剩余數據。在上面的樣例中,會有一個事件產生在RFD句柄上。由於在第2步運行了一個寫操作,然后,事件將會在第3步被銷毀。

由於第4步的讀取操作沒有讀空文件輸入緩沖區內的數據,因此我們在第5步調用 epoll_wait(2)完畢后。是否掛起是不確定的。

epoll工作在ET模式的時候,必須使用非堵塞套接口。以避免因為一個文件句柄的堵塞讀/堵塞寫操作把處理多個文件描寫敘述符的任務餓死。最好以以下的方式調用ET模式的epoll接口,在后面會介紹避免可能的缺陷。 
   i    基於非堵塞文件句柄 
   ii   僅僅有當read(2)或者write(2)返回EAGAIN時才須要掛起。等待。

但這並非說每次read()時都須要循環讀。直到讀到產生一個EAGAIN才覺得此次事件處理完畢。當read()返回的讀到的數據長度小於請求的數據長度時,就能夠確定此時緩沖中已沒有數據了。也就能夠覺得此事讀事件已處理完畢。 

Level Triggered 工作模式 
相反的,以LT方式調用epoll接口的時候,它就相當於一個速度比較快的poll(2),而且不管后面的數據是否被使用,因此他們具有相同的職能。由於即使使用ET模式的epoll,在收到多個chunk的數據的時候仍然會產生多個事件。調用者能夠設定EPOLLONESHOT標志,在 epoll_wait(2)收到事件后epoll會與事件關聯的文件句柄從epoll描寫敘述符中禁止掉。因此當EPOLLONESHOT設定后,使用帶有 EPOLL_CTL_MOD標志的epoll_ctl(2)處理文件句柄就成為調用者必須作的事情。 


然后詳解ET, LT: 

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確認(這句話不理解)。

 

在很多測試中我們會看到假設沒有大量的idle -connection或者dead-connection。epoll的效率並不會比select/poll高非常多,可是當我們遇到大量的idle- connection(比如WAN環境中存在大量的慢速連接),就會發現epoll的效率大大高於select/poll。

(未測試) 



另外。當使用epoll的ET模型來工作時,當產生了一個EPOLLIN事件后, 
讀數據的時候須要考慮的是當recv()返回的大小假設等於請求的大小,那么非常有可能是緩沖區還有數據未讀完,也意味着該次事件還沒有處理完,所以還須要再次讀取: 

Java代碼   收藏代碼
  1. while(rs)  
  2. {  
  3.   buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);  
  4.   if(buflen < 0)  
  5.   {  
  6.     // 因為是非堵塞的模式,所以當errno為EAGAIN時,表示當前緩沖區已無數據可讀  
  7.     // 在這里就當作是該次事件已處理處.  
  8.     if(errno == EAGAIN)  
  9.      break;  
  10.     else  
  11.      return;  
  12.    }  
  13.    else if(buflen == 0)  
  14.    {  
  15.      // 這里表示對端的socket已正常關閉.  
  16.    }  
  17.    if(buflen == sizeof(buf)  
  18.      rs = 1;   // 須要再次讀取  
  19.    else  
  20.      rs = 0;  
  21. }  



還有,假如發送端流量大於接收端的流量(意思是epoll所在的程序讀比轉發的socket要快),因為是非堵塞的socket,那么send()函數盡管返回,但實際緩沖區的數據並未真正發給接收端,這樣不斷的讀和發。當緩沖區滿后會產生EAGAIN錯誤(參考man send),同一時候,不理會這次請求發送的數據.所以,須要封裝socket_send()的函數用來處理這樣的情況,該函數會盡量將數據寫完再返回,返回-1表示出錯。在socket_send()內部,當寫緩沖已滿(send()返回-1,且errno為EAGAIN),那么會等待后再重試.這樣的方式並不非常完美,在理論上可能會長時間的堵塞在socket_send()內部,但暫沒有更好的辦法. 

Java代碼   收藏代碼
  1. ssize_t socket_send(int sockfd, const char* buffer, size_t buflen)  
  2. {  
  3.   ssize_t tmp;  
  4.   size_t total = buflen;  
  5.   const char *p = buffer;  
  6.   
  7.   while(1)  
  8.   {  
  9.     tmp = send(sockfd, p, total, 0);  
  10.     if(tmp < 0)  
  11.     {  
  12.       // 當send收到信號時,能夠繼續寫,但這里返回-1.  
  13.       if(errno == EINTR)  
  14.         return -1;  
  15.   
  16.       // 當socket是非堵塞時,如返回此錯誤,表示寫緩沖隊列已滿,  
  17.       // 在這里做延時后再重試.  
  18.       if(errno == EAGAIN)  
  19.       {  
  20.         usleep(1000);  
  21.         continue;  
  22.       }  
  23.   
  24.       return -1;  
  25.     }  
  26.   
  27.     if((size_t)tmp == total)  
  28.       return buflen;  
  29.   
  30.     total -= tmp;  
  31.     p += tmp;  
  32.   }  
  33.   
  34.   return tmp;  
  35. }  

epoll為什么這么快 

epoll是多路復用IO(I/O Multiplexing)中的一種方式,可是僅用於linux2.6以上內核,在開始討論這個問題之前,先來解釋一下為什么須要多路復用IO. 
以一個生活中的樣例來解釋. 
如果你在大學中讀書,要等待一個朋友來訪,而這個朋友僅僅知道你在A號樓,可是不知道你詳細住在哪里,於是你們約好了在A號樓門口見面. 
假設你使用的堵塞IO模型來處理這個問題,那么你就僅僅能一直守候在A號樓門口等待朋友的到來,在這段時間里你不能做別的事情,不難知道,這樣的方式的效率是低下的. 
如今時代變化了,開始使用多路復用IO模型來處理這個問題.你告訴你的朋友來了A號樓找樓管大媽,讓她告訴你該怎么走.這里的樓管大媽扮演的就是多路復用IO的角色. 
進一步解釋select和epoll模型的差異. 
select版大媽做的是例如以下的事情:比方同學甲的朋友來了,select版大媽比較笨,她帶着朋友挨個房間進行查詢誰是同學甲,你等的朋友來了,於是在實際的代碼中,select版大媽做的是下面的事情: 
Java代碼   收藏代碼
  1. int n = select(&readset,NULL,NULL,100);   
  2. for (int i = 0; n > 0; ++i)   
  3. {   
  4.    if (FD_ISSET(fdarray[i], &readset))   
  5.    {   
  6.       do_something(fdarray[i]);   
  7.       --n;   
  8.    }  
  9. }   

epoll版大媽就比較先進了,她記下了同學甲的信息,比方說他的房間號,那么等同學甲的朋友到來時,僅僅須要告訴該朋友同學甲在哪個房間就可以,不用自己親自帶着人滿大樓的找人了.於是epoll版大媽做的事情能夠用例如以下的代碼表示: 
Java代碼   收藏代碼
  1. n=epoll_wait(epfd,events,20,500);   
  2. for(i=0;i<n;++i)   
  3. {   
  4.     do_something(events[n]);   
  5. }   
  6. 在epoll中,重要的作用結構epoll_event定義例如以下:   
  7. typedef union epoll_data {   
  8.      void *ptr;   
  9.      int fd;   
  10.      __uint32_t u32;   
  11.      __uint64_t u64;   
  12. } epoll_data_t;   
  13. struct epoll_event {   
  14.                 __uint32_t events;      /* Epoll events */   
  15.                 epoll_data_t data;      /* User data variable */   
  16. };  

能夠看到,epoll_data是一個union結構體,它就是epoll版大媽用於保存同學信息的結構體,它能夠保存非常多類型的信息:fd,指針,等等.有了這個結構體,epoll大媽能夠不用吹灰之力就能夠定位到同學甲. 
別小看了這些效率的提高,在一個大規模並發的server中,輪詢IO是最耗時間的操作之中的一個.再回到那個樣例中,假設每到來一個朋友樓管大媽都要全樓的查詢同學,那么處理的效率必定就低下了,過不久樓底就有不少的人了. 
對照最早給出的堵塞IO的處理模型, 能夠看到採用了多路復用IO之后, 程序能夠自由的進行自己除了IO操作之外的工作, 僅僅有到IO狀態發生變化的時候由多路復用IO進行通知, 然后再採取對應的操作, 而不用一直堵塞等待IO狀態發生變化了. 
從上面的分析也能夠看出,epoll比select的提高實際上是一個用空間換時間思想的詳細應用. 

多進程server中,epoll的創建應該在創建子進程之后 

看我的測試代碼,似乎應該是在創建子進程之后創建epoll的fd,否則程序將會有問題,試將代碼中兩個CreateWorker函數的調用位置分別調用,一個在創建epoll fd之前,一個在之后,在調用在創建之前的代碼會出問題,在我的機器上(linux內核2.6.26)表現的症狀就是全部進程的epoll_wait函數返回0, 而client似乎被堵塞了: 
server端: 
Java代碼   收藏代碼
  1. #include <iostream>  
  2. #include <sys/socket.h>  
  3. #include <sys/epoll.h>  
  4. #include <netinet/in.h>  
  5. #include <arpa/inet.h>  
  6. #include <fcntl.h>  
  7. #include <unistd.h>  
  8. #include <stdio.h>  
  9. #include <errno.h>  
  10. #include <sys/types.h>  
  11. #include <sys/wait.h>  
  12.   
  13. using namespace std;  
  14.   
  15. #define MAXLINE 5  
  16. #define OPEN_MAX 100  
  17. #define LISTENQ 20  
  18. #define SERV_PORT 5000  
  19. #define INFTIM 1000  
  20.   
  21. typedef struct task_t  
  22. {  
  23.     int fd;  
  24.     char buffer[100];  
  25.     int n;  
  26. }task_t;  
  27.   
  28. int CreateWorker(int nWorker)  
  29. {  
  30.     if (0 < nWorker)  
  31.     {  
  32.         bool bIsChild;  
  33.         pid_t nPid;  
  34.   
  35.         while (!bIsChild)  
  36.         {  
  37.             if (0 < nWorker)  
  38.             {  
  39.                 nPid = ::fork();  
  40.                 if (nPid > 0)  
  41.                 {  
  42.                     bIsChild = false;  
  43.                     --nWorker;  
  44.                 }  
  45.                 else if (0 == nPid)  
  46.                 {  
  47.                     bIsChild = true;  
  48.                     printf("create worker %d success!\n", ::getpid());  
  49.                 }  
  50.                 else  
  51.                 {  
  52.                     printf("fork error: %s\n", ::strerror(errno));  
  53.                     return -1;  
  54.                 }  
  55.             }  
  56.             else   
  57.             {  
  58.                 int nStatus;  
  59.                 if (-1 == ::wait(&nStatus))  
  60.                 {  
  61.                     ++nWorker;  
  62.                 }  
  63.             }  
  64.         }  
  65.     }  
  66.   
  67.     return 0;  
  68. }  
  69.   
  70. void setnonblocking(int sock)  
  71. {  
  72.     int opts;  
  73.     opts=fcntl(sock,F_GETFL);  
  74.     if(opts<0)  
  75.     {  
  76.         perror("fcntl(sock,GETFL)");  
  77.         exit(1);  
  78.     }  
  79.     opts = opts|O_NONBLOCK;  
  80.     if(fcntl(sock,F_SETFL,opts)<0)  
  81.     {  
  82.         perror("fcntl(sock,SETFL,opts)");  
  83.         exit(1);  
  84.     }     
  85. }  
  86.   
  87. int main()  
  88. {  
  89.     int i, maxi, listenfd, connfd, sockfd,epfd,nfds;  
  90.     ssize_t n;  
  91.     char line[MAXLINE];  
  92.     socklen_t clilen;  
  93.     struct epoll_event ev,events[20];  
  94.   
  95.     struct sockaddr_in clientaddr;  
  96.     struct sockaddr_in serveraddr;  
  97.     listenfd = socket(AF_INET, SOCK_STREAM, 0);  
  98.        bzero(&serveraddr, sizeof(serveraddr));  
  99.     serveraddr.sin_family = AF_INET;  
  100.     char *local_addr="127.0.0.1";  
  101.     inet_aton(local_addr,&(serveraddr.sin_addr));//htons(SERV_PORT);  
  102.     serveraddr.sin_port=htons(SERV_PORT);  
  103.       // 地址重用  
  104.     int nOptVal = 1;  
  105.     socklen_t nOptLen = sizeof(int);  
  106.     if (-1 == ::setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &nOptVal, nOptLen))  
  107.     {  
  108.         return -1;  
  109.     }      
  110.     setnonblocking(listenfd);  
  111.     bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));  
  112.     listen(listenfd, LISTENQ);      
  113.       
  114.     CreateWorker(5);  
  115.       
  116.     //把socket設置為非堵塞方式  
  117.       
  118.     //生成用於處理accept的epoll專用的文件描寫敘述符  
  119.     epfd=epoll_create(256);      
  120.     //設置與要處理的事件相關的文件描寫敘述符  
  121.     ev.data.fd=listenfd;  
  122.     //設置要處理的事件類型  
  123.     ev.events=EPOLLIN|EPOLLET;  
  124.     //ev.events=EPOLLIN;  
  125.     //注冊epoll事件  
  126.     epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);  
  127.    
  128.      //CreateWorker(5);  
  129.        
  130.     maxi = 0;  
  131.       
  132.     task_t task;   
  133.     task_t *ptask;  
  134.     while(true)   
  135.     {  
  136.         //等待epoll事件的發生  
  137.         nfds=epoll_wait(epfd,events,20,500);  
  138.         //處理所發生的全部事件       
  139.         for(i=0;i<nfds;++i)  
  140.         {  
  141.             if(events[i].data.fd==listenfd)  
  142.             {                  
  143.                 connfd = accept(listenfd,NULL, NULL);  
  144.                 if(connfd<0){                      
  145.                     printf("connfd<0, listenfd = %d\n", listenfd);  
  146.                     printf("error = %s\n", strerror(errno));  
  147.                     exit(1);  
  148.                 }  
  149.                 setnonblocking(connfd);  
  150.                  
  151.                 //設置用於讀操作的文件描寫敘述符  
  152.                 memset(&task, 0, sizeof(task));  
  153.                 task.fd = connfd;  
  154.                 ev.data.ptr = &task;  
  155.                 //設置用於注冊的讀操作事件  
  156.                 ev.events=EPOLLIN|EPOLLET;  
  157.                 //ev.events=EPOLLIN;  
  158.                 //注冊ev  
  159.                 epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);  
  160.             }  
  161.             else if(events[i].events&EPOLLIN)  
  162.             {  
  163.                 cout << "EPOLLIN" << endl;  
  164.                 ptask = (task_t*)events[i].data.ptr;  
  165.                 sockfd = ptask->fd;  
  166.                   
  167.                 if ( (ptask->n = read(sockfd, ptask->buffer, 100)) < 0) {  
  168.                     if (errno == ECONNRESET) {  
  169.                         close(sockfd);  
  170.                         events[i].data.ptr = NULL;  
  171.                     } else  
  172.                         std::cout<<"readline error"<<std::endl;  
  173.                 } else if (ptask->n == 0) {  
  174.                     close(sockfd);  
  175.                     events[i].data.ptr = NULL;  
  176.                 }  
  177.                 ptask->buffer[ptask->n] = '\0';  
  178.                 cout << "read " << ptask->buffer << endl;  
  179.                   
  180.                 //設置用於寫操作的文件描寫敘述符                                  
  181.                 ev.data.ptr = ptask;  
  182.                 //設置用於注測的寫操作事件  
  183.                 ev.events=EPOLLOUT|EPOLLET;  
  184.                                   
  185.                 //改動sockfd上要處理的事件為EPOLLOUT  
  186.                 epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);  
  187.             }  
  188.             else if(events[i].events&EPOLLOUT)  
  189.             {     
  190.                 cout << "EPOLLOUT" << endl;  
  191.                 ptask = (task_t*)events[i].data.ptr;  
  192.                 sockfd = ptask->fd;  
  193.                   
  194.                 write(sockfd, ptask->buffer, ptask->n);  
  195.                   
  196.                 //設置用於讀操作的文件描寫敘述符                
  197.                 ev.data.ptr = ptask;  
  198.                   
  199.                 //改動sockfd上要處理的事件為EPOLIN  
  200.                 epoll_ctl(epfd,EPOLL_CTL_DEL,sockfd,&ev);  
  201.                 cout << "write " << ptask->buffer;  
  202.                 memset(ptask, 0, sizeof(*ptask));  
  203.                 close(sockfd);  
  204.             }  
  205.         }  
  206.     }  
  207.     return 0;  
  208. }  

測試client: 
#!/usr/bin/perl 

use strict; 
use Socket; 
use IO::Handle; 

sub echoclient 

    my $host = "127.0.0.1"; 
    my $port = 5000; 

    my $protocol = getprotobyname("TCP"); 
    $host = inet_aton($host); 

    socket(SOCK, AF_INET, SOCK_STREAM, $protocol) or die "socket() failed: $!"; 

    my $dest_addr = sockaddr_in($port, $host); 
    connect(SOCK, $dest_addr) or die "connect() failed: $!"; 

    SOCK->autoflush(1); 

    my $msg_out = "hello world\n"; 
    print "out = ", $msg_out; 
    print SOCK $msg_out; 
    my $msg_in = <SOCK>; 
    print "in = ", $msg_in; 

    close SOCK; 


#&echoclient; 
#exit(0); 

for (my $i = 0; $i < 9999; $i++) 

    echoclient; 

我查看了lighttpd的實現,也是在創建完子進程之后才創建的epoll的fd. 
請問誰知道哪里有解說這個的文檔? 
假如fd1是由A進程增加epfd的,並且用的是ET模式,那么增加通知的是進程B,顯然B進程不會對fd1進行處理。所以以后fd1的事件再不會通知。所以 經過幾次循環之后,全部的fd都沒有事件通知了。所以epoll_wait在timeout之后就返回0了。

而在client的結果可想而知。僅僅能是被堵塞。 
也就是說, 這是一種發生在epoll fd上面的類似於"驚群"的現象. 
對於linux socket與epoll配合相關的一些心得記錄 

沒有多少高深的東西。全當記錄,盡管簡單。可是沒有做過測試還是挺easy讓人糊塗的 

     int nRecvBuf=32*1024;//設置為32K 
setsockopt(s,SOL_SOCKET,SO_RCVBUF,(const char*)&nRecvBuf,sizeof(int)); 
1、通過上面語句能夠簡單設置緩沖區大小,測試證明:跟epoll結合的時候僅僅有當單次發送的數據全被從緩沖區讀完成之后才會再次被觸發。多次發送數據假設沒有讀取完成當緩沖區未滿的時候數據不會丟失,會累加到后面。 
2、 假設緩沖區未滿。同一連接多次發送數據會多次收到EPOLLIN事件。 
單次發送數據>socket緩沖區大小的數據數據會被堵塞分次發送,所以循環接收能夠用ENLIGE錯誤推斷。 
   3、假設緩沖區滿。新發送的數據不會觸發epoll事件(也無異常)。每次recv都會為緩沖區騰出空間,僅僅有當緩沖區空暇大小可以再次接收數據epollIN事件可以再次被觸發 
接收時接收大小為0表示client斷開(不可能有0數據包觸發EPOLLIN),-1表示異常。針對errorno進行推斷能夠確定是合理異常還是須要終止的異常,>0而不等於緩沖區大小表示單次發送結束。 
   4、 假設中途暫時調整接收緩存區大小。而且在上一次中數據沒有全然接收到用戶空間,數據不會丟失。會累加在一起 

所以總結起來,系統對於數據的完整性還是做了相當的保正,至於穩定性沒有作更深一步的測試 

   新添加: 
   5、假設主accept監聽的soctet fd也設置為非堵塞,那么單純靠epoll事件來驅動的服務器模型會存在問題,並發壓力下發現。每次accept僅僅從系統中取得第一個。所以假設恰馮多個連接同一時候觸發server fd的EPOLLIN事件,在返回的event數組中體現不出來,會出現丟失事件的現象,所以當用ab等工具簡單的壓載就會發現每次都會有最后幾條信息得不到處理,原因就在於此,我如今的解決的方法是將server fd的監聽去掉。用一個線程堵塞監聽。accept成功就處理檢測client fd,然后在主線程循環監聽client事件。這樣epoll在邊緣模式下出錯的概率就小,測試表明效果明顯 
6、對於SIG部分信號還是要做屏蔽處理,不然對方socket中斷等正常事件都會引起整個服務的退出 
7、sendfile(fd, f->SL->sendBuffer.inFd, (off_t *)&f->SL->sendBuffer.offset, size_need);注意sendfile函數的地三個變量是傳送地址,偏移量會自己主動添加。不須要手動再次添加。否則就會出現文件傳送丟失現象 
8、單線程epoll驅動模型誤解:曾經我一直覺得單線程是無法處理webserver這種有嚴重網絡延遲的服務,但nginx等優秀server都是機遇事件驅動模型,開始我在些的時候也是操心這些問題,后來測試發現。當client socket設為非堵塞模式的時候,從讀取數據到解析http協議,到發送數據均在epoll的驅動下速度很快,沒有必要採用多線程,我的單核cpu (奔三)就能夠達到10000page/second,這在公網上是遠遠無法達到的一個數字(網絡延遲更為嚴重)。所以單線程的數據處理能力已經非常高了。就不須要多線程了,所不同的是你在架構server的時候須要將全部堵塞的部分拆分開來。當epoll通知你能夠讀取的時候,實際上部分數據已經到了 socket緩沖區。你所讀取用的事件是將數據從內核空間復制到用戶空間,同理,寫也是一樣的,所以epoll重要的地方就是將這兩個延時的部分做了類似的異步處理,假設不須要處理更為復雜的業務,那單線程足以滿足1000M網卡的最高要求,這才是單線程的意義。 
    我曾經構建的webserver就沒有理解epoll,採用epoll的邊緣觸發之后怕事件丟失,或者單線程處理堵塞,所以自己用多線程構建了一個任務調度器。全部收到的事件統統壓進任無調度器中,然后多任務處理。我還將read和write分別用兩個調度器處理。並打算假設中間須要特殊的耗時的處理就添加一套調度器,用少量線程+epoll的方法來題高性能,后來發現read和write部分調度器是多余的。epoll本來就是一個事件調度器,在后面再次緩存事件分部處理還不如將epoll設為水平模式,所以多此一舉。可是這個調度起還是實用處的 
   上面講到假設中間有耗時的工作。比方數據庫讀寫,外部資源請求(文件,socket)等這些操作就不能堵塞在主線程里面。所以我設計的這個任務調度器就實用了,在epoll能處理的事件驅動部分就借用epoll的。中間部分採用模塊化的設計,用函數指針達到面相對象語言中的“托付”的作用,就能夠滿足不同的須要將任務(fd標識)增加調度器。讓多線程循環運行。假設中間再次遇到堵塞就會再次增加自己定義的堵塞器,檢測完畢就增加再次存入調度器,這樣就能夠將多種復雜的任務划分開來,相當於在處理的中間環節在自己購置一個類似於epoll的事件驅動器 
    9、多系統兼容:我如今倒是認為與其構建一個多操作系統都支持的server不如構建特定系統的,假設想遷移再次修改,由於一旦兼顧到多個系統的化會大大添加系統的復雜度,而且不能最優性能,每一個系統都有自己的獨有的優化選項。所以我認為遷移的工作量遠遠小於兼顧的工作量 
10模塊化編程,盡管用c還是要講求一些模塊化的設計的。我如今才發現差點兒面相對想的語言所能實現的全部高級特性在c里面差點兒都有相應的解決的方法(臨時發現除了操作符重載),全部學過高級面相對象的語言的朋友不放把模式用c來實現,也是一種樂趣,便於維護和自己閱讀 
   11、養成凝視的好習慣 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM