为什么操作系统限制文件描述符数量?

20

在尝试研究实现消息队列服务器的最佳方式后,我提出了这个问题。为什么操作系统会对进程和全局系统可以拥有的打开文件描述符数量设限呢?

我目前的服务器实现使用zeromq,并为每个连接的websocket客户端打开一个订阅器socket。显然,单个进程只能处理到达fd上限数量的客户端。

当我研究这个主题时,我发现很多关于如何将系统限制提高到64k fd之类的级别的信息,但它从未提到它如何影响系统性能,以及为什么最初设置为1k或更低?

我的当前方法是尝试使用自己的循环中的协程,以及所有客户端及其订阅通道的映射,向所有客户端分派消息。但我很想听到一个确定的答案,关于文件描述符限制以及它们如何影响那些尝试在每个客户端级别上使用具有持久连接的应用程序。


好的,我从所有这些答案中得出结论:1)问题在于可用RAM;2)如果可移植性很重要,Web服务器应用程序不应依赖于使用大量动态分配的文件描述符。因为实施服务器的人将不得不调整他们的服务器FD限制。 - jdi
4个回答

19

可能是因为文件描述符的值是文件描述符表中的一个索引。因此,可能文件描述符的数量将决定表的大小。普通用户不希望将一半的内存用于处理数百万个他们永远不需要的文件描述符的表。


我喜欢这个答案,因为它明确解释了问题出在可用的RAM上。所以这就是我需要知道的,我的应用程序需要消耗大量的文件描述符,并且我的服务器专门针对该应用程序进行了调整。谢谢! - jdi

4
有些操作会因为文件描述符的潜在数量而变慢。其中一个例子是“关闭所有文件描述符,除了stdinstdoutstderr”,唯一可移植*的方法是尝试关闭除这三个之外的每个可能的文件描述符,如果你可能有数百万个文件描述符打开,这将变得非常缓慢。

*: 如果你不介意非可移植性,则可以查看/proc/self/fd,但这并不重要。

这并不是一个特别好的理由,但这确实是一个理由。另一个原因只是为了防止 bug 程序(即泄漏文件描述符的程序)消耗过多的系统资源。


我不认为这是主要原因。下面TMN的回答更好。 - David Roussel

4
为了提高性能,打开文件表需要静态分配,因此其大小需要固定。文件描述符只是指向该表中的偏移量,因此所有条目都需要连续。您可以调整表格的大小,但这需要停止进程中的所有线程并为文件表分配新块,然后将所有条目从旧表复制到新表。这不是动态执行的操作,特别是当您之所以这样做是因为旧表已满时!

3
在Unix平台上是正确的。在Windows上,使用文件句柄,一个进程默认可以分配1600万个句柄。句柄表是动态分配的,因此您更有可能耗尽内存而不是句柄。但是如果您确实用完了句柄,会发生奇怪的事情。请参阅http://blogs.technet.com/b/markrussinovich/archive/2009/09/29/3283844.aspx。 - David Roussel
1
还有其他一些东西会占用更多的空间(可能也需要更多时间) - 比如用于select()的FD掩码。 - Nick Johnson
谢谢。我可以看到提高限制并不是一件动态的事情。我可以看到这个答案会引导人们建议为FDs分配一个更大的静态表,以获得更专用的内存? - jdi

2
在Unix系统中,进程创建fork()和fork()/exec()习惯需要迭代所有潜在的进程文件描述符,尝试关闭每个描述符,通常只保留几个文件描述符(如stdin、stdout、stderr)不变或重定向到其他地方。
由于这是启动进程的Unix API,必须在创建新进程时执行它,包括在shell脚本中调用的每个非内置命令。
其他需要考虑的因素是,尽管一些软件可能使用sysconf(OPEN_MAX)来动态确定进程可以打开的文件数,但很多软件仍然使用C库的默认FD_SETSIZE,通常为1024个描述符,因此无论任何管理定义的更高限制,都不能超过这么多文件。
Unix旧有的异步I/O机制基于文件描述符集,使用位偏移量表示等待的文件和已准备好或出现异常情况的文件。随着这些描述符集需要在每个运行循环中设置和清除,当处理数千个文件时,它无法良好扩展。现代非标准API出现在主要Unix变体上,包括*BSD上的kqueue()和Linux上的epoll(),以解决处理大量描述符时的性能问题。
值得注意的是,select()/poll()仍然被很多软件广泛使用,因为它长期以来一直是POSIX异步I/O的API。现代POSIX异步IO方法现在是aio_* API,但它可能与kqueue()或epoll() API不具备竞争力。我没有真正使用过aio,当然它不会像本地方法一样提供性能和语义,也不能像本地方法那样聚合多个事件以实现更高的性能。*BSD上的kqueue()对于事件通知具有非常好的边缘触发语义,使其能够取代select()/poll()而不强制对应用程序进行大型结构更改。Linux epoll()遵循*BSD kqueue()的先例并对其进行了改进,而后者又遵循了Sun/Solaris evports的先例。
结论是,即使进程无法利用那些描述符,根据它们使用的API,增加系统中允许打开文件的数量也会为系统中的每个进程添加时间和空间开销。此外,还存在聚合系统限制,限制可以打开的文件数。这个在FreeBSD上使用nginx处理10万到20万个并发连接的旧但有趣的调优摘要提供了一些维护打开连接的开销的见解,另一个涵盖更广泛系统范围,但“仅”看到10K连接就像珠穆朗玛峰一样高。

可能最好的Unix系统编程参考书是W. Richard Stevens的《Unix环境高级编程》


1
哦,有哪些文档支持需要fork()迭代所有文件描述符尝试关闭每一个?fork()手册说子进程继承父进程的文件描述符表的副本。这是否意味着如果想在fork()上关闭文件描述符,那么如果父进程中允许更多的文件描述符,则可能需要迭代更大的列表? - jdi
我想我永远不会知道这个原因。这仅适用于Unix,没有任何参考资料。 - Aseem Bansal
@AseemBansal 是的,这是Unix,因为问题特别涉及文件描述符限制,而不是Windows文件句柄或Mach微内核端口。 - Andrew Hacking
@jdi更新了一些有关历史和当前API的信息,以及一些关于扩大连接时开销的指南。 - Andrew Hacking
@jdi,OS/X 就是 Unix,事实上它是为数不多的几个被OpenGroup认证为真正Unix的系统之一。Linux不符合OpenGroup定义的Unix标准,*BSD虽然有Unix的遗产,但实际上也不符合标准,但从实际角度来看,它们对于大多数人来说已经足够接近了。 - Andrew Hacking
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接