Libvirt 事件处理机制(二)客户端初始化监听与事件处理

 4小时前     7  

文章目录

上篇文章介绍了 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,表示循环监听所有虚机所有事件,且打印事件时带上时间戳。

打印结果如下,客户端监听到了虚机内部的重启和关机事件。

Libvirt 事件处理机制(二)客户端初始化监听与事件处理

现在我们具体介绍客户端事件监听机制。

2、libvirt 客户端事件监听机制

简单来说,libvirt 客户端事件监听机制就是基于 glib 事件循环(监听事件+定时器事件),客户端向服务端注册监听事件并设置回调函数,服务端在监听到相应事件后发送给客户端,客户端在收到事件后再调用回调函数进行处理

1、整体流程

整个事件监听流程包括客户端监听和服务端监听:

1、S:服务端启动时初始化事件循环接口(virEventRegisterDefaultImpl),默认基于 glib 实现;
2、S:服务端轮询(virEventRunDefaultImpl);
3、C:客户端初始化事件循环接口(virEventRegisterDefaultImpl),同样默认基于 glib 实现;
4、C:客户端连接服务端(virConnectOpenAuth);

  1. C:客户端添加对连接 socket 的监听,当 socket 可读即服务端向客户端发送内容时会触发回调 virNetSocketEventHandle

5、C:客户端向服务端注册监听事件并设置回调函数(比如对于虚机相关事件调用 virConnectDomainEventRegisterAny 进行注册);

  1. C:客户端添加定时器事件,并设置好回调函数如 myDomainEventCallbackvirDomainEventStateRegisterClient);
  2. C→S:客户端向服务端发送注册监听事件 rpc 请求(call(REMOTE_PROC_CONNECT_DOMAIN_EVENT_CALLBACK_REGISTER_ANY));

6、S:服务端处理 rpc 请求(remoteDispatchConnectDomainEventCallbackRegisterAny

  1. S:服务端添加定时器事件,并设置好回调函数如 remoteRelayDomainEventLifecycleqemuConnectDomainEventRegisterAny

7、C:客户端轮询(virEventRunDefaultImpl

当服务端监听到相应事件后,会进行一系列流程将事件发送给客户端:

1、S:服务端将事件放入队列中,并更新定时器 timeout=0( virObjectEventStateQueue->virObjectEventStateQueueRemote ,参考前文服务端初始化监听与事件处理);

2、S:服务端轮询中触发定时器事件,调用相关事件的回调如 remoteRelayDomainEventLifecyclevirEventGLibTimeoutDispatch->virObjectEventTimer);

  1. S→C:在 remoteRelayDomainEventLifecycle 中,调用 remoteDispatchObjectEventSend 将事件发送给客户端;

3、C:客户端监听 socket 可读,触发回调 virNetClientIncomingEventvirEventGLibHandleDispatch->virNetSocketEventHandle(前流程 4.1),经过一系列流程调用 virObjectEventStateQueueRemote,同样将事件放入队列并更新定时器 timeout=0;

4、C:客户端轮询中触发定时器事件,调用相关事件的回调如 myDomainEventCallbackvirEventGLibTimeoutDispatch->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
Libvirt 事件处理机制(二)客户端初始化监听与事件处理

对于 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 的监听,这样当服务端向客户端发送事件时会触发相应的回调(virEventGLibHandleDispatchvirNetSocketEventHandlevirNetClientIncomingEvent)。

Libvirt 事件处理机制(二)客户端初始化监听与事件处理

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 请求】
Libvirt 事件处理机制(二)客户端初始化监听与事件处理

注意,此时 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
Libvirt 事件处理机制(二)客户端初始化监听与事件处理

同样,此时 interval 为 -1,默认未启用定时器事件,需要等服务端监听到事件后才更新 interval 触发定时器事件。

3、事件处理流程

1、服务端处理

1、当 libvirt 服务端监测到 qemu 侧发送的虚机事件后,会调用virObjectEventStateQueue将事件加入队列并更新定时器 interval(参考前文服务端初始化监听与事件处理)。

Libvirt 事件处理机制(二)客户端初始化监听与事件处理

2、更新定时器 interval 为 0 后,会触发定时器事件,调用回调函数 virEventGLibTimeoutDispatchvirObjectEventTimer,经过一系列调用,最后调用到生命周期事件的回调函数 remoteRelayDomainEventLifecycle,将事件发送给客户端。

Libvirt 事件处理机制(二)客户端初始化监听与事件处理

2、客户端处理

1、客户端收到服务端发送的事件后,连接 socket 可读,触发回调virEventGLibHandleDispatchvirNetSocketEventHandlevirNetClientIncomingEvent,经过一系列处理,更新定时器 interval 为0;

Libvirt 事件处理机制(二)客户端初始化监听与事件处理

2、触发定时器事件,调用回调函数 virEventGLibTimeoutDispatchvirObjectEventTimer,经过一系列调用,最后调用到客户端设置的回调函数 myDomainEventCallback

Libvirt 事件处理机制(二)客户端初始化监听与事件处理

3、至此,整个事件监听处理流程完成。

Libvirt 事件处理机制(二)客户端初始化监听与事件处理

3、总结

libvirt 整个事件监听处理机制基于 glib 事件循环,设计得很巧妙。服务端和客户端都是一套机制,服务端监听 qmp socket,客户端监听连接 socket。当事件发生的时候再触发定时器事件,其中服务端定时器事件回调函数将事件发送给客户端,客户端定时器事件回调函数进行相应处理,处理后均将 interval 更新为 0,然后等待下一次事件。

下面是使用大模型根据本文内容生成的流程图,先用 Gemini 3.1 Pro 生成初始内容,再让 GPT-5.4 Thinking 进行优化,最后再人工进行一些调整,整体效果不错。

Libvirt 事件处理机制(二)客户端初始化监听与事件处理

此外,还有一些实现上的小细节:

  • 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、参考

1、Libvirt Event Loop 简介

2、libvirt的event监听机制和代码实现

3、Libvirt的事件机制

4、Module libvirt-event from libvirt

5、Libvirt's event loop

版权声明:小傅 发表于 4小时前,共 8727 字。
转载请注明:Libvirt 事件处理机制(二)客户端初始化监听与事件处理 | 太傅博客

暂无评论

暂无评论...