accept (EMFILE错误)文件描述符用尽解决方案
通常情况下,服务端调用 accept 函数会返回一个新的文件描述符,用于和客户端之间的数据传输
在服务器的开发中,有时会遇到这种情况:当调用 accept 函数接受客户端连接,函数返回失败,对应的错误码是 EMFILE
,它表示当前进程打开的文件描述符已达上限,此时,服务器不能再接受客户端连接
当遇到上述问题,怎么合理的处理呢,下面就来分析一下
建立连接的流程
先简单回顾下客户端和服务器建立连接的流程,具体的如下图所示:
1 | 1. 客户端发起 SYN 请求 |
上面的第 1、第 2、第 3 步是 TCP 的三次握手,它是由内核中TCP协议完成的, 第 4 步是应用层调用 accept 接口
在 epoll 中的问题
epoll
是 Linux中IO多路复用模型,在服务器的开发中有广泛的应用,下面就以epoll
为例来详细说明
服务器端创建侦听文件描述符listenfd
之后, 向epoll
注册读事件
当 epoll
检测到 listenfd
上有读事件发生,会立即通知应用层,应用层调用accept
接受新连接,而此时进程打开的文件描述符数量已经达到上限了,所以每次 accept
都是失败的
这里会出现以下几个问题
- 由于 每次
accept
都失败了,相当于listenfd
上的可读事件没有处理,epoll
会不停的触发listenfd
上的可读事件,应用层也就会不停的调用accept
,然后又出现accept
调用失败,如此这般不停的执行无效的循环,程序就陷入了busy loop中。CPU占用率会达到100%,影响了服务器的性能。 - 上面提到服务器在不停的执行无效的循环, 将会引发另一个问题,如果此时有新客户端连接到来,建立连接的过程会很慢
前面说的epoll
默认是使用了水平触发模式,如果使用边沿触发模式会出现什么问题呢?
边沿触发模式下,
listenfd
从无读事件状态到有读事件状态时,才会通知到应用层,在应用层处理完listenfd
上所有的读事件之前,epoll
不会再通知应用层也就是说,应用层收到
listenfd
上读事件通知之后,需要把listenfd
上所有的读事件全部处理完,下次listenfd
上再有读事件时,才会通知应用层回到
accept
的问题上,在边沿触发模式下,当epoll
通知应用层listenfd
上有可读事件时,应用层调用accept
, 由于此时进程打开的文件描述符数量已经达到上限了,所以accept
调用失败,也即listenfd
上的可读事件还没有处理,在应用层处理完listenfd
上可读事件之前,epoll
不会再通知应用层listenfd
上有可读事件如果在应用层处理完
listenfd
上可读事件之前,有新的客户端连接到来,这个时候epoll
是不会通知应用层listenfd
上有可读事件,这会导致一个严重的问题:accept
只要出现了EMFILE
的错误码,就再也无法接受客户端的连接了
所以,当出现 EMFILE
时,不管使用epoll
的水平触发模式还是边沿触发模式都会存在问题
如何解决
EMFILE
表示进程打开的文件描述符数量达到上限了,可以把这个值调大些,但这治标不治本
本来系统设置文件描述符数量上限是为了限制进程对系统资源的过度占用,况且,这个值调整到多大合适呢,总不能无限大吧,所以调整上限值的方式不是最合适的方式
accept 成功时会返回一个新的文件描述符,如果此时进程打开的文件描述符已经达到上限了,就会返回失败
- 死等。鸵鸟算法(传说中鸵鸟看到危险就把头埋在地底下。当你对某一件事情没有一个很好的解决方法时,那就忽略它,就像鸵鸟面对危险时会把它深埋在沙砾中,装作看不到。这样的算法称为“鸵鸟算法“。鸵鸟算法,是平衡性能和复杂性而选择的一种方法。)
- 退出程序。似乎小题大做,为这种暂时的错误而中断现有的服务似乎不值得
- 改用边沿触发,但是之后再也接收不到新连接
- 准备一个空闲的文件描述符。如遇到这种情况,先关闭这个空闲的文件,获得了一个文件描述符的名额;再
accept
拿到新socket
连接的描述符;随后立刻关闭close
它,这样就优雅的断开了客户端连接;最后重新打开一个空闲的文件,把“坑”占住,已被再次出现这种情况时使用。这总方式具体的处理步骤如下:
1 | 1、事先准备一个空闲的文件描述符 idlefd,相当于先占一个"坑"位 |
下面是处理 EMFILE
的伪代码:
1 | int ret = accept( listenfd, (struct sockaddr*)&addr, sizeof(addr) ); |
file descriptor
是hard limit
,我们可以自己设一个稍微小一点的soft limit
,如果超过soft limit
就主动关闭新连接,这样就避免触及file descriptor耗尽
这种边界条件。比方说当前进程的max file descriptor
是1024,那么我们可以在连接数打到1000的时候进入“拒绝新连接”的状态