前言

QEMU 系统模拟模式能够提供运行虚拟机所需的完整环境,包括 CPU,内存和外部设备等。其中,设备模拟是系统模拟模式的重要组成部分,也是系统模拟模式和用户模拟模式的本质区别之一。设备模型是 QEMU 中用于描述和实现设备模拟的软件结构和框架,它为设备提供了标准化的接口和方法,使得开发者可以轻松地为 QEMU 添加新的设备。本文将以 8.0.0 版本的 QEMU RISC-V (qemu-system-riscv64) 为例,分析介绍 QEMU 设备模型的生命周期。

设备类型注册

定义

QEMU 模拟的每一种设备都必须定义对应的设备类型,这个设备类型在代码中由结构体 TypeInfo 描述:

/* include/qom/object.h: 412 */

struct TypeInfo
{
    const char *name;
    const char *parent;

    size_t instance_size;
    size_t instance_align;
    void (*instance_init)(Object *obj);
    void (*instance_post_init)(Object *obj);
    void (*instance_finalize)(Object *obj);

    bool abstract;
    size_t class_size;

    void (*class_init)(ObjectClass *klass, void *data);
    void (*class_base_init)(ObjectClass *klass, void *data);
    void *class_data;

    InterfaceInfo *interfaces;
};

该结构体的成员变量比较繁多,目前只需要关注以下两个成员:

  • name: 设备类型名称
  • parent: 父设备类型名称

通过这两个变量我们可以发现,QEMU 设备模型中使用名称作为设备类型的唯一标识,并且设备类型之间还存在继承关系。

注册

完成设备类型的定义后,必须注册这一设备类型,以通知 QEMU 系统可以支持这一类型的设备,为后续添加这一类型的设备准备条件。注册工作由 type_register 函数负责:

/* qom/object.c: 140 */

static TypeImpl *type_register_internal(const TypeInfo *info)
{
    TypeImpl *ti;
    ti = type_new(info);

    type_table_add(ti);
    return ti;
}

TypeImpl *type_register(const TypeInfo *info)
{
    assert(info->parent);
    return type_register_internal(info);
}

该函数通过调用 type_new 生成一个 TypeInfo 对应的 TypeImpl 类型,并以 name 为关键字添加到名为 type_table 的一个哈希表中。type_table 是 QEMU 维护的一个全局的类型哈希表,使用类型名字符串索引到具体的 TypeImpl 对象。

下面给出 TypeImpl 结构体的定义:

/* qom/object.c: 48 */

struct TypeImpl
{
    const char *name;

    size_t class_size;

    size_t instance_size;
    size_t instance_align;

    void (*class_init)(ObjectClass *klass, void *data);
    void (*class_base_init)(ObjectClass *klass, void *data);

    void *class_data;

    void (*instance_init)(Object *obj);
    void (*instance_post_init)(Object *obj);
    void (*instance_finalize)(Object *obj);

    bool abstract;

    const char *parent;
    TypeImpl *parent_type;

    ObjectClass *class;

    int num_interfaces;
    InterfaceImpl interfaces[MAX_INTERFACES];
};

通过观察不难发现 TypeInfoTypeImpl 两个结构体的结构相似,绝大多数成员变量都相同,那么为什么注册过程中不直接使用,而要重新复制一遍呢?这与 QEMU 对象系统的设计理念有关,TypeInfo 结构体是面向 API 使用者的一个工具,使用者只有在注册设备类型的时候会使用到 TypeInfo,提供类型注册所需的必要信息和回调函数,此后 QEMU 会根据 TypeInfo 自动生成对应的 TypeImpl 结构体,至此 TypeInfo 的生命周期就结束了。换句话说,TypeInfo 保存静态的注册数据,而 TypeImpl 保存动态的运行数据,这种设计降低了系统的耦合度,提升了稳定性。

下面我们来分析 type_register() 函数的调用时机,这里就需要引申出另一个问题,就是注册函数本身也有 “注册 - 执行” 机制。我们以虚拟网卡设备 e1000 为例进行分析:

/* hw/e1000.c: 1822 */

static void e1000_register_types(void)
{
    int i;

    type_register_static(&e1000_base_info);
    for (i = 0; i < ARRAY_SIZE(e1000_devices); i++) {
        const E1000Info *info = &e1000_devices[i];
        TypeInfo type_info = {};

        type_info.name = info->name;
        type_info.parent = TYPE_E1000_BASE;
        type_info.class_data = (void *)info;
        type_info.class_init = e1000_class_init;

        type_register(&type_info);
    }
}

type_init(e1000_register_types)

函数 e1000_register_types 负责执行 e1000 网卡的设备类型注册,注意最后一行代码调用 type_init 函数对 e1000_register_types 函数进行注册,下面给出 type_init 函数的定义:

/* include/qemu/module.h: 56 */

#define type_init(function) module_init(function, MODULE_INIT_QOM)

type_init 函数实际上是通过宏的形式对 module_init 进行包装:

/* include/qemu/module.h: 35 */

#define module_init(function, type)                                         \
static void __attribute__((constructor)) do_qemu_init_ ## function(void)    \
{                                                                           \
    register_module_init(function, type);                                   \
}

这里我们需要关注的是这个修饰 __attribute__((constructor)),它是 GCC 的一个扩展,用于确保被修饰的函数在 main 函数执行之被调用。仍然以上文的 e1000 网卡为例,调用 type_init 时传入的函数名为 e1000_register_types,这个宏的作用就是生成函数 static void do_qemu_init_e1000_register_types(void),并保证其在 main 函数之前被调用,以达到自动初始化的目的。

下面分析 register_module_init 函数的行为:

/* util/module.c: 70 */

void register_module_init(void (*fn)(void), module_init_type type)
{
    ModuleEntry *e;
    ModuleTypeList *l;

    e = g_malloc0(sizeof(*e));
    e->init = fn;
    e->type = type;

    l = find_type(type);

    QTAILQ_INSERT_TAIL(l, e, node);
}

QEMU 维护了一个全局的链表头数组 init_type_list,分别指向不同类型的注册函数链表,具体如下图所示:

register_module_init 函数会将待注册的设备类型注册函数,这里的例子中是 e1000_register_types,添加到 MODULE_INIT_QOM 下标所指向的链表尾部,完成注册工作。直到此时,真正的设备类型注册函数都没有执行,到底什么时候进行设备类型的注册呢?在之前的文章中,我们有分析过 QEMU 主事件循环的一般执行流程,main 函数会调用 qemu_init 函数进行初始化,qemu_init 在执行过程中会调用 qemu_init_subsystems 函数,而 qemu_init_subsystems 则会调用 module_call_init 函数,并传入参数 MODULE_INIT_QOM

/* util/module.c: 755 */

void module_call_init(module_init_type type)
{
    ModuleTypeList *l;
    ModuleEntry *e;

    if (modules_init_done[type]) {
        return;
    }

    l = find_type(type);

    QTAILQ_FOREACH(e, l, node) {
        e->init();
    }

    modules_init_done[type] = true;
}

这里 module_call_init 函数作用就是找到 MODULE_INIT_QOM 对应的链表,然后依次执行链表项中的 init 成员函数,对于 e1000 网卡而言就是之前通过 register_module_init 注册的 e1000_register_types 函数。

到此为止,设备类型注册的逻辑链条就算完整了。

设备类型初始化

设备类型注册后,在使用之前需要初始化该类型,并生成对应的 ObjectClass 对象。

这里回顾一下上文介绍的 TypeImpl 结构体,可以发现其中有一个名为 classObjectClass 类型的成员变量,所谓初始化其实就是初始化这个 class 成员,负责具体初始化工作的是 type_initialize 函数:

/* qom/object.c: 286 */

static void type_initialize(TypeImpl *ti)
{
    TypeImpl *parent;

    if (ti->class) {
        return;
    }

    ti->class_size = type_class_get_size(ti);
    ti->instance_size = type_object_get_size(ti);
    /* Any type with zero instance_size is implicitly abstract.
     * This means interface types are all abstract.
     */
    if (ti->instance_size == 0) {
        ti->abstract = true;
    }
    if (type_is_ancestor(ti, type_interface)) {
        assert(ti->instance_size == 0);
        assert(ti->abstract);
        assert(!ti->instance_init);
        assert(!ti->instance_post_init);
        assert(!ti->instance_finalize);
        assert(!ti->num_interfaces);
    }
    ti->class = g_malloc0(ti->class_size);

    parent = type_get_parent(ti);
    if (parent) {
        type_initialize(parent);
        GSList *e;
        int i;

        g_assert(parent->class_size <= ti->class_size);
        g_assert(parent->instance_size <= ti->instance_size);
        memcpy(ti->class, parent->class, parent->class_size);
        ti->class->interfaces = NULL;

        for (e = parent->class->interfaces; e; e = e->next) {
            InterfaceClass *iface = e->data;
            ObjectClass *klass = OBJECT_CLASS(iface);

            type_initialize_interface(ti, iface->interface_type, klass->type);
        }

        for (i = 0; i < ti->num_interfaces; i++) {
            TypeImpl *t = type_get_by_name(ti->interfaces[i].typename);
            if (!t) {
                error_report("missing interface '%s' for object '%s'",
                             ti->interfaces[i].typename, parent->name);
                abort();
            }
            for (e = ti->class->interfaces; e; e = e->next) {
                TypeImpl *target_type = OBJECT_CLASS(e->data)->type;

                if (type_is_ancestor(target_type, t)) {
                    break;
                }
            }

            if (e) {
                continue;
            }

            type_initialize_interface(ti, t, t);
        }
    }

    ti->class->properties = g_hash_table_new_full(g_str_hash, g_str_equal, NULL,
                                                  object_property_free);

    ti->class->type = ti;

    while (parent) {
        if (parent->class_base_init) {
            parent->class_base_init(ti->class, ti->class_data);
        }
        parent = type_get_parent(parent);
    }

    if (ti->class_init) {
        ti->class_init(ti->class, ti->class_data);
    }
}

type_initialize 函数首先执行一些必要的检查,接着会设置类型的大小并创建该类型,设置类型实例的大小,为实例化设备做准备,
然后初始化父设备类型和接口类型,最后调用父设备类型的 class_base_init 和自己的 class_init 完成初始化工作。

此外,通过上述代码我们还可以得出两点结论:

  • QEMU 设备管理是一个树型结构
  • QEMU 设备模型采用了面向对象的思想,存在继承关系

本文重点关注设备的生命周期,以上两点这里不展开讨论,在后面的文章中会专题分析。

设备实例化

完成了设备类型的初始化,接着就要实例化设备了,这个过程由 object_initialize 函数实现,而 object_initialize 又通过调用 object_initialize_with_type 完成实例化工作。抽象地看,设备实例化主要完成三件事:首先,建立 ObjectObjectClass 之间的关联;然后,递归调用父类型的 instance_init 函数;最后,调用自己的 instance_init 函数。需要注意的是,这种抽象的视角简化了设备实例化流程,也模糊了很多真实场景下的具体细节。

实例化函数

下面以 DeviceClass 为例详细分析设备的实例化过程。DeviceClass 是 QEMU 设备模型的核心组件,它继承自 ObjectClass,提供了设备的基本行为和属性的模板,是很多设备的父类,其实例化过程很有代表性。下图给出了 DeviceClass 有关类和对象之间的关系:

这里我们需要关注的是负责实例化 DeviceClass 的函数 device_initfn

/* hw/core/qdev.c: 652 */

static void device_initfn(Object *obj)
{
    DeviceState *dev = DEVICE(obj);

    if (phase_check(PHASE_MACHINE_READY)) {
        dev->hotplugged = 1;
        qdev_hot_added = true;
    }

    dev->instance_id_alias = -1;
    dev->realized = false;
    dev->allow_unplug_during_migration = false;

    QLIST_INIT(&dev->gpios);
    QLIST_INIT(&dev->clocks);
}

device_initfn 函数的整体执行逻辑比较简单,主要分为以下四个步骤:

  • 设备状态转换: 将传入的 Object 指针转换为 DeviceState 指针。DEVICE 是一个宏,用于从基类 Object 转换到派生类 DeviceState
  • 检查机器准备状态: 使用 phase_check 函数检查当前的机器初始化阶段是否为 PHASE_MACHINE_READY。如果是,则设置设备的 hotplugged 属性为 1,表示该设备是在虚拟机已经启动后热插入的。同时,全局变量 qdev_hot_added 被设置为 true,表示有设备被热插入
  • 初始化设备属性:instance_id_alias 被设置为 -1,表示没有别名;realized 被设置为 false,表示设备尚未实现;allow_unplug_during_migration 被设置为 false,表示在迁移期间不允许拔出此设备
  • 初始化设备的 GPIO 和时钟列表: 使用 QLIST_INIT 宏初始化了设备的 GPIO 和时钟列表

realized 属性设置

我们注意到 device_initfn 函数将 DeviceStaterealized 设置为 false,意味着这个设备还没有被实现,那么这个属性究竟是什么时候被设置为 true 的呢?通过分析 QEMU 的启动流程,可以梳理出如下函数调用链:

QEMU 在初始化阶段经过一系列函数调用,最终调用 qdev_realize 函数,该函数是 QEMU 设备模型中的一个核心函数,用于激活设备,也就是将设备对应的 DeviceState 结构的 realized 属性设置为 true,表示设备已经被实例化并准备好在模拟环境中运行。在设置 realized 属性之前,函数会执行一些基础的检查,以确保设备尚未被实例化并且没有父总线,然后函数会检查设备总线类型并设置父总线,最后调用 object_property_set_bool 函数设置 realized 属性为 true,完成设备实例化。

设备销毁

设备销毁的步骤与设备创建过程正好相反,一般包含以下四个步骤:

  • 设备反实例化: 停止操作设备并释放占用资源后,通过代码 object_property_set_bool(OBJECT(dev), "realized", false, &error)realized 属性设置为 false,标记设备已经反实例化
  • 从总线中移除: 设备通常连接到一个总线,在销毁设备之前,需要将其从父总线中移除
  • 销毁设备对象: 调用 object_unref 函数减少设备对象的引用计数,当引用计数降为 0 时,并调用设备对象的析构函数,释放所占内存空间及其他资源
  • 反注册设备类型: 有时可能还需要反注册设备类型,注销回调函数,清理释放为设备分配的各种资源

总而言之,设备的销毁过程可以视作设备添加过程的逆操作,除了顺序相反之外,执行流程基本相似,设备销毁的细节也不是本文关注的重点,这里仅介绍主要步骤,不再详细分析。

总结

本文从设备的生命周期这一角度入手,介绍了设备类型注册、设备类型初始化、设备实例化等环节的基本原理和执行流程,打通了从设备类型表创建到设备销毁的逻辑链条,在一个较为抽象的层面对 QEMU 设备模型进行了初步分析。下一篇文章中,我们将专题介绍 QEMU 对象模型以及面向对象的设备管理方法。

参考资料