1、背景
在高并发服务器程序中,文件描述符资源是有限的。当一个程序同时处理多个网络连接时,每个连接都会占用一个文件描述符。如果系统没有足够的文件描述符可用,调用 accept()(用于接收新的连接)或其他文件操作时可能会失败,导致程序无法继续处理请求。在 Linux 和 macOS 等操作系统中,文件描述符是有限资源,并且每个进程通常只能使用一定数量的文件描述符。如果应用程序的并发连接数接近或者超过了操作系统的文件描述符限制(可以通过 ulimit 设置),系统会返回 EMFILE 错误,表示文件描述符已经用尽。在这种情况下,程序通常需要采取一些策略来优化文件描述符的使用,避免因资源枯竭导致整个系统宕机或无法响应新的请求。
2、mIdle机制
在 Brynet 网络库中,mIdle 作为一个占位符文件描述符,帮助缓解了文件描述符不足的情况。简单来说,mIdle 机制通过释放并占用一个文件描述符槽,提高后续 accept() 调用成功的几率,从而有效降低了由于文件描述符不足导致的连接失败。
class ListenSocket : public brynet::base::NonCopyable
{
public:
TcpSocket::Ptr accept()
{
const auto clientFD = brynet::net::base::Accept(mFD, nullptr, nullptr);
if (clientFD == BRYNET_INVALID_SOCKET)
{
#if defined BRYNET_PLATFORM_LINUX || defined BRYNET_PLATFORM_DARWIN || defined BRYNET_PLATFORM_FREEBSD
if (BRYNET_ERRNO == EMFILE) // 这个错误表示文件描述符被耗尽
{
// Thanks libev and muduo.
// Read the section named "The special problem of
// accept()ing when you can't" in libev's doc.
// By Marc Lehmann, author of libev.
/*
mIdle.reset() 会释放掉 mIdle 占用的文件描述符槽,
从而让系统“腾出”一些空间,以便后续的 accept() 可以成功接收新的连接。
*/
mIdle.reset();
TcpSocket::Create(brynet::net::base::Accept(mFD, nullptr, nullptr), true);
// 再次尝试获取一个文件描述符,这里个人理解是打了一个时间差,如果其它的地方有释放,这里就会成功
mIdle = brynet::net::TcpSocket::Create(::open("/dev/null", O_RDONLY | O_CLOEXEC), true);
}
#endif
// 这里个人认为写的不是很好,如果上面accept成功了,是不是可以考虑不抛出error呢
if (BRYNET_ERRNO == EINTR)
{
throw EintrError();
}
else
{
throw AcceptError(BRYNET_ERRNO);
}
}
return TcpSocket::Create(clientFD, true);
}
protected:
explicit ListenSocket(BrynetSocketFD fd)
: mFD(fd)
{
#if defined BRYNET_PLATFORM_LINUX || defined BRYNET_PLATFORM_DARWIN || defined BRYNET_PLATFORM_FREEBSD
mIdle = brynet::net::TcpSocket::Create(::open("/dev/null", O_RDONLY | O_CLOEXEC), true);
#endif
}
virtual ~ListenSocket()
{
brynet::net::base::SocketClose(mFD);
}
private:
const BrynetSocketFD mFD;
/*
mIdle 是一个占用文件描述符的套接字,
但它并不用于处理实际的连接或数据传输,
而是通过打开 /dev/null 文件来占用一个文件描述符。
/dev/null 是一个特殊的设备,它不做任何 I/O 操作,写入的数据会被丢弃,读取会立即返回 EOF。
*/
#if defined BRYNET_PLATFORM_LINUX || defined BRYNET_PLATFORM_DARWIN || defined BRYNET_PLATFORM_FREEBSD
brynet::net::TcpSocket::Ptr mIdle;
#endif
};
3、mIdle机制的优势
- 缓解文件描述符不足问题,当系统文件描述符池接近耗尽时,mIdle 机制通过释放占用的文件描述符并重新分配,提高了文件描述符池的流动性。这种策略能显著减少因文件描述符枯竭导致的 accept() 失败,提升高并发场景下程序的稳定性。
- 无需额外的资源开销,mIdle 机制通过使用 /dev/null 文件作为占位符,不涉及任何实际的 I/O 操作。因此,它不会带来额外的性能开销,也不会对系统资源产生负担。它仅通过占用一个文件描述符槽来实现文件描述符的释放与占用,从而保持系统的高效运行。
4、mIdle机制的局限
虽然 mIdle 机制对文件描述符的管理和高并发连接的处理起到了缓解作用,但它并不能从根本上解决文件描述符耗尽的问题。当文件描述符池完全耗尽时,单纯依赖 mIdle 是无法解决问题的。
- 文件描述符池的限制是操作系统层面的问题,mIdle 机制只能作为一种缓解策略,而不是根本解决方案。程序仍然需要对文件描述符使用情况进行合理规划,避免无意义的连接占用过多资源。
- 当系统的负载过高,文件描述符用尽的速度非常快时,mIdle 机制可能会遇到极限,它无法完全阻止 EMFILE 错误的发生。在这种情况下,需要通过增加操作系统的文件描述符限制(如通过 ulimit 调整)来从根本上扩展可用文件描述符数量。
5、总结
mIdle 机制是一个非常巧妙的设计,它通过释放和占用文件描述符来缓解文件描述符不足的问题,尤其适用于高并发场景。它通过合理管理文件描述符,提高了 accept() 操作的成功率,进而增强了程序的并发处理能力。然而,mIdle 并非解决文件描述符不足的根本方法,它更多的是一种优化策略,在并发负载较大时发挥作用。要想彻底解决文件描述符枯竭的问题,仍然需要通过系统配置和程序层面的优化来拓宽文件描述符的使用空间。