CentOS7移植代码到Rocky Linux8触发GLib断言失败崩溃问题
问题根因
- 报错触发的断言
(i <= 0 || fds[i - 1].fd < fds[i].fd)是GLib主循环在执行poll前的强校验,要求所有待监听的文件描述符必须严格按fd数值升序排列、无重复项。 - 版本差异是问题出现的核心诱因:CentOS 7 预装的GLib版本为2.50.3,该版本内部会自动对传入的pollfd列表做排序、去重,没有该校验逻辑,不规范的代码不会触发崩溃;Rocky Linux 8预装的GLib版本为2.56.4,新增了该断言校验,移除了部分场景下的自动排序逻辑,不符合要求的fd列表会直接触发进程abort。
- 从strace捕获的poll系统调用参数可以直接看到异常:待监听的fd列表顺序为
1、5、3、4,其中fd=5大于后续的fd=3、fd=4,完全不满足升序要求,直接触发断言。 - 问题源头是自定义GSource实现(对应
capture_msgs_funcs结构体):代码中创建了自定义GSource挂载到默认主上下文,该GSource负责向主循环返回待监听的pollfd列表,其返回的fd列表存在乱序、重复追加的问题。另外代码中开启了g_source_set_can_recurse(source, TRUE),如果递归调度主循环时重复追加fd,也会加剧该问题。
排查步骤
- 第一步:注释掉自定义GSource的attach相关逻辑,重新运行程序,如果崩溃消失,即可确认问题出在该自定义GSource的实现上。
- 第二步:定位
capture_msgs_funcs结构体中对应的query回调(GSourceFuncs的第三个函数指针,负责返回待监听的fd集合),检查fd写入逻辑:- 是否存在同一个fd被多次追加到poll数组的情况
- 是否未对追加的fd按数值从小到大做排序
- 第三步:检查是否存在多线程无锁操作默认主上下文的情况:GLib默认主上下文不是线程安全的,其他线程未持有上下文锁就追加GSource、修改pollfd列表,也会导致fd列表顺序被打乱。
修复方案
- 修正自定义GSource的
query回调逻辑,保证返回的pollfd列表无重复、严格升序:
参考实现逻辑如下:// 假设你已将所有需要监听的fd存入临时数组tmp_fds,有效长度为cnt // 1. 先对fd做去重 int dedup_len = 0; for (int i = 0; i < cnt; i++) { int duplicated = 0; for (int j = 0; j < dedup_len; j++) { if (tmp_fds[j] == tmp_fds[i]) { duplicated = 1; break; } } if (!duplicated) { tmp_fds[dedup_len++] = tmp_fds[i]; } } // 2. 对去重后的fd做升序排序 for (int i = 0; i < dedup_len; i++) { for (int j = i + 1; j < dedup_len; j++) { if (tmp_fds[i] > tmp_fds[j]) { int swap_tmp = tmp_fds[i]; tmp_fds[i] = tmp_fds[j]; tmp_fds[j] = swap_tmp; } } } // 3. 将排序去重后的fd写入GLib要求的pollfd数组 *n_fds = dedup_len; for (int i = 0; i < dedup_len; i++) { fds[i].fd = tmp_fds[i]; fds[i].events = G_IO_IN; // 按实际业务需要设置监听事件 fds[i].revents = 0; } - 规范主上下文的多线程操作:所有涉及主上下文、GSource的修改操作,必须放在
g_main_context_acquire()和g_main_context_release()包裹的临界区内,禁止跨线程无锁操作默认主循环。 - 移除不必要的递归配置:如果业务不需要递归调度主循环,删除
g_source_set_can_recurse(source, TRUE)这行配置,避免递归迭代时重复追加pollfd。
临时调试规避方案(不推荐生产环境使用)
可以通过设置环境变量G_MAIN_CONTEXT_POLL_SORT=1,强制GLib内部对待监听的pollfd列表做自动排序、去重,跳过断言检查。该参数不是所有GLib小版本都支持,且会带来额外性能开销,仅用于临时定位问题,正式环境必须修正代码逻辑。
内容的提问来源于stack exchange,提问作者ctsuzuki
相关产品推荐
相关产品推荐

