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 }
分析:这里的程序使用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,都需要加上循环处理