本文已参与「新人创作礼」活动, 一起开启掘金创作之路。
前言
优雅的处理 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恢复。
如此一来,是最为优雅的解决方案。
- 事先准备一个空闲的文件描述符 idlefd
- close(idlefd) ,此时程序就会有一个空闲的文件描述符供accept使用
- clientfd = accept()
- send(clientfd ,"Connection reached the upper limit")
- close(clientfd)
- 恢复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{
//正常流程
}
}
}