accept()ing when you can’t问题分析

accept()ing when you can’t问题

在libev的官方文档中提到一个accept()ing when you can’t的问题,下面是作者对于这个问题的分析:
For example, larger servers often run out of file descriptors (because of resource limits),causing accept to fail with ENFILE but not rejecting the connection, leading to libev signalling readinesson the next iteration again (the connection still exists after all), and typically causing the program toloop at 100% CPU usage.One of the easiest ways to handle this situation is to just ignore it - when the program encounters anoverload, it will just loop until the situation is over. While this is a form of busy waiting,no OS offers an event-based way to handle this situation, so it’s the best one can do.A better way to handle the situation is to log any errors other than EAGAIN and EWOULDBLOCK,making sure not to flood the log with such messages, and continue as usual, which at least gives the user an idea of what could be wrong (“raise the ulimit!”). For extra points one could stop the ev_io watcheron the listening fd “for a while”, which reduces CPU usage.If your program is single-threaded, then you could also keep a dummy file descriptor for overloadsituations (e.g. by opening /dev/null), and when you run into ENFILE or EMFILE, close it, run accept,close that fd, and create a new dummy fd. This will gracefully refuse clients under typical overload conditions.The last way to handle it is to simply log the error and exit, as is often done with malloc failures,but this results in an easy opportunity for a DoS attack.
在上面的这段作者的分析,内容大致如下:

在大型服务器中,经过会出现描述符用完(因为linux的资源限制导致的)的,这会导致accept()失败,并返还一个ENFILE 错误,但是并没有拒绝这个连接,连接仍然在连接队列中,这导致在下一次迭代的时候,仍然会触发监听描述符的可读事件,这导致程序busy loop。一种简单的处理方式就是当程序遇到这种问题,就直接忽略掉,直到这种情况消失,显然这种方法将会导致busy waiting,一种比较好的处理方式就是记录除了EAGAIN或EWOULDBLOCK其他任何错误,告诉用户出现了某种错误,并停止监听描述符的可读事件,减少CPU的使用。如果程序是单线程的,我们可以先open /dev/null。保留一个描述符,当accept()出现ENFILE或EMFILE错误的时候,close掉/dev/null这个fd,然后accept,再close掉accept产生的fd,然后再次open/dev/null。这是一种比较优雅的方式来拒绝掉客户端的连接。最后一种方式则是遇到accept()的这种错误,直接拒绝并退出。但是显然这种方式很容易受到Dos攻击。

问题分析

我参考了陈硕的muduo库,还有陈硕对accept()问题的分析。将这个问题的解决方案总结如下:
当文件描述符达到最大值时,可以采用如下的一些解决方案:

  • 调高进程文件描述符数目
  • 忙等
  • 退出程序
  • 关闭监听套接字,那么什么时候重新打开呢?
  • 如果是epoll模型,可以改用edge trigger,问题是如果漏掉一次accept,那么程序再也不会接受到新连接
  • 准备一个空闲的文件描述符,遇到这种情况先关闭这个空闲文件,获得一个文件描述符名额,再accept(2)拿到socket连接的文件描述符,随后立刻close(2),这样就优雅的断开了与客户端的连接。最后重新打开空闲文件。把”坑”填上,以备再次出现这种情况时使用。
    上述的几个方案中,每个方案都有其优缺点,下面是对上面的几个方案的分析:

对于第一种方案,治标不治本,提高进程的文件描述符后,仍然会出现描述符不够用的情况。
对于第二种方案,libev的作者也提到了,这种方案会导致cpu busy watting。
对于第三种方案,libev的作者提到了,但是这种方案很容易受到Dos攻击。
对于第四种方案,libev的作者提到了,记录日志,然后关闭监听描述符,减少CPU的使用,
但是libev的作者并没有提及何时才能再次打开listenfd重新监听呢。显然没有一个好的时机。
对于第五种方案,改成edge trigger后,当accept遇到EMFILE/ENFILE错误的时候,下次就不会
再次触发监听描述符的可读事件了,这显然避免了cpu busy watting,但是连接仍然在连接队列
中,这导致永远不会触发下降沿/上升沿的事件,也就是说程序此后再也不能接受新的连接了。
对于第六种方案,也是目前觉得处理比较优雅的地方。

方案的实现

对于上述的第六种方案的简易实现

int idlefd = open("/dev/null",O_RDONLY | O_CLOEXEC);
connfd = accept4(listenfd,(struct sockaddr*)&peeraddr,
                &peerlen,SOCK_NONBLOCK|SOCK_CLOEXEC);
if (connfd == -1) {
//处理EMFILE错误
    if (errno == EMFILE) {

        close(idlefd);
        idlefd = accept(listenfd,NULL,NULL);
        close(idlefd);
        idlefd = open("/dev/null",O_RDONLY | O_CLOEXEC);
        continue;
    } else {
        ERR_EXIT("accept4");   
    } // end else
} //end if
©️2020 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值