• 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,都需要加上循环处理

  • 相关阅读:
    大话数据结构笔记——第二章 算法
    大话数据结构笔记——第一章 数据结构绪论
    Vue学习笔记【33】——nrm的安装使用
    Vue学习笔记【33】——相关文章
    flag-icon-css
    谷歌浏览页面常用快捷键
    PHPStorm IDE 快捷键(MAC版)
    npm install报错类似于npm WARN tar ENOENT: no such file or directory, open '*** ode_modules.staging***
    lodash
    Docker学不会?不妨看看这篇文章
  • 原文地址:https://www.cnblogs.com/jabnih/p/5021185.html
Copyright © 2020-2023  润新知