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
");
 34         exit(-1);
 35     }
 36 
 37     int nPort = atoi(argv[1]);
 38     if (nPort < 0)
 39     {
 40         printf("Port Invalid
");
 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,都需要加上循环处理

原文地址:https://www.cnblogs.com/jabnih/p/5021185.html