accept(EMFILE错误)文件描述符用尽了怎么办
Mzy 金丹

accept (EMFILE错误)文件描述符用尽解决方案

通常情况下,服务端调用 accept 函数会返回一个新的文件描述符,用于和客户端之间的数据传输

在服务器的开发中,有时会遇到这种情况:当调用 accept 函数接受客户端连接,函数返回失败,对应的错误码是 EMFILE,它表示当前进程打开的文件描述符已达上限,此时,服务器不能再接受客户端连接

当遇到上述问题,怎么合理的处理呢,下面就来分析一下

建立连接的流程

先简单回顾下客户端和服务器建立连接的流程,具体的如下图所示:

pic_ace04886.png

1
2
3
4
5
6
7
8
9
10
11
12
13
1. 客户端发起 SYN 请求



2. 服务器收到客户端的 SYN 请求后,内核把连接放入半连接队列,同时给客户端返回一个 SYN + ACK



3. 客户端向服务器返回一个确认的 ACK, 服务器收到本次 ACK 之后,三次握手完成,同时,内核把连接从半连接队列中移除,创建新完全连接,加入到全连接队列中



4. 应用层调用 accept 函数从全连接队列中取出连接

上面的第 1、第 2、第 3 步是 TCP 的三次握手,它是由内核中TCP协议完成的, 第 4 步是应用层调用 accept 接口

在 epoll 中的问题

epoll 是 Linux中IO多路复用模型,在服务器的开发中有广泛的应用,下面就以epoll为例来详细说明

服务器端创建侦听文件描述符listenfd之后, 向epoll注册读事件

epoll 检测到 listenfd 上有读事件发生,会立即通知应用层,应用层调用accept接受新连接,而此时进程打开的文件描述符数量已经达到上限了,所以每次 accept 都是失败的

这里会出现以下几个问题

  1. 由于 每次 accept 都失败了,相当于listenfd上的可读事件没有处理,epoll 会不停的触发 listenfd 上的可读事件,应用层也就会不停的调用 accept,然后又出现 accept 调用失败,如此这般不停的执行无效的循环,程序就陷入了busy loop中。CPU占用率会达到100%,影响了服务器的性能。
  2. 上面提到服务器在不停的执行无效的循环, 将会引发另一个问题,如果此时有新客户端连接到来,建立连接的过程会很慢

前面说的epoll默认是使用了水平触发模式,如果使用边沿触发模式会出现什么问题呢?

边沿触发模式下,listenfd从无读事件状态到有读事件状态时,才会通知到应用层,在应用层处理完listenfd上所有的读事件之前,epoll 不会再通知应用层

也就是说,应用层收到 listenfd 上读事件通知之后,需要把listenfd上所有的读事件全部处理完,下次listenfd上再有读事件时,才会通知应用层

回到 accept 的问题上,在边沿触发模式下,当epoll通知应用层 listenfd 上有可读事件时,应用层调用 accept, 由于此时进程打开的文件描述符数量已经达到上限了,所以 accept 调用失败,也即listenfd上的可读事件还没有处理,在应用层处理完 listenfd 上可读事件之前,epoll 不会再通知应用层 listenfd 上有可读事件

如果在应用层处理完 listenfd 上可读事件之前,有新的客户端连接到来,这个时候epoll是不会通知应用层listenfd 上有可读事件,这会导致一个严重的问题:accept 只要出现了 EMFILE的错误码,就再也无法接受客户端的连接了

所以,当出现 EMFILE 时,不管使用epoll 的水平触发模式还是边沿触发模式都会存在问题

如何解决

  1. EMFILE 表示进程打开的文件描述符数量达到上限了,可以把这个值调大些,但这治标不治本

本来系统设置文件描述符数量上限是为了限制进程对系统资源的过度占用,况且,这个值调整到多大合适呢,总不能无限大吧,所以调整上限值的方式不是最合适的方式

accept 成功时会返回一个新的文件描述符,如果此时进程打开的文件描述符已经达到上限了,就会返回失败

  1. 死等。鸵鸟算法(传说中鸵鸟看到危险就把头埋在地底下。当你对某一件事情没有一个很好的解决方法时,那就忽略它,就像鸵鸟面对危险时会把它深埋在沙砾中,装作看不到。这样的算法称为“鸵鸟算法“。鸵鸟算法,是平衡性能和复杂性而选择的一种方法。)
  2. 退出程序。似乎小题大做,为这种暂时的错误而中断现有的服务似乎不值得
  3. 改用边沿触发,但是之后再也接收不到新连接
  4. 准备一个空闲的文件描述符。如遇到这种情况,先关闭这个空闲的文件,获得了一个文件描述符的名额;再accept拿到新socket连接的描述符;随后立刻关闭close它,这样就优雅的断开了客户端连接;最后重新打开一个空闲的文件,把“坑”占住,已被再次出现这种情况时使用。这总方式具体的处理步骤如下:
1
2
3
4
5
6
7
1、事先准备一个空闲的文件描述符 idlefd,相当于先占一个"坑"

2、调用 close 关闭 idlefd,关闭之后,进程就会获得一个文件描述符名额

3、再次调用 accept 函数, 此时就会返回新的文件描述符 clientfd, 立刻调用 close 函数,关闭 clientfd

4、重新创建空闲文件描述符 idlefd,重新占领 "坑" 位,再出现这种情况的时候又可以使用

下面是处理 EMFILE 的伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int ret = accept( listenfd, (struct sockaddr*)&addr, sizeof(addr) );

if (-1 == ret)
{
if ( errno == EMFILE )
{
//关闭空闲文件描述符,释放 "坑"位
close(idlefd);

//接受 clientfd
clientfd = accept( listenfd, nullptr, nullptr);
//关闭 clientfd,防止一直触发 listenfd 上的可读事件
close(clientfd);

//重新占领 "坑"位
idlefd = ::open("/dev/null", O_RDONLY | O_CLOEXEC);
}
}
  1. file descriptorhard limit,我们可以自己设一个稍微小一点的soft limit,如果超过soft limit 就主动关闭新连接,这样就避免触及file descriptor耗尽这种边界条件。比方说当前进程的max file descriptor是1024,那么我们可以在连接数打到1000的时候进入“拒绝新连接”的状态
Powered by Hexo & Theme Keep
Unique Visitor Page View