在尽我所能研究实现消息队列服务器的最佳方法后,我提出了这个问题。为什么操作系统会限制一个进程和全局系统可以拥有的打开文件描述符的数量?我当前的服务器实现使用 zeromq,并为每个连接的 websocket 客户端打开一个订阅者套接字。显然,单个进程只能处理客户端到 fds 的限制。当我研究这个主题时,我发现了很多关于如何将系统限制提高到高达 64k fds 的信息,但它从来没有提到它如何影响系统性能以及为什么它一开始是 1k 或更低?我目前的方法是尝试使用协程在其自己的循环中向所有客户端发送消息,以及所有客户端及其订阅通道的映射。
4 回答
这可能是因为文件描述符值是文件描述符表的索引。因此,可能的文件描述符的数量将决定表的大小。普通用户不希望他们的一半内存被一个文件描述符表用完,该表可以处理他们永远不需要的数百万个文件描述符。
当您有很多潜在的文件描述符时,某些操作会减慢速度。stdin
一个例子是“关闭除,stdout
和之外的所有文件描述符”的操作stderr
——唯一可移植的*方法是尝试关闭除这三个之外的所有可能的文件描述符,如果您可能拥有数百万个文件描述符,这可能会成为一项缓慢的操作的文件描述符打开。
*:如果你愿意不便携,你可以进去看看/proc/self/fd
——但这不是重点。
这不是一个特别好的理由,但它是一个理由。另一个原因只是为了防止有缺陷的程序(即“泄漏”文件描述符的程序)消耗过多的系统资源。
出于性能考虑,打开的文件表需要静态分配,因此它的大小需要固定。文件描述符只是该表的偏移量,因此所有条目都需要是连续的。您可以调整表的大小,但这需要停止进程中的所有线程并为文件表分配一个新的内存块,然后将所有条目从旧表复制到新表。这不是您想要动态执行的操作,尤其是当您这样做的原因是因为旧表已满时!
在 unix 系统上,进程创建 fork() 和 fork()/exec() 习惯用法需要迭代所有潜在的进程文件描述符,试图关闭每一个,通常只留下几个文件描述符,例如 stdin、stdout、stderr 未触及或重定向到其他地方。
由于这是用于启动进程的 unix api,因此必须在任何时候创建新进程时执行此操作,包括执行在 shell 脚本中调用的每个非内置命令。
其他需要考虑的因素是,虽然某些软件可能会使用sysconf(OPEN_MAX)
动态确定进程可能打开的文件数量,但许多软件仍使用 C 库的默认值FD_SETSIZE
,通常为 1024 个描述符,因此不能超过无论管理上定义的上限如何,都会打开许多文件。
Unix 有一个基于文件描述符集的传统异步 I/O 机制,这些文件描述符集使用位偏移来表示要等待的文件以及准备好或处于异常条件的文件。它不适用于数千个文件,因为每次运行循环时都需要设置和清除这些描述符集。更新的非标准 api 出现在主要的 unix 变体上,包括kqueue()
在 *BSD 和epoll()
Linux 上,以解决处理大量描述符时的性能缺陷。
需要注意的select()/poll()
是,很多软件仍在使用它,因为长期以来它一直是用于异步 I/O 的 POSIX api。现代 POSIX 异步 IO 方法现在是aio_*
API,但它可能无法与kqueue()
API竞争epoll()
。我没有在愤怒中使用过 aio,而且它肯定不具备原生方法提供的性能和语义,因为它们可以聚合多个事件以获得更高的性能。*BSD 上的 kqueue() 具有非常好的事件通知边缘触发语义,允许它替换 select()/poll() 而不会强制对应用程序进行大的结构更改。Linux epoll() 遵循 *BSD kqueue() 的领先地位并对其进行了改进,而后者又遵循了 Sun/Solaris evports 的领先地位。
结果是,增加系统中允许打开的文件的数量会增加系统中每个进程的时间和空间开销,即使它们无法根据他们正在使用的 api 使用这些描述符。对于允许的打开文件的数量,也有聚合系统限制。在 FreeBSD 上使用 nginx 的 100k-200k 同时连接的这个较旧但有趣的调整摘要提供了一些关于维护开放连接的开销的洞察力,另一个涵盖更广泛的系统但“仅”将 10K 连接视为珠穆朗玛峰。
Unix 系统编程的最佳参考可能是 W. Richard Stevens Advanced Programming in the Unix Environment