epoll ET模式陷阱分析


0. 前言

  這篇文章主要記錄在使用epoll實現NIO接入時所遇到的問題。

1. epoll簡介

  epoll是Linux下提供的NIO,其主要有兩種模式,ET(Edge trige)和LT(Level trige)。在linux下使用man epoll手冊即可知道這兩種模式主要的區別:

  ET:邊緣觸發,故名思議,所添加的描述符,只在當其改變狀態的時候才會觸發一次,就如同數電里面電平的邊緣觸發。

  在man里面列舉了一個例子,當一個fd添加到epoll中時,當有2KB數據到達時,epoll_wait會返回事件個數以及其fd,此時,當程序讀取了1KB數據后繼續調用epoll_wait,1)對於ET來說會繼續等待,2)而LT則是繼續觸發返回。

 

2. epoll錯誤程序分析

  下述為錯誤的示例:

  1 #include <sys/epoll.h>
  2 #include <unistd.h>
  3 #include <fcntl.h>
  4 #include <sys/types.h>
  5 #include <sys/socket.h>
  6 #include <errno.h>
  7 #include <string.h>
  8 
  9 #define MAX_BACKLOG    256
 10 #define MAX_EVENTS    1024
 11 
 12 static int SetNonBlock(int nFd)
 13 {
 14     int nOldOpt = fcntl(nFd, F_GETFD, 0);
 15     int nNewOpt = nOldOpt | O_NONBLOCK;
 16 
 17     return fcntl(nFd, F_SETFD, nNewOpt);
 18 }
 19 
 20 static void AddEvent(int nEpfd, int nFd)
 21 {
 22     struct epoll_event event;
 23     event.data.fd = nFd;
 24     event.events = EPOLLIN | EPOLLOUT | EPOLLRDHUP | EPOLLET;
 25 
 26     return epoll_ctl(nEpfd, EPOLL_CTL_ADD, &event);
 27 }
 28 
 29 int main(int argc, char const *argv[])
 30 {
 31     if (argc != 2)
 32     {
 33         printf("usage: CMD Port\n");
 34         exit(-1);
 35     }
 36 
 37     int nPort = atoi(argv[1]);
 38     if (nPort < 0)
 39     {
 40         printf("Port Invalid\n");
 41         exit(-1);
 42     }
 43 
 44     int nSvrFd = socket(AF_INET, SOCK_STREAM, 0);
 45     //設置非阻塞
 46     SetNonBlock(nSvrFd);
 47 
 48     //綁定地址
 49     struct sockaddr_in addr;
 50     bzero(&addr, sizeof(addr));
 51 
 52     addr.sin_addr = 0;
 53     addr.sin_port = htons(argv[1]);
 54     addr.sin_family = htos(AF_INET);
 55 
 56     if (0 != bind(nSvrFd, (struct sockaddr*)&addr, sizeof(addr)))
 57     {
 58         perror("Bind Failure:");
 59         exit(-1);
 60     }
 61 
 62     //監聽端口
 63     if (0 != listen(nSvrFd, MAX_BACKLOG))
 64     {
 65         perror("Listen Failure:");
 66         exit(-1);
 67     }
 68 
 69     int nEpfd = epoll_create(1024);
 70     struct epoll_event events[MAX_EVENTS];
 71 
 72     AddEvent(nEpfd, nSvrFd);
 73 
 74     while (1)
 75     {
 76         //等待事件到來
 77         int nReadyNums = epoll_wait(nEpfd, events, MAX_EVENTS, -1);
 78 
 79         for (int i = 0; i < nReadyNums; ++i)
 80         {
 81             if (events[i].data.fd == nSvrFd)
 82             {
 83                 //這里對於ET模式來說是有問題的
 84                 int nClientFd = accept(nSvrFd, NULL, NULL);
 85                 if (-1 != nClientFd)
 86                 {
 87                     //設置為非阻塞
 88                     SetNonBlock(nClientFd);
 89                     //添加事件監聽
 90                     AddEvent(nEpfd, nClientFd);
 91                 }
 92 
 93             }  else
 94             {
 95                 //處理FD事件
 96             }
 97         }
 98     }
 99 
100     return 0;
101 }
View Code

  分析:這里的程序使用ET + 非阻塞,對於accept沒有使用循環接收,則會導致當兩個連接同時接入的時候,只觸發一次,則accept一次,另外一個則停留在SYN隊列中。當終端超時后發送FIN狀態,這邊只是將該連接標識為只讀狀態。並連接處於CLOSE_WAIT狀態。當下一次接入的時候,觸發epoll,這次accept的卻是上一次的連接,這次的連接依然停留在SYN隊列中。如果后續都是單次觸發的話,則會導致后續交易都失敗。

  這里對KEEPALIVE選項做個補充,KEEPALIVE是用於檢測到連接斷開后有操作系統自動釋放資源,但是並不會釋放SYN隊列里面的連接,也就是說,CLOSE_WAIT狀態會被清除,但是問題還是存在,會把現象遮蔽了。

  正確應該是在accept加上一個循環:

while ((nClientFd = accept(nSvrFd, NULL, NULL)) > 0)
{
     //設置為非阻塞
     SetNonBlock(nClientFd);
     //添加事件監聽
     AddEvent(nEpfd, nClientFd);
}

 

3. 總結

  對於ET模式+非阻塞,無論是recv還是accept,都需要加上循環處理


免責聲明!

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



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