目录
虚拟化技术栈 · libvirt

libvirt GLib 事件循环:避免连接清理饥饿

在文件描述符耗尽时,优先执行连接清理,使服务有机会释放 FD 并恢复接收请求。

vireventglib: Remove handles with the highest priorityMichal Privoznik · 原作者故障分析与修复提交

1问题背景#

服务用尽可打开的文件描述符后,accept() 因 EMFILE 失败,监听 socket 却持续处于可读状态。事件循环反复处理接收失败,低优先级的旧连接清理得不到执行,FD 无法及时释放。

2原有方案#

libvirt 原先使用自身的 poll 事件后端,2020 年在同一 virEvent 接口下接入 GLib。随后发现的故障出在任务优先级:连接清理使用普通 g_idle_add,优先级低于持续就绪的 I/O 事件,可能长期无法执行。

3设计方案#

移除监听时销毁事件源并设置 removed,防止延迟清理期间重新启用监听。对象释放交由主循环执行,清理任务使用 G_PRIORITY_HIGH,优先于默认优先级的 I/O 事件运行。

4改进分析#

延迟释放避免释放回调在移除监听的持锁调用链中重入;提高优先级则避免旧连接清理被失败的 accept 持续阻塞。修复只调整 handle 清理的优先级,原作者提交提供了故障分析和复现步骤。

5源码分析#

5.1事件接口与后端注册#

virEvent 为文件描述符和定时器分别提供增加、更新、移除操作。注册 GLib 实现后,这六个操作由同一事件后端处理。默认使用 GLib 属于内部实现选择,公开 API 不保证该后端保持不变。

src/util/vireventglib.cv10.10.0 · L490–L502
查看原文件
490static gpointer virEventGLibRegisterOnce(gpointer data G_GNUC_UNUSED)491{492    eventlock = g_new0(GMutex, 1);493    timeouts = g_ptr_array_new_with_free_func(g_free);494    handles = g_ptr_array_new_with_free_func(g_free);495    virEventRegisterImpl(virEventGLibHandleAdd,496                         virEventGLibHandleUpdate,497                         virEventGLibHandleRemove,498                         virEventGLibTimeoutAdd,499                         virEventGLibTimeoutUpdate,500                         virEventGLibTimeoutRemove);501    return NULL;502}
virEventGLibRegisterOnce

把文件描述符和定时器的增、改、删六个操作接到统一 virEvent API。调用方仍使用 libvirt 的接口;GLib 是这个固定版本的默认实现,不是公开 API 对未来后端的承诺。

5.2GSource 与 GMainContext#

GSource 保存 FD 事件的检查与派发方式,并附加到 GMainContext。默认 handle 路径传入 NULL,使用全局默认 context,由主循环等待事件并分派回调。Unix 版本仍以 GPollFD 描述待监听的 FD;接入 GLib 本身不代表消除了 poll 或提高了 I/O 性能。

src/util/vireventglibwatch.cv10.10.0 · L236–L252
查看原文件
236GSource *237virEventGLibAddSocketWatch(int fd,238                           GIOCondition condition,239                           GMainContext *context,240                           virEventGLibSocketFunc func,241                           gpointer opaque,242                           GDestroyNotify notify)243{244    GSource *source = NULL;245246    source = virEventGLibCreateSocketWatch(fd, condition);247    g_source_set_callback(source, (GSourceFunc)func, opaque, notify);248249    g_source_attach(source, context);250251    return source;252}
virEventGLibAddSocketWatch

创建 socket 对应的 GSource,设置回调后挂到指定 GMainContext。默认 handle 实现传入 NULL,使用全局默认 context;底层仍登记 GPollFD,不能把换成 GLib 理解为不再轮询或天然更快。

5.3持锁路径中的释放限制#

移除监听时仍持有 socket 锁,而释放函数 virNetSocketEventFree 会再次获取同一把锁。同步调用可能造成重入死锁,因此事件接口要求释放回调脱离该调用栈执行。

src/rpc/virnetsocket.cv10.10.0 · L2194–L2209
查看原文件
2194void virNetSocketRemoveIOCallback(virNetSocket *sock)2195{2196    virObjectLock(sock);21972198    if (sock->watch < 0) {2199        VIR_DEBUG("Watch not registered on socket %p", sock);2200        virObjectUnlock(sock);2201        return;2202    }22032204    virEventRemoveHandle(sock->watch);2205    /* Don't unref @sock, it's done via eventloop callback. */2206    sock->watch = -1;22072208    virObjectUnlock(sock);2209}
virNetSocketRemoveIOCallback

移除监听时仍持有 socket 锁。如果这里同步调用释放回调,后者再次锁定同一个 socket 就会遇到重入锁问题。注释明确把事件持有的引用留给事件循环回调释放。

5.4监听移除与清理优先级#

销毁事件源后设置 removed,使后续查找排除该 watch。清理使用高优先级 idle source,即使监听 socket 因 EMFILE 持续就绪,清理仍可优先运行,为释放旧连接占用的 FD、恢复 accept 创造条件。

src/util/vireventglib.cv10.10.0 · L277–L296
查看原文件
277    if (data->source != NULL) {278        g_source_destroy(data->source);279        vir_g_source_unref(data->source, NULL);280        data->source = NULL;281        data->events = 0;282    }283284    /* since the actual watch deletion is done asynchronously, a handleUpdate call may285     * reschedule the watch before it's fully deleted, that's why we need to mark it as286     * 'removed' to prevent reuse287     */288    data->removed = TRUE;289    g_idle_add_full(G_PRIORITY_HIGH, virEventGLibHandleRemoveIdle, data, NULL);290291    ret = 0;292293 cleanup:294    g_mutex_unlock(eventlock);295    return ret;296}
virEventGLibHandleRemove

先移除 GSource 并标记 removed,避免延迟清理期间的 Update 重新启用监听。随后把真正的释放排到高优先级 idle source;高优先级针对默认 I/O 事件,不会抢占一个已经运行的回调。

5.5释放回调与事件记录清理#

主循环调用 ff 后,才锁住 handles 数组删除记录。用户释放函数可以有自己的锁和清理逻辑;它不再沿着 RemoveHandle 的持锁路径同步重入。这里返回 FALSE,让这个清理任务只执行一次。

src/util/vireventglib.cv10.10.0 · L239–L255
查看原文件
239static gboolean240virEventGLibHandleRemoveIdle(gpointer data)241{242    struct virEventGLibHandle *h = data;243244    PROBE(EVENT_GLIB_REMOVE_HANDLE_IDLE,245          "watch=%d ff=%p opaque=%p",246          h->watch, h->ff, h->opaque);247    if (h->ff)248        (h->ff)(h->opaque);249250    g_mutex_lock(eventlock);251    g_ptr_array_remove_fast(handles, h);252    g_mutex_unlock(eventlock);253254    return FALSE;255}
virEventGLibHandleRemoveIdle

释放 opaque 的 ff 回调在 eventlock 外运行,完成后才锁住数组移除 handle。idle 回调返回 FALSE,只执行一次。延后调用使 ff 不在 RemoveHandle 的持锁调用栈中同步重入。

5.6socket 引用归还与关闭时机#

RPC 释放函数取出用户数据、清空回调后,释放 socket 锁,再调用用户 ff。函数退出时,g_autoptr 归还事件循环持有的引用。如果仍有其他引用,socket 不会立即销毁,因此移除监听并不保证 FD 已经关闭。

src/rpc/virnetsocket.cv10.10.0 · L2126–L2141
查看原文件
2126static void virNetSocketEventFree(void *opaque)2127{2128    g_autoptr(virNetSocket) sock = opaque;2129    virFreeCallback ff;2130    void *eopaque;21312132    virObjectLock(sock);2133    ff = sock->ff;2134    eopaque = g_steal_pointer(&sock->opaque);2135    sock->func = NULL;2136    sock->ff = NULL;2137    virObjectUnlock(sock);21382139    if (ff)2140        ff(eopaque);2141}
virNetSocketEventFree

释放回调短暂锁住 socket,取出用户数据并清空回调指针,然后解锁后调用用户的 ff。g_autoptr 在函数退出时归还事件循环持有的 socket 引用;只有最后一个引用释放才会销毁对象。

6适用范围与限制#

删除监听后仍要继续运行主循环,释放回调才有机会执行;socket 也只有在最后一个引用归还后才销毁。高优先级不能抢占正在执行的回调,耗时工作仍会阻塞同一个 context。

分析基于 libvirt v10.10.0;G_PRIORITY_HIGH 用于这里的 handle 清理,timeout 清理仍有不同安排。公开 API 不保证默认实现永远是 GLib。vir_g_source_unref 还包含旧 GLib 的兼容处理,不能把它和 opaque 释放回调混为一步。

7参考资料#

vireventglib: Remove handles with the highest priority

Daniel P. Berrangé:接入 GLib 默认事件实现

从 libvirt-glib 引入 GMainContext 实现

libvirt 事件接口的释放回调约定

GLib 主循环:context 与事件优先级

GLib idle_add_full

FD 耗尽的故障与修复来自原作者提交;本文对照固定版本源码解释调用顺序,未在本站重跑故障复现或性能测试。

65 个匹配案例