-
通常情况下,服务端调用 accept 函数会返回一个新的文件描述符,用于和客户端之间的数据传输
-
在服务器的开发中,有时会遇到这种情况:当调用 accept 函数接受客户端连接,函数返回失败,对应的错误码是 EMFILE, 它表示当前进程打开的文件描述符已达上限,此时,服务器不能再接受客户端连接
当遇到上述问题,怎么合理的处理呢,下面就来分析一下
建立连接的流程
先简单回顾下客户端和服务器建立连接的流程,具体的如下图所示:
- 客户端发起SYNC请求
- 服务器收到客户端的SYN请求后,内核把连接放入半连接队列,同时给客户端返回一个SYN + ACK
- 客户端项服务器返回一个确认的ACK,服务器收到本次ACK之后,三次握手完成,同时,内核把连接从半连接队列中移除,创建新完全连接,加入到全连接队列中
- 应用层调度accept函数丛全连接队列中取出连接
上面的第 1、第 2、第 3 步是 TCP 的三次握手,它是由内核中TCP协议完成的, 第 4 步是应用层调用 accept 接口
在epoll中的问题
epoll是Linux中IO多路复用模型,在服务器的开发中有广泛的应用,下面就以epoll为例来讲解
服务器端创建监听文件描述符listenfd之后,向epoll注册读事件
当epoll检测到listenfd上有事件发生,会立即通知应用层,应用层调度accept接收新连接,而此时进程打开的文件描述符数量已经达到上限了,所以每次accept都是失败的
这里会出现以下几个问题:
- 由于每次accept都失败了,相当于listenfd上的可读事件没有处理,epoll会不停的触发listenfd上的可读事件,应用层也就不停地调度accept,然后又出现accept调用失败,如此这般不停地执行无效的循环,白白浪费了CPU的资源
- 上面提到服务器在不停地执行无效的循环,将会引发另一个问题,如果此时有新客户端连接到来,建立连接的过程会很慢
前面说的是epoll默认是使用了水平触发模式,如果使用垂直触发模式会出现什么问题呢?
垂直触发模式下,listenfd从无读事件状态到有读事件状态时,才会通知到应用层,在应用层处理完listenfd上所有的读事件之前,epoll不会再通知应用层。
也就是说,应用层在收到listenfd上读事件通知之后,需要把listenfd上所有的读事件全部处理完,下次listenfd上再有读事件时,才会通知应用层
回到accept的问题上,在垂直触发模式下,当epoll通知应用层listenfd上有可读事件时,应用层调度accept,由于此时进程打开的文件描述符数量已经达到上限了,所以accept调用失败。
也即 listenfd 上的可读事件还没有处理,在应用层处理完 listenfd 上可读事件之前,epoll 不会再通知应用层 listenfd 上有可读事件
如果在应用层处理完listenfd上可读事件之前,有新的客户端连接到来,这个时候epoll是不会通知应用层listenfd上有可读事件,这会导致一个严重的问题:accept只要出现了EMPIFL的错误码,就再也无法接收客户端的连接了
所以,当出现 EMFILE 时,不管使用 epoll 的水平触发模式还是垂直触发模式都会存在问题
如何解决
方法1
EMFILE表示进程打开的文件描述符数量达到上限了,可以把这个值调大些,但这治标不治本。
本来系统设置文件描述符数量上限是为了限制进程对系统资源的过度占用,况且,这个值调整到多大合适呢,总不能无限大吧,所以调整上限值的方式不是最合适的方式
accept 成功时会返回一个新的文件描述符,如果此时进程打开的文件描述符数量已经达到上限了,就会返回失败
假如此时能关闭一个空闲的文件描述符,让出一个名额,再调用 accept 就会创建成功,这种方式具体的处理步骤如下:
1、事先准备一个空闲的文件描述符 idlefd,相当于先占一个"坑"位
2、调用 close 关闭 idlefd,关闭之后,进程就会获得一个文件描述符名额
3、再次调用 accept 函数, 此时就会返回新的文件描述符 clientfd, 立刻调用 close 函数,关闭 clientfd
4、重新创建空闲文件描述符 idlefd,重新占领 “坑” 位,再出现这种情况的时候又可以使用
下面是处理 EMFILE 的伪代码:
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);
}
}
方法2
static int aio_server_accept_sock2(ACL_ASTREAM *astream, ACL_AIO *aio)
{
const char *myname = "aio_serer_accept_sock2";
ACL_VSTREAM *vstream = acl_aio_vstream(astream);
int listen_fd = ACL_VSTREAM_SOCK(vstream);
int fd, *fds, i, j, delay_listen = 0, sock_type;
fds = (int*) tls_alloc(sizeof(int) * acl_var_aio_max_accept);
for (i = 0; i < acl_var_aio_max_accept; i++) {
fd = acl_accept(listen_fd, NULL, 0, &sock_type);
#ifdef ACL_WINDOWS
if (fd != ACL_SOCKET_INVALID) {
#else
if (fd >= 0) {
#endif
/* TCP 连接避免发送延迟现象 */
#ifdef AF_INET6
if (sock_type == AF_INET || sock_type == AF_INET6)
#else
if (sock_type == AF_INET)
#endif
acl_tcp_set_nodelay(fd);
fds[i] = fd;
} else if (errno == EMFILE) {
delay_listen = 1;
acl_msg_warn("%s(%d), %s: accept connection: %s",
__FILE__, __LINE__, myname, acl_last_serror());
} else if (errno == EAGAIN || errno == EINTR)
break;
else
acl_msg_fatal("%s(%d), %s: accept connection: %s",
__FILE__, __LINE__, myname, acl_last_serror());
}
if (delay_listen) {
acl_aio_disable_read(astream);
acl_aio_request_timer(aio, restart_listen, astream, 2000000, 0);
}
return i;
}