优雅的处理 accept= -1 出现errno = EMFILE 文件描述符达到上限 的问题

85 阅读4分钟

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

前言

  优雅的处理 accept= -1 出现errno = EMFILE 文件描述符达到上限 的问题

什么情况下会出现errno = EMFILE

  在Posix API 与 网络协议栈 详细介绍 一文中,我们知道了accpet函数只做两件事:1. 从全连接队列里面取出一个TCB结点 2. 为这个TCB结点分配一个fd,把fd和TCB做一个一对一对应的关系。

在这里插入图片描述   accept= -1,errno=EMFILE只有一个原因,那就是文件描述符达到上限,一个进程能够打开的文件描述符的个数是有上限的。

  所以这就衍生出了一个非常简单粗暴的解决方案:提高上限,即把一个进程能够打开的文件描述符的个数通过Linux内核参数调整,那么进程能够连接的个数自然就上去了。具体操作方法参考Linux服务器百万并发实现与问题排查一文中的error : Too many open files

  但是我们要想一下,这种方法真的可靠吗?虽然暂时的解决了问题,那么等连接又达到了上限后,怎么办呢,我们总不能无限制的去提高上限吧。所以这个方案治标不治本。

优雅的解决方案

  说实话,在我没有思考之前,我的第一反应是,accept出错就出错呗,continue就好了,不管这个连接了。但是这样是行不通的,我们可以看到上图,该TCB一直存在全连接队列里面,说明什么?epoll水平触发会一直返回 listenfd 的读事件(epoll的实现原理),而现在fd不够用,又continue,随后又触发读事件,又continue。这就形成了无效的循环。简单来说,如果不处理掉这个TCB,epoll会不停的触发listenfd的读事件。造成无效的循环流程,浪费CPU资源

  如果变成epoll边沿触发呢?边沿触发配对非阻塞和while的流程。比如现在全连接队列有10个待取的连接,而fd已经达到了上限。此时第一个TCB在accept的时候出现了EMFILE错误,那么必然是break。在没有新的连接进入之前,epoll不会再触发listenfd的读回调,那也就代表着,如果程序这个时候前面有很多连接都close掉了,而全连接队列里面的连接因为第一个连接被break,导致后面的9个待取的连接都无法执行accept的流程。这也是有问题的,也就是说,不论ET还是LT,出现errno = EMFILE,我们都需要去解决。

  假设现在fd的上限是1024,我们可以限制连接数,对连接的个数计数,最大为1000,那么在1001个连接到来时,我们可以先accept这个连接(上限是1024,那么1001肯定不会出错),然后发生一个消息给对端:send(fd,"Connection reached the upper limit"),随后断开这个连接close(fd)。这样一来,我们即处理了这个连接,又没有出错。

  如果在业务场景没有特别标注需要限制连接数的时候,我们肯定不会像上面的做法一样,浪费24个文件描述符不用。我们肯定是越能压榨机器越好,也就是说,我们对上面进行改进,我们预先霸占一个文件描述符idleFd,而不是进行连接计数,在遇到errno = EMFILE 的时候,先close(idleFd),此时程序就会有一个空闲的文件描述符供accept使用,那么clientfd=accept(),然后再把这个clientfd关掉close(clientfd),最后把原来的idleFd恢复。如此一来,是最为优雅的解决方案。

  1. 事先准备一个空闲的文件描述符 idlefd
  2. close(idlefd) ,此时程序就会有一个空闲的文件描述符供accept使用
  3. clientfd = accept()
  4. send(clientfd ,"Connection reached the upper limit")
  5. close(clientfd)
  6. 恢复idlefd的文件描述符

那么用代码来写就是如下的部分代码了。

//水平触发
int idleFd = open("/dev/null", O_RDONLY | O_CLOEXEC);

int accept_cb(int fd, int events, void *arg) {
    struct sockaddr_in client_addr;
    memset(&client_addr, 0, sizeof(struct sockaddr_in));
    socklen_t client_len = sizeof(client_addr);

    int clientfd = accept(fd, (struct sockaddr *) &client_addr, &client_len);
    if (clientfd == -1 && errno == EMFILE) {
        close(idleFd);
        idleFd = accept(fd, (struct sockaddr *) &client_addr, &client_len);
        
        send(idleFd, "Connection reached the upper limit", sizeof("Connection reached the upper limit"), 0);
        
        close(idleFd);
        idleFd = open("/dev/null", O_RDONLY | O_CLOEXEC);
    }
}
//边沿触发
int idleFd = open("/dev/null", O_RDONLY | O_CLOEXEC);

int accept_cb(int fd, int events, void *arg) {
    struct sockaddr_in client_addr;
    memset(&client_addr, 0, sizeof(struct sockaddr_in));
    socklen_t client_len = sizeof(client_addr);

    while (1) {
        int clientfd = accept(fd, (struct sockaddr *) &client_addr, &client_len);
        if (clientfd == -1) {
            if (errno == EMFILE) {
                close(idleFd);
                idleFd = accept(fd, (struct sockaddr *) &client_addr, &client_len);
                send(idleFd, "Connection reached the upper limit", sizeof("Connection reached the upper limit"), 0);
                close(idleFd);
                idleFd = open("/dev/null", O_RDONLY | O_CLOEXEC);
            }
            else if (errno == EAGAIN || errno == EWOULDBLOCK) {
                return;
            }
        }else{
            //正常流程
        }
    }
}