在服务器编程中,经常会遇到 Too many open files 这个报错,而且这个报错如果处理不好,很有可能会导致服务器死循环。

以上是我用rust写的一个非常简单的tcp服务器,它的主要逻辑是,先创建一个listener,然后再在循环里不断调用listener.accept接收tcp连接,如果接收成功,就调用handle_client处理这个连接,如果接收失败,就打印一行错误日志。
handle_client里的逻辑也非常简单,就是等待客户端关闭连接,或等待其发送任意数据,当这两种情况发生时,handle_client就会直接关闭这个连接。
当然,如果在等待期间报错了,handle_client也会打印一行错误日志。
下面我们就会使用这段程序,来演示服务器死循环的情况,这段程序不必非要用rust编写,用其他语言也都可以。
测试代码我已经放到github了,如果想要自己动手测试的,可以clone下来自己试下。
代码地址:https://github.com/ytcoode/too-many-open-files
先启动该服务器:

由上图可见,该服务器的进程id是312004,监听地址是0.0.0.0:9999。
再查看下该服务器已打开的文件数:

一共是10个,主要包括标准输入输出、epoll、及一些socket。
再查看下该服务器进程最多可打开的文件数:

看选中行,Soft Limit那一列,其表示该进程最多可用的文件描述符数量为1024个,即最多可同时打开的文件数为1024个。
我们把它改小一点,方便后续测试:

上图中,先使用prlimit命令将该服务器进程的Max open files数改成12,然后再用cat命令确认下该改动已生效。
至此,我们已经设置好该服务器进程最多可用的文件描述符数量为12,其当前已用的文件描述符数量为10,所以该服务器最多还可以再接收2个tcp连接。
我们用 `ncat localhost 9999` 命令建立连接试一下,当然你也可以用telnet, nc等其他命令,只要能建立tcp连接就行:

由上图服务器日志可见,该tcp连接已建立成功。
再看下当前服务器已使用的文件描述符数量:

由上图可见,新建socket使用的文件描述符为10,当前服务器进程已使用11个文件描述符,到目前为止一切正常。
用同样的命令再建立一个tcp连接,这次应该也能连接成功,不过会有一些有意思的事情发生:

首先看上图中最后一行info日志,它表示第二次tcp连接也建立成功了,如果此时去看文件描述符数量,也正好是12。
不过此次连接建立也导致不断的error日志输出,该服务器死循环了。
但此时,如果我们关闭第二次ncat命令建立的tcp连接,服务器又不会一直输出error日志了,它又会恢复到正常状态:

看上图中的最后一条info日志,它表示第二个tcp连接正常关闭了,且当前已建立的连接数量是1。
此时,如果我们去看文件描述符数量,其也变成了11,这里就不再截图了,有兴趣的可以自己动手试下。
首先,在linux的世界里,一切皆文件,这里就包括socket。
其次,linux为保证系统的整体性安全,为每个进程限制了其最大可使用的文件描述符数量,即最大可打开的文件数,这个数量就是上面我们用 `cat /proc/$(pidof too-many-open-files)/limits` 命令输出的Max open files行,Soft Limit列对应的值,该值是可以通过各种方式修改的,在我的系统上,该值默认为1024。
接着,我们启动了服务器,然后通过 `l /proc/$(pidof too-many-open-files)/fd/` 命令查看该服务器已使用的文件描述符数量,其为10。
之后,我们用prlimit命令将该服务器进程最大可使用的文件描述符数量改成了12,这样该服务器就还只剩两个文件描述符可用。
再之后,我们用ncat命令建立了两个tcp连接,在服务器端的循环里,accept接收到这两个连接并进行处理,此时该服务器进程消耗完了最后两个可用的文件描述符。
接下来,服务器代码进入下一次循环,继续调用accept尝试接收新的连接,问题的关键点也就出现在了这里。
accept是个系统调用,我们看下其对应的内核实现:

这个是accept系统调用的入口函数,沿着函数调用,可找到以下代码:

由上图可见,在真正的do_accept之前,会先调用get_unused_fd_flags找一个还未被使用的文件描述符,如果寻找时报错了,即newfd < 0,则直接返回该错误码给用户层,如果找到了一个可用的文件描述符,则开始执行真正的accept操作。
继续看get_unused_fd_flags函数:

它在调用其他函数之前,会通过 rlimit(RLIMIT_NOFILE) 获取当前进程最大可使用的文件描述符数量,即我们上面通过prlitmit命令设置的12。
继续往下看,我们会找到以下代码:

该函数的目的是分配一个文件描述符,即fd,图中选中行之前是找到一个还未被使用的fd,然后判断该 fd 是否 >= end,如果是,则goto到out,进而return error,而这个error就是EMFILE。
那end值是什么呢?它就是上面用 rlimit(RLIMIT_NOFILE) 获取的当前进程最大可用的文件描述符数。
结合上面的例子我们知道,当服务器接收完两个tcp连接后,其最大可使用的12个文件描述符已全部被用完,当其循环到下一次accept系统调用后,会最终进入到上图这个函数,这次新分配的fd值一定是12(因为fd值从0开始的,所以fd值为12表示第13个文件描述符),而我们又限制了该进程最大可用12个文件描述符,即我们限制了end值为12,所以在上图选中行进行判断时,fd 一定是 >= end 的,所以,该函数一定会返回EMFILE这个错误码。
而EMFILE是什么呢?

它就是我们在运行测试程序时看到的 Too many open files 这个错误。
示例程序调用accept收到这个错误码后,会打印一行error日志,然后继续循环调用accept,然后继续报错,就这样,服务器就在accept这里发生了死循环。
因为 too many open files 是个临时性错误,当进程中的其他地方关闭了一些文件,或者管理人员调高了该进程的 max open files值,accept就不会再报 EMFILE 错误,也就不会再死循环了。
所以其处理方法也很简单,就是在accept发生错误时,sleep一段时间,这样既防止了cpu 100%的发生,也给进程时间来调整已用及最大的文件描述符数。
会有,epoll只是个通知机制,当epoll检测到有连接可被接收时,还是会通过accept来接收这个连接。
不过这里分成两种情况。
当使用epoll的edge-triggered模式时,正确写法是要一直循环调用accept接收连接,直到其返回 EAGAIN 或 EWOULDBLOCK 错误码,表示已经没有连接可接收了,这时才能退出accept循环,但如果在这之前accept返回了 too many open files 这个错误,就会发生死循环了。
当使用epoll的level-triggered模式时,可以不必一直循环调用accept直到其返回EAGAIN 或 EWOULDBLOCK,可以提前退出,但如果操作系统里还有建立好的连接等待被接收,epoll还是会一直通知应用层,告知其要调用accept接收这些连接,如果此时文件描述符没有了,accept还是会一直报 too many open files 错误,最终还是进入到了死循环。
下面我们看下go内置的http服务器,是如何处理这个问题的:

当accept返回err后,其会通过ne.Temporary()来检查该err是否是临时性错误,如果是,则会根据一定的规则,sleep一段时间。
这里,临时性错误就包括 EMFILE,即too many open files错误:

我们也可以写个简单的例子测试下:

按照之前的方式,让其触发 too many open files 这个错误:

由图可见,和我们上面分析的一样,其也陷入了死循环,但是它用sleep的方式,防止cpu使用率100%。
下面我们看下redis是如何处理这个问题的:

当anetTcpAccept返回 too many open files 错误时,它只打印了一行错误日志,就直接return了。
不过因为redis使用的是level-triggered模式的epoll,所以虽然这里直接return了,但因为底层的连接没接收出来,epoll一直会调用这个函数,然后一直报错,进而死循环。
实验下:

可以看到,其一直在输出这个错误。
希望通过这篇文章,能给大家的技术水平带来一点提高。

我正在尝试使用ruby和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我
我想安装一个带有一些身份验证的私有(private)Rubygem服务器。我希望能够使用公共(public)Ubuntu服务器托管内部gem。我读到了http://docs.rubygems.org/read/chapter/18.但是那个没有身份验证-如我所见。然后我读到了https://github.com/cwninja/geminabox.但是当我使用基本身份验证(他们在他们的Wiki中有)时,它会提示从我的服务器获取源。所以。如何制作带有身份验证的私有(private)Rubygem服务器?这是不可能的吗?谢谢。编辑:Geminabox问题。我尝试“捆绑”以安装新的gem..
我脑子里浮现出一些关于一种新编程语言的想法,所以我想我会尝试实现它。一位friend建议我尝试使用Treetop(Rubygem)来创建一个解析器。Treetop的文档很少,我以前从未做过这种事情。我的解析器表现得好像有一个无限循环,但没有堆栈跟踪;事实证明很难追踪到。有人可以指出入门级解析/AST指南的方向吗?我真的需要一些列出规则、常见用法等的东西来使用像Treetop这样的工具。我的语法分析器在GitHub上,以防有人希望帮助我改进它。class{initialize=lambda(name){receiver.name=name}greet=lambda{IO.puts("He
我有多个ActiveRecord子类Item的实例数组,我需要根据最早的事件循环打印。在这种情况下,我需要打印付款和维护日期,如下所示:ItemAmaintenancerequiredin5daysItemBpaymentrequiredin6daysItemApaymentrequiredin7daysItemBmaintenancerequiredin8days我目前有两个查询,用于查找maintenance和payment项目(非排他性查询),并输出如下内容:paymentrequiredin...maintenancerequiredin...有什么方法可以改善上述(丑陋的)代
大约一年前,我决定确保每个包含非唯一文本的Flash通知都将从模块中的方法中获取文本。我这样做的最初原因是为了避免一遍又一遍地输入相同的字符串。如果我想更改措辞,我可以在一个地方轻松完成,而且一遍又一遍地重复同一件事而出现拼写错误的可能性也会降低。我最终得到的是这样的:moduleMessagesdefformat_error_messages(errors)errors.map{|attribute,message|"Error:#{attribute.to_s.titleize}#{message}."}enddeferror_message_could_not_find(obje
最近,当我启动我的Rails服务器时,我收到了一长串警告。虽然它不影响我的应用程序,但我想知道如何解决这些警告。我的估计是imagemagick以某种方式被调用了两次?当我在警告前后检查我的git日志时。我想知道如何解决这个问题。-bcrypt-ruby(3.1.2)-better_errors(1.0.1)+bcrypt(3.1.7)+bcrypt-ruby(3.1.5)-bcrypt(>=3.1.3)+better_errors(1.1.0)bcrypt和imagemagick有关系吗?/Users/rbchris/.rbenv/versions/2.0.0-p247/lib/ru
在Rails4.0.2中,我使用s3_direct_upload和aws-sdkgems直接为s3存储桶上传文件。在开发环境中它工作正常,但在生产环境中它会抛出如下错误,ActionView::Template::Error(noimplicitconversionofnilintoString)在View中,create_cv_url,:id=>"s3_uploader",:key=>"cv_uploads/{unique_id}/${filename}",:key_starts_with=>"cv_uploads/",:callback_param=>"cv[direct_uplo
我遵循MichaelHartl的“RubyonRails教程:学习Web开发”,并创建了检查用户名和电子邮件长度有效性的测试(名称最多50个字符,电子邮件最多255个字符)。test/helpers/application_helper_test.rb的内容是:require'test_helper'classApplicationHelperTest在运行bundleexecraketest时,所有测试都通过了,但我看到以下消息在最后被标记为错误:ERROR["test_full_title_helper",ApplicationHelperTest,1.820016791]test
我收到这个错误:RuntimeError(自动加载常量Apps时检测到循环依赖当我使用多线程时。下面是我的代码。为什么会这样?我尝试多线程的原因是因为我正在编写一个HTML抓取应用程序。对Nokogiri::HTML(open())的调用是一个同步阻塞调用,需要1秒才能返回,我有100,000多个页面要访问,所以我试图运行多个线程来解决这个问题。有更好的方法吗?classToolsController0)app.website=array.join(',')putsapp.websiteelseapp.website="NONE"endapp.saveapps=Apps.order("
我是rails的新手,想在form字段上应用验证。myviewsnew.html.erb.....模拟.rbclassSimulation{:in=>1..25,:message=>'Therowmustbebetween1and25'}end模拟Controller.rbclassSimulationsController我想检查模型类中row字段的整数范围,如果不在范围内则返回错误信息。我可以检查上面代码的范围,但无法返回错误消息提前致谢 最佳答案 关键是您使用的是模型表单,一种显示ActiveRecord模型实例属性的表单。c