上篇文章介绍了 libvirt/qemu/kvm 虚拟化环境中 libvirt 服务端事件处理机制,这篇文章介绍下 libvirt 客户端事件处理机制,包括初始化、注册监听与事件处理。
如有不对之处,烦请指正,感谢。
0、环境信息
- arch:x86_64
- libvirt:9.10.0
- qemu:10.2.0
其中 libvirt 来自 欧拉社区(调试方便),qemu 来自上游社区。
1、libvirt 客户端事件监听示例
我们先在虚机内部执行 reboot 和 shutdown 操作来看下 libvirt 客户端(virsh)监听到的事件。
客户端 virsh 命令为 virsh event --all --loop --timestamp,表示循环监听所有虚机所有事件,且打印事件时带上时间戳。
打印结果如下,客户端监听到了虚机内部的重启和关机事件。
现在我们具体介绍客户端事件监听机制。
2、libvirt 客户端事件监听机制
简单来说,libvirt 客户端事件监听机制就是基于 glib 事件循环(监听事件+定时器事件),客户端向服务端注册监听事件并设置回调函数,服务端在监听到相应事件后发送给客户端,客户端在收到事件后再调用回调函数进行处理。
1、整体流程
整个事件监听流程包括客户端监听和服务端监听:
1、S:服务端启动时初始化事件循环接口(virEventRegisterDefaultImpl),默认基于 glib 实现;
2、S:服务端轮询(virEventRunDefaultImpl);
3、C:客户端初始化事件循环接口(virEventRegisterDefaultImpl),同样默认基于 glib 实现;
4、C:客户端连接服务端(virConnectOpenAuth);
- C:客户端添加对连接 socket 的监听,当 socket 可读即服务端向客户端发送内容时会触发回调
virNetSocketEventHandle;
5、C:客户端向服务端注册监听事件并设置回调函数(比如对于虚机相关事件调用 virConnectDomainEventRegisterAny 进行注册);
- C:客户端添加定时器事件,并设置好回调函数如
myDomainEventCallback(virDomainEventStateRegisterClient); - C→S:客户端向服务端发送注册监听事件 rpc 请求(
call(REMOTE_PROC_CONNECT_DOMAIN_EVENT_CALLBACK_REGISTER_ANY));
6、S:服务端处理 rpc 请求(remoteDispatchConnectDomainEventCallbackRegisterAny)
- S:服务端添加定时器事件,并设置好回调函数如
remoteRelayDomainEventLifecycle(qemuConnectDomainEventRegisterAny)
7、C:客户端轮询(virEventRunDefaultImpl)
当服务端监听到相应事件后,会进行一系列流程将事件发送给客户端:
1、S:服务端将事件放入队列中,并更新定时器 timeout=0( virObjectEventStateQueue->virObjectEventStateQueueRemote ,参考前文服务端初始化监听与事件处理);
2、S:服务端轮询中触发定时器事件,调用相关事件的回调如 remoteRelayDomainEventLifecycle(virEventGLibTimeoutDispatch->virObjectEventTimer);
- S→C:在
remoteRelayDomainEventLifecycle中,调用remoteDispatchObjectEventSend将事件发送给客户端;
3、C:客户端监听 socket 可读,触发回调 virNetClientIncomingEvent( virEventGLibHandleDispatch->virNetSocketEventHandle(前流程 4.1),经过一系列流程调用 virObjectEventStateQueueRemote,同样将事件放入队列并更新定时器 timeout=0;
4、C:客户端轮询中触发定时器事件,调用相关事件的回调如 myDomainEventCallback(virEventGLibTimeoutDispatch->virObjectEventTimer)。
至此,整个监听流程处理完成,客户端设置的回调 myDomainEventCallback 在事件发生后被调用。
下面我们通过源码+gdb调试详细介绍具体流程。
2、客户端监听流程
客户端示例代码见附录 4,基于 libvirt 自带的示例 examples/c/misc/event-test.c 修改,监听虚机生命周期事件(VIR_DOMAIN_EVENT_ID_LIFECYCLE),回调函数为 myDomainEventCallback。
1、初始化事件循环接口
初始化事件循环接口调用的是 virEventRegisterDefaultImpl,就是对事件监听和定时器事件的实现进行初始化,其调用如下:
virEventRegisterDefaultImpl
- virEventGLibRegister
- virEventGLibRegisterOnce
- virEventRegisterImpl
- addHandleImpl = virEventGLibHandleAdd
- updateHandleImpl = virEventGLibHandleUpdate
- removeHandle = virEventGLibHandleRemove
- addTimeoutImpl = virEventGLibHandleAdd
- updateHandleImpl = virEventGLibTimeoutUpdate
- removeTimeoutImpl = virEventGLibHandleRemove
对于 virEventGLibHandleAdd,其实就是调用 virEventGLibAddSocketWatch 基于 glib 添加事件监听。同样地,对于 virEventGLibTimeoutAdd,就是调用 virEventGLibTimeoutCreate 基于 glib 添加定时器事件。至于更新和移除同样都是基于 glib 实现,就不具体介绍了。
# 添加事件监听
GSource *
virEventGLibAddSocketWatch(int fd,
GIOCondition condition,
GMainContext *context,
virEventGLibSocketFunc func,
gpointer opaque,
GDestroyNotify notify)
{
GSource *source = NULL;
source = virEventGLibCreateSocketWatch(fd, condition);
g_source_set_callback(source, (GSourceFunc)func, opaque, notify);
g_source_attach(source, context);
return source;
}
# 添加定时器事件
static GSource *
virEventGLibTimeoutCreate(int interval,
struct virEventGLibTimeout *data)
{
GSource *source = g_timeout_source_new(interval);
g_source_set_callback(source,
virEventGLibTimeoutDispatch,
data, NULL);
g_source_attach(source, NULL);
return source;
}
2、连接服务端
客户端通过 virConnectOpenAuth 连接服务端时会调用 virEventGLibHandleAdd 添加对 socket 的监听,这样当服务端向客户端发送事件时会触发相应的回调(virEventGLibHandleDispatch→virNetSocketEventHandle→virNetClientIncomingEvent)。
3、注册监听事件及回调函数
对于虚机相关事件,客户端调用 virConnectDomainEventRegisterAny 进行注册,其包括通过添加定时器事件设置回调函数和向服务端发起注册 rpc 请求。
virConnectDomainEventRegisterAny
- conn->driver->connectDomainEventRegisterAny | remoteConnectDomainEventRegisterAny
- virDomainEventStateRegisterClient【添加定时器事件回调函数】
- virObjectEventStateRegisterID
- state->timer = virEventAddTimeout(-1, -1, virObjectEventTimer, state, ...)
- addTimeoutImpl | virEventGLibTimeoutAdd
- data->cb = cb = virObjectEventTimer
- virObjectEventCallbackListAddID
- cb->cb = callback | **myDomainEventCallback**
- call(REMOTE_PROC_CONNECT_DOMAIN_EVENT_CALLBACK_REGISTER_ANY) | rpc call【向服务器发起 rpc 请求】
注意,此时 interval 为 -1,默认未启用定时器事件,需要等客户端收到服务端发送的事件后才更新 interval 触发定时器事件。
4、轮询
客户端调用 virEventRunDefaultImpl进行轮询,如果检测到事件,则调用相应回调函数。
virEventRunDefaultImpl
- virEventGLibRunOnce
- g_main_context_iteration
3、服务端监听流程
1、初始化事件循环接口及轮询
服务端在启动的时候调用 virEventRegisterDefaultImpl 完成事件循环接口初始化,并调用 virEventRunDefaultImpl 进行轮询。
main
- virNetDaemonNew
- virEventRegisterDefaultImpl
- virNetDaemonRun
- virEventRunDefaultImpl
2、处理客户端注册监听事件请求
对于客户端的注册监听请求,服务端调用remoteDispatchConnectDomainEventCallbackRegisterAny进行处理,组装好 callback 结构体后,调用 virConnectDomainEventRegisterAny 完成事件注册处理,实际上也是添加定时器事件,对于生命周期事件,其回调函数为 remoteRelayDomainEventLifecycle。
remoteDispatchConnectDomainEventCallbackRegisterAny
- virConnectDomainEventRegisterAny
- conn->driver->connectDomainEventRegisterAny | qemuConnectDomainEventRegisterAny
- virDomainEventStateRegisterID
- virObjectEventStateRegisterID
- state->timer = virEventAddTimeout(-1, -1, virObjectEventTimer, state, ...)
- addTimeoutImpl | virEventGLibTimeoutAdd
- data->cb = cb = virObjectEventTimer
- virObjectEventCallbackListAddID
- cb->cb = callback | remoteRelayDomainEventLifecycle
同样,此时 interval 为 -1,默认未启用定时器事件,需要等服务端监听到事件后才更新 interval 触发定时器事件。
3、事件处理流程
1、服务端处理
1、当 libvirt 服务端监测到 qemu 侧发送的虚机事件后,会调用virObjectEventStateQueue将事件加入队列并更新定时器 interval(参考前文服务端初始化监听与事件处理)。
2、更新定时器 interval 为 0 后,会触发定时器事件,调用回调函数 virEventGLibTimeoutDispatch→virObjectEventTimer,经过一系列调用,最后调用到生命周期事件的回调函数 remoteRelayDomainEventLifecycle,将事件发送给客户端。
2、客户端处理
1、客户端收到服务端发送的事件后,连接 socket 可读,触发回调virEventGLibHandleDispatch→virNetSocketEventHandle→virNetClientIncomingEvent,经过一系列处理,更新定时器 interval 为0;
2、触发定时器事件,调用回调函数 virEventGLibTimeoutDispatch→virObjectEventTimer,经过一系列调用,最后调用到客户端设置的回调函数 myDomainEventCallback。
3、至此,整个事件监听处理流程完成。
3、总结
libvirt 整个事件监听处理机制基于 glib 事件循环,设计得很巧妙。服务端和客户端都是一套机制,服务端监听 qmp socket,客户端监听连接 socket。当事件发生的时候再触发定时器事件,其中服务端定时器事件回调函数将事件发送给客户端,客户端定时器事件回调函数进行相应处理,处理后均将 interval 更新为 0,然后等待下一次事件。
下面是使用大模型根据本文内容生成的流程图,先用 Gemini 3.1 Pro 生成初始内容,再让 GPT-5.4 Thinking 进行优化,最后再人工进行一些调整,整体效果不错。
此外,还有一些实现上的小细节:
- 在
remoteConnectDomainEventRegisterAny中,对于同类事件(同一个 eventID,count 为 1)只需要向服务端发送一次注册请求,避免重复调用; - 在
virObjectEventStateQueueRemote中,只在事件队列长度为 1 时(state->queue->count 为 1)启用定时器,因为第一个事件放入队列前队列是空的,需要从禁用状态(-1)切到立即触发(0)。此外,virObjectEventStateFlush在开始时把队列清空并把 interval 设为-1(禁用),因此下一批事件只有在队列长度 从 0 变成 1的那一刻需要把 interval 重新设为 0 来触发一次 flush,队列已经非空时再设 0 只是重复调度、增加开销且可能造成抖动。
4、附录:客户端示例代码
- 编译命令:
gcc -g -o event-test event-test.c $(pkg-config --cflags --libs libvirt)
/*
* 客户端事件监听示例代码
* 监听虚机生命周期事件:VIR_DOMAIN_EVENT_ID_LIFECYCLE
* 回调函数:myDomainEventCallback
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#define VIR_ENUM_SENTINELS
#include <libvirt/libvirt.h>
#include <libvirt/virterror.h>
#define G_GNUC_UNUSED __attribute__((__unused__))
int run = 1;
static const char *
eventToString(int event)
{
switch ((virDomainEventType) event) {
case VIR_DOMAIN_EVENT_DEFINED:
return "Defined";
case VIR_DOMAIN_EVENT_UNDEFINED:
return "Undefined";
case VIR_DOMAIN_EVENT_STARTED:
return "Started";
case VIR_DOMAIN_EVENT_SUSPENDED:
return "Suspended";
case VIR_DOMAIN_EVENT_RESUMED:
return "Resumed";
case VIR_DOMAIN_EVENT_STOPPED:
return "Stopped";
case VIR_DOMAIN_EVENT_SHUTDOWN:
return "Shutdown";
case VIR_DOMAIN_EVENT_PMSUSPENDED:
return "PMSuspended";
case VIR_DOMAIN_EVENT_CRASHED:
return "Crashed";
case VIR_DOMAIN_EVENT_LAST:
break;
}
return "unknown";
}
// 回调函数
static int
myDomainEventCallback(virConnectPtr conn G_GNUC_UNUSED,
virDomainPtr dom,
int event,
int detail,
void *opaque G_GNUC_UNUSED)
{
printf("%s EVENT: Domain %s(%d) %s\n", __func__, virDomainGetName(dom),
virDomainGetID(dom), eventToString(event));
return 0;
}
static void
myFreeFunc(void *opaque)
{
char *str = opaque;
printf("%s: Freeing [%s]\n", __func__, str);
free(str);
}
static void
stop(int sig)
{
printf("Exiting on signal %d\n", sig);
run = 0;
}
int
main(int argc, char **argv)
{
int ret = EXIT_FAILURE;
virConnectPtr dconn = NULL;
int callbackret = 0;
size_t i;
if (virInitialize() < 0) {
fprintf(stderr, "Failed to initialize libvirt");
goto cleanup;
}
/* 初始化事件循环默认实现,基于 glib */
if (virEventRegisterDefaultImpl() < 0) {
fprintf(stderr, "Failed to register event implementation: %s\n",
virGetLastErrorMessage());
goto cleanup;
}
/* 连接服务端 */
dconn = virConnectOpenAuth(argc > 1 ? argv[1] : NULL,
virConnectAuthPtrDefault,
VIR_CONNECT_RO);
if (!dconn) {
printf("error opening\n");
goto cleanup;
}
signal(SIGTERM, stop);
signal(SIGINT, stop);
/* 注册事件及设置回调 */
printf("Registering event callbacks\n");
callbackret = virConnectDomainEventRegisterAny(dconn, NULL,
VIR_DOMAIN_EVENT_ID_LIFECYCLE,
VIR_DOMAIN_EVENT_CALLBACK(myDomainEventCallback),
strdup("VIR_DOMAIN_EVENT_ID_LIFECYCLE"),
myFreeFunc);
if (callbackret < 0)
goto cleanup;
/* 轮询 */
while (run) {
if (virEventRunDefaultImpl() < 0) {
fprintf(stderr, "Failed to run event loop: %s\n",
virGetLastErrorMessage());
}
}
printf("Deregistering event callbacks\n");
virConnectDomainEventDeregisterAny(dconn, callbackret);
ret = EXIT_SUCCESS;
cleanup:
if (dconn) {
printf("Closing connection: ");
if (virConnectClose(dconn) < 0)
printf("failed\n");
printf("done\n");
}
return ret;
}
5、参考
4、Module libvirt-event from libvirt