用 GLib 处理事件,并让连接清理及时运行
libvirt 把 FD 和定时器接到 GLib 主循环,将停止监听与释放对象分开,并提高连接清理任务的优先级,避免资源用尽时清理一直排不上。
工程问题#
移除 socket 监听时,调用方可能还持有 socket 锁,立即执行释放回调会再次请求同一把锁。把释放延后也有陷阱:当 FD 达到上限,accept() 持续失败,监听 socket 一直可读,低优先级的清理便可能一直没有机会执行。
实际实现#
统一 virEvent API 由 GLib 的 GSource 实现。Remove 先停止事件源、标记 removed,再安排高优先级 idle 回调释放 opaque。释放回调不占着 eventlock 执行;RPC socket 最后归还事件循环持有的引用。
处理流程#
- 通过统一 virEvent API 注册 FD 和定时器,由 GSource 交给主循环调度
- 删除时先停止 GSource 并标记 removed,后续 Update 不再找到该监听
- 以 G_PRIORITY_HIGH 安排一次清理,在 eventlock 外执行释放回调
- 归还 socket 的事件引用;最后一个引用消失时释放对象及其 FD
源码片段#
固定版本 · 原文摘录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}把文件描述符和定时器的增、改、删六个操作接到统一 virEvent API。调用方仍使用 libvirt 的接口;GLib 是这个固定版本的默认实现,不是公开 API 对未来后端的承诺。
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}创建 socket 对应的 GSource,设置回调后挂到指定 GMainContext。默认 handle 实现传入 NULL,使用全局默认 context;底层仍登记 GPollFD,不能把换成 GLib 理解为不再轮询或天然更快。
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}移除监听时仍持有 socket 锁。如果这里同步调用释放回调,后者再次锁定同一个 socket 就会遇到重入锁问题。注释明确把事件持有的引用留给事件循环回调释放。
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}先移除 GSource 并标记 removed,避免延迟清理期间的 Update 重新启用监听。随后把真正的释放排到高优先级 idle source;高优先级针对默认 I/O 事件,不会抢占一个已经运行的回调。
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}释放 opaque 的 ff 回调在 eventlock 外运行,完成后才锁住数组移除 handle。idle 回调返回 FALSE,只执行一次。延后调用使 ff 不在 RemoveHandle 的持锁调用栈中同步重入。
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}释放回调短暂锁住 socket,取出用户数据并清空回调指针,然后解锁后调用用户的 ff。g_autoptr 在函数退出时归还事件循环持有的 socket 引用;只有最后一个引用释放才会销毁对象。
六段连续源码串起后端注册、GSource 接入、持锁移除、延迟清理和 socket 引用归还。历史提交用于说明真实故障与方案变化,没有将原作者的复现步骤写成本站实测。
收益与取舍#
工程分析带来的收益
- 调用方通过六个事件操作使用同一套接口,默认实现可以从原有 poll 后端改为 GLib
- 移除与释放分开,避免释放回调沿着持锁调用链立即重入
- 高优先级清理不会被持续就绪的默认优先级监听事件一直压住,有机会归还已关闭连接的 FD
付出的代价
- 删除监听不等于已经释放 opaque 或关闭 FD,主循环必须继续运行,其他引用也必须归还
- G_PRIORITY_HIGH 只影响尚未调度的事件,不会中断正在执行的长回调,也不是“优先级越高越好”
- v10.10.0 默认使用 GLib 仍是内部实现细节;应用需要保证 GLib 集成时应使用 libvirt-glib 的相应接口
可比较的选择
原有 virEventPoll 后端自己管理 poll、定时器和唤醒。2020 年改接 GLib 沿用了统一 API;这个变化本身不能证明吞吐或延迟更优。
默认低优先级 idle 清理此前的真实实现。FD 用尽且监听 socket 持续可读时,普通 I/O 事件可能一直先运行,待清理连接占用的 FD 迟迟不能释放。
版本与适用范围#
固定于 libvirt v10.10.0 的默认 GLib 事件后端及 RPC socket 路径,以非 Windows FD 为例。高优先级修复针对 handle 清理,不能推广为所有 timeout 都使用同一优先级。GSource 停止、GSource 引用释放、opaque 清理和 socket 最终关闭是不同步骤;不承诺任意嵌套主循环或应用回调都天然安全。
参考来源#
源码、官方文档与原始论文相关实现与比较#
4 个相关案例nginx 将小额分配变成池内游标推进,并以池的生命周期统一释放;较大的分配另建记录管理。
Go 的 HTTP 正文传输复用 io.CopyBuffer;复制器以 Reader/Writer 为基础,并探测可选的专用传输能力。
KVM 预先匹配设备通知地址,命中后 signal eventfd,使通知无需让 vCPU 逐次退回 QEMU 分派。
QEMU 重建 FlatView 后发布新指针,已有读者继续使用旧视图,直到引用归还且 RCU 宽限期结束才销毁。