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

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

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

建立连接的流程

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


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 调用失败,如此这般不停的执行无效的循环,白白浪费了CPU的资源

  2. 上面提到服务器在不停的执行无效的循环, 将会引发另一个问题,如果此时有新客户端连接到来,建立连接的过程会很慢

前面说的 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 就会创建成功,这总方式具体的处理步骤如下:


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);
}
}

如何优雅的处理 accept 出现 EMFILE 的问题的更多相关文章

  1. linux 网络编程 基础

    网络编程基础 套接字编程需要指定套接字地址作为参数,不同的协议族有不同的地址结构,比如以太网其结构为sockaddr_in. 通用套接字: struct sockaddr { sa_family_t ...

  2. accept 文件描述符用尽处理

    if (events[i].data.fd == listenfd) { peerlen = sizeof(peeraddr); connfd = ::accept4(listenfd, (struc ...

  3. Java8之——简洁优雅的Lambda表达式

    Java8发布之后,Lambda表达式,Stream等等之类的字眼边慢慢出现在我们字眼.就像是Java7出现了之后,大家看到了“钻石语法”,看到了try-with-resource等等.面对这些新东西 ...

  4. 如何优雅的设计React组件

    如何优雅的设计 React 组件 如今的 web 前端已被 React.Vue 和 Angular 三分天下,一统江山十几年的 jQuery 显然已经很难满足现在的开发模式.那么,为什么大家会觉得 j ...

  5. java~lambda表达式让查询更优雅

    在java之前的版本里,如果希望从集合时查找符合条件的数据,如果先遍历他,这种写法是我们不能接受的,所以现在java有了lambda就很好的解决了这个问题,让代码更优雅一些! /** * lambda ...

  6. Golang的优雅重启

    更新(2015年4月):Florian von Bock已将本文中描述的内容转换为一个名为endless的优秀Go包 . 如果您有Golang HTTP服务,可能需要重新启动它以升级二进制文件或更改某 ...

  7. JDK8漫谈——代码更优雅

    简介 lambda表达式,又称闭包(Closure)或称匿名方法(anonymous method).将Lambda表达式引入JAVA中的动机源于一个叫"行为参数"的模式.这种模式 ...

  8. [译]Golang中的优雅重启

    原文 Graceful Restart in Golang 作者 grisha 声明:本文目的仅仅作为个人mark,所以在翻译的过程中参杂了自己的思想甚至改变了部分内容,其中有下划线的文字为译者添加. ...

  9. 接口处理篇 accept bind connect atan2 htons inet_addr inet_aton inet_ntoa listen ntohl recv send sendto socket

    accept(接受socket连线) 相关函数 socket,bind,listen,connect 表头文件 #include<sys/types.h> #include<sys/ ...

随机推荐

  1. 利用协程爬网页,自动切换io 精典案例:

    首先Python提供的协程库gevent好像并不能知道那些程序使用了io  所以要加一个补丁,mondey,以下同步和异步各爬一次的案例 : , from urllib import requesti ...

  2. 支持Cron表达式、间隔时间的工具(TaskScheduler)

    后台任务如何支持间隔时间.Cron表达式两种方式? 分享一个项目TaskScheduler,这是我从Furion项目中拷出来的 源码:https://gitee.com/dot-net-core/ta ...

  3. Dockerfile常见命令

    Dockerfile结构 Dockerfile的结构分成了若干部分,每个部分之间的先后顺序有明确的要求: 部分 命令 基础镜像信息 FROM 维护者信息 MAINTAINER 镜像操作指令 RUN.C ...

  4. crontab 语法和最快速的学习

    1.Cron 时间表语法 # ┌───────────── 分钟 (0 - 59) # │ ┌───────────── 小时 (0 - 23) # │ │ ┌───────────── 月的某天 ( ...

  5. mybatis零碎

    <           <    小于号 >          >    大于号 &     &        和 &apos;     '     单 ...

  6. jquery获取一个元素符合条件的第一个父元素

    closest jQuery 1.3新增.从元素本身开始,逐级向上级元素匹配,并返回最先匹配的元素.. closest会首先检查当前元素是否匹配,如果匹配则直接返回元素本身.如果不匹配则向上查找父元素 ...

  7. Java基础系列(19)- Switch结构

    package struct; public class SwitchDemo01 { //case穿透 //switch 匹配一个具体的值 public static void main(Strin ...

  8. Linux系列(3) - ls

    作用 ls:查询目录中内容 格式 ls [选项] [文件或目录] 选项 描述 -a 显示所有文件,包括隐藏文件.隐藏文件是.开头的 -l 显示详细信息:ls -l简写为ll,使用频率很高 -d 查看目 ...

  9. 重新嫁接rm命令

    ### 重定义rm命令 #### 定义回收站目录trash_path='~/.trash'# 判断 $trash_path 定义的文件是否存在,如果不存在,那么就创建 $trash_path.if [ ...

  10. 感恩笔记之二_SQL语句扩展功能

    前言导读: 本章是对SQL语句基础功能中,一些功能用法的扩展使用的总结,都是实际工作中一些经验的积累. 1 select列查询功能组合使用 --1 函数处理+列计算+列改名 select 函数(列) ...