C/C++编程:如何优雅的处理 Accept 出现 Emfile 的问题

本文探讨了服务器在接收到EMFILE错误时如何处理,重点介绍了两种解决方案:一是通过预设空闲描述符并动态关闭以接纳新连接,二是使用AIO模块实现批量接受。讨论了epoll的水平触发和垂直触发模式下的问题,并提供了相应的代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

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

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

建立连接的流程

先简单回顾下客户端和服务器建立连接的流程,具体的如下图所示:
在这里插入图片描述

  1. 客户端发起SYNC请求
  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都是失败的

这里会出现以下几个问题:

  • 由于每次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;
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值