如何避免libevent 1.4.14中event_base_new因文件句柄耗尽触发SIGABRT崩溃
问题
我们使用libevent 1.4.14版本,在生产环境中发现当主机文件描述符耗尽时,进程会因libevent触发的SIGABRT信号崩溃。以下测试代码可复现该问题:
#include <event.h> #include <sys/epoll.h> #include <sys/resource.h> #include <iostream> int main(int argc, char* argv[]) { struct rlimit rl; rl.rlim_cur = rl.rlim_max = 400; // artificially limit number of fds int ret = setrlimit(RLIMIT_NOFILE, &rl); if (ret) { int errno_ = errno; std::cerr << "unable to set RLIMIT_NOFILE " << errno_ << std::endl; } for (int i = 0; i < 400; i++) { event_base_new(); } }
程序最终会崩溃并输出错误信息:
[err] evsignal_init: socketpair: Too many open files libevent exit code: 1
调用栈显示崩溃源于event_base_new流程:
@ 00000000025ca144 event_err [...]/log.c:88 @ 00000000025cc0e2 evsignal_init [...]/signal.c:112 @ 00000000025cb73b epoll_init [...]/epoll.c:295 @ 00000000025c8f41 event_base_new [...]/event.c:192
请问是否有办法阻止event_base_new导致进程崩溃,转而将错误向上传递(例如返回NULL)?
解决方案
针对libevent 1.4.14的这个问题,有几种可行的处理方式:
1. 自定义错误处理函数,替换默认崩溃逻辑
libevent允许通过event_set_log_callback设置自定义日志回调,替换默认的event_err(默认会调用abort()触发崩溃)。你可以在初始化libevent前注册自己的回调,遇到错误时仅记录日志,不触发SIGABRT:
#include <event.h> #include <stdio.h> void custom_log_callback(int severity, const char* msg) { // 仅记录错误日志,不执行abort() fprintf(stderr, "[custom err] %s\n", msg); } int main() { // 注册自定义回调,覆盖默认崩溃逻辑 event_set_log_callback(custom_log_callback); // 后续正常调用event_base_new... }
注意:这种方式会全局改变libevent的错误处理行为,所有原本会触发崩溃的错误都会转为日志记录,需要自行在业务代码中判断event_base_new的返回值是否为NULL,处理初始化失败的情况。
2. 预检查文件描述符余量,提前规避错误
在调用event_base_new前,先检查当前可用的文件描述符数量,确保有足够FD供libevent初始化(libevent初始化信号处理需要创建socketpair,至少需要2个可用FD)。
可以通过getrlimit获取FD上限,再遍历/proc/self/fd目录(Linux环境)统计已使用的FD数量:
#include <sys/resource.h> #include <dirent.h> #include <stdio.h> int get_used_fds() { int count = 0; DIR* dir = opendir("/proc/self/fd"); if (dir == NULL) return -1; struct dirent* entry; while ((entry = readdir(dir)) != NULL) { count++; } closedir(dir); // 减去.和..两个目录项 return count - 2; } bool has_enough_fds() { struct rlimit rl; if (getrlimit(RLIMIT_NOFILE, &rl) != 0) return false; int used = get_used_fds(); if (used == -1) return false; // 预留至少2个FD给libevent的socketpair初始化 return (rl.rlim_cur - used) >= 2; }
调用event_base_new前先执行has_enough_fds(),如果返回false则跳过初始化,自行处理错误。
3. 修改libevent源码,让错误向上传递
如果上述方法无法满足需求,可以直接修改libevent源码:
- 找到
evsignal_init函数(signal.c文件)中调用event_err的位置,将其改为返回错误码,而非触发abort(); - 修改
epoll_init函数逻辑,让它在evsignal_init失败时返回错误,最终让event_base_new返回NULL。
这种方式需要重新编译libevent,适合对稳定性要求极高的场景,但会增加后续版本升级的维护成本。
内容的提问来源于stack exchange,提问作者Savior

