前言

上一篇文章介绍了 QEMU 设备模型的生命周期,分析了 QEMU 中设备类型注册、设备类型初始化、设备实例化等环节的基本原理与执行流程,在这个过程中我们不难发现 QEMU 设备模型采用了面向对象的设计思想,本文将以 8.0.0 版本的 QEMU RISC-V (qemu-system-riscv64) 为例,阐述 QEMU 中面向对象的设备管理机制。

在 Linux 中一切皆文件,而 QEMU 中模拟的一切实体皆对象。以 CPU 模拟为例,QEMU 中要实现对各种 CPU 架构的模拟,而且对于同一种架构的 CPU,比如 RISC-V 架构,由于功能特性的不同,也会有不同的 CPU 型号。任何型号的 CPU 中都有 CPU 的通用属性,同时也包含各自特有的属性,使用面向对象的设计思想可以非常高效地实现各种型号 CPU 的模拟。

此外,在主板上一个设备会通过总线与其他的设备相连接,而其他的设备也可以进一步通过总线与更多的设备连接,同时一个总线上会连接多个设备。这种总线型关系的模拟也可以非常便捷地使用面向对象的模型实现。

QEMU 对象模型(QEMU Object Model, QOM)提供了一个通用的面向对象框架,用于注册用户创建的设备类型和实例化这些类型的对象。QOM 提供以下功能:

  • 动态的设备类型注册系统
  • 支持设备类型的单一继承
  • 无状态接口的多重继承
  • 将内部成员映射到公开的属性

QOM 中最基础的对象是 TYPE_OBJECT,它提供所有对象共有的基本方法。

基类

ObjectClass 是 QOM 中所有类的基础,每一类对象会实例化一个 ObjectClass,类的成员是这类对象通用的内容:

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

struct ObjectClass
{
    /* private: */
    Type type;
    GSList *interfaces;

    const char *object_cast_cache[OBJECT_CLASS_CAST_CACHE];
    const char *class_cast_cache[OBJECT_CLASS_CAST_CACHE];

    ObjectUnparent *unparent;

    GHashTable *properties;
};

Object 是 QOM 中所有对象的基础,QOM 的每一个设备会实例化一个 ObjectObject 的成员是每个设备独有的内容:

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

struct Object
{
    /* private: */
    ObjectClass *class;
    ObjectFree *free;
    GHashTable *properties;
    uint32_t ref;
    Object *parent;
};

Object 的第一个成员是指向 ObjectClass 的指针。由于 C 语言保证结构体的第一个成员始终从该结构体首地址开始,只要任何子对象将其父对象作为第一个成员,就可以直接将其转换为 Object

QOM 中 ObjectClassObject 的关系,可以像上一篇文章中一样理解为设备类型与具体设备的关系,一个 ObjectClass 可以对应多个 Object,而 一个 Object 只会指向一个 ObjectClass。也可以将 ObjectClass 理解为设备驱动,Object 理解为设备。ObjectClass 所描述的主要是某一类设备的通用操作接口,而且 ObjectClass 也会实例化一个实体,这个实体是这一类设备所共用的;而每一个实际的设备都对应一个 Object,每个 Object 又会有一个指针指向 ObjectClass

下面以 virtio-net 设备为例,分析 QEMU 对象模型的继承和派生关系:

  1. QOM 最底层的基类是 ObjectClass,对应的对象是 Object
  2. 设备的类 DeviceClass,对应的对象是 DeviceState
  3. PCI 设备在设备类的基础上派生出了 PCIDeviceClass,对应的对象是 PCIDevice
  4. virtio-pci 设备又在 PCI 设备的基础上派生出 VirtioPCIClass,对应的对象是 VirtIOPCIProxy
  5. virtio-net 设备在 VIRTIO-PCI 设备的基础上进一步派生出新的类,这里需要注意的是 virtio-net 设备相比 virtio-pci 设备并不需要增加新的内容,所以这一层派生出的依旧是 VirtioPCIClass,但是对应的对象依然需要增加网络设备特有的内容,因此会派生出新结构体 VirtIONetPCI

本质上 C 语言中派生的实现形式就是结构体的包含嵌套,派生类层层包含父类,具体关系如下图所示:

正如上文在分析 Object 时介绍的那样,为了更好的在派生结构体之间互相引用,通常把被引用的结构体,也就是父类放在自己的第一个字段,这样就可以很方便地通过首地址加偏移量的方式进行对象类型转换。就拿上图中的 VirtIONetPCI 结构体来说,它的各级父类的 VirtioPCIProxyPCIDeviceDeviceState 以及 Object 指针指向的就是自己的首地址。

除了 ObjectClassObject 这两个结构体之外,QOM 中还有两个重要的数据结构,就是上一篇文章中已经介绍过的 TypeInfoTypeImplTypeInfo 是与 ObjectClassObject 并列的结构,每一类设备不仅对应一种 ObjectClassObject,而且还需要对应一个 TypeInfo 结构。在 QOM 中,TypeInfo 是统一的,并不是 ObjectClassObject 那样自底向上层层派生的结构。TypeInfo 是一个对外的接口,其目的是收集用户创建设备的必要信息,以实例化出相应的 ObjectClassObject。QOM 内部维护了一个全局的 TypeTmpl 哈希表,当 QMP 命令生产一个对象的时候,会从该表中找到对应的 TypeImpl 结构,然后根据 TypeImpl 的内容去初始化化相应的 ObjectClassObject。需要注意的是,如果已经注册过相同类型的设备,则已有 ObjectClass,无需再次初始化。

基于 QOM 的设备管理

创建新设备类型

使用 QOM 对象模型创建新设备类型的详细内容可以参考 QEMU 官方文档,QOM 各接口的使用说明详见 include/qom/object.h 文件中各函数的注释。这里仅以最简单的设备类型为例,介绍使用 QOM 对象模型创建新的设备类型的一般方法:

#include "qdev.h"

#define TYPE_MY_DEVICE "my-device"

// No new virtual functions: we can reuse the typedef for the
// superclass.
typedef DeviceClass MyDeviceClass;
typedef struct MyDevice
{
    DeviceState parent_obj;

    int reg0, reg1, reg2;
} MyDevice;

static const TypeInfo my_device_info = {
    .name = TYPE_MY_DEVICE,
    .parent = TYPE_DEVICE,
    .instance_size = sizeof(MyDevice),
};

static void my_device_register_types(void)
{
    type_register_static(&my_device_info);
}

type_init(my_device_register_types)

在上述代码中,我们使用 TypeInfo 结构体描述了新创建的 my-device 设备类型,并定义了设备类型注册函数。这里需要注意的是,在 MyDevice 结构体中,父对象必须是结构体的的第一个成员,以便实现父对象向子对象的投射。my_device_info 结构体的父类是 TYPE_DEVICE,该类是在 QEMU 中所有设备的父类,TYPE_DEVICE 提供一些通用方法来处理 QEMU 设备模型。

对于多个静态设备类型,也可以使用辅助宏 DEFINE_TYPES 一并注册:

static const TypeInfo device_types_info[] = {
    {
        .name = TYPE_MY_DEVICE_A,
        .parent = TYPE_DEVICE,
        .instance_size = sizeof(MyDeviceA),
    },
    {
        .name = TYPE_MY_DEVICE_B,
        .parent = TYPE_DEVICE,
        .instance_size = sizeof(MyDeviceB),
    },
};

DEFINE_TYPES(device_types_info)

如上文所述,每个 Object 都有一个与之关联的 ObjectClassObjectClass 是动态实例化的,但任何给定 Object 都只有一个 ObjectClass 实例。对于每个新的设备类型,都要定义由 ObjectClass 动态转换为 MyDeviceClass 的方法,也会定义由 Object 动态转换为 MyDevice 的方法。以下涉及的宏 OBJECT_GET_CLASSOBJECT_CLASS_CHECKOBJECT_CHECK 都在 include/qemu/object.h 文件中定义:

#define MY_DEVICE_GET_CLASS(obj) \
   OBJECT_GET_CLASS(MyDeviceClass, obj, TYPE_MY_DEVICE)
#define MY_DEVICE_CLASS(klass) \
   OBJECT_CLASS_CHECK(MyDeviceClass, klass, TYPE_MY_DEVICE)
#define MY_DEVICE(obj) \
   OBJECT_CHECK(MyDevice, obj, TYPE_MY_DEVICE)

另外,如果 ObjectClass 的实现可以作为模块构建,则必须调用 module_obj 函数,以确保 QEMU 在需要时正确加载模块:

module_obj(TYPE_MY_DEVICE);

设备类型初始化

在初始化 Object 之前,必须先初始化对应的 ObjectClassObjectClass 的初始化顺序是首先初始化父类,当父类对象初始化完成后,它将被复制到当前 ObjectClass 中,剩余都将被清零。这样做的目的在于,该 ObjectClass 能够自动继承父类已初始化的任何虚拟函数指针。

如果我们在定义新类型中,实现了父类的虚函数,那么需要定义新的 class 的初始化函数,并且在 TypeInfo 数据结构中,给 TypeInfoclass_init 字段赋予该初始化函数的函数指针:

#include "qdev.h"

void my_device_class_init(ObjectClass *klass, void *class_data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);
    dc->reset = my_device_reset;
}

static const TypeInfo my_device_info = {
    .name = TYPE_MY_DEVICE,
    .parent = TYPE_DEVICE,
    .instance_size = sizeof(MyDevice),
    .class_init = my_device_class_init,
};

需要注意的是,引入新的虚拟方法需要 Object 定义自己的结构体,并在 TypeInfo 中添加 .class_size 成员,每个方法还需要一个封装函数,以方便调用:

#include "qdev.h"

typedef struct MyDeviceClass
{
    DeviceClass parent_class;

    void (*frobnicate) (MyDevice *obj);
} MyDeviceClass;

static const TypeInfo my_device_info = {
    .name = TYPE_MY_DEVICE,
    .parent = TYPE_DEVICE,
    .instance_size = sizeof(MyDevice),
    .abstract = true, // or set a default in my_device_class_init
    .class_size = sizeof(MyDeviceClass),
};

void my_device_frobnicate(MyDevice *obj)
{
    MyDeviceClass *klass = MY_DEVICE_GET_CLASS(obj);

    klass->frobnicate(obj);
}

派生类

当我们需要从一个类创建一个派生类时,如果需要重写该类原有的虚拟方法,派生类中,可以增加相关的属性将类原有的虚拟函数指针保存,然后给虚拟函数赋予新的函数指针,保证父类原有的虚拟函数指针不会丢失:

typedef struct MyState MyState;

typedef void (*MyDoSomething)(MyState *obj);

typedef struct MyClass {
    ObjectClass parent_class;

    MyDoSomething do_something;
} MyClass;

static void my_do_something(MyState *obj)
{
    // do something
}

static void my_class_init(ObjectClass *oc, void *data)
{
    MyClass *mc = MY_CLASS(oc);

    mc->do_something = my_do_something;
}

static const TypeInfo my_type_info = {
    .name = TYPE_MY,
    .parent = TYPE_OBJECT,
    .instance_size = sizeof(MyState),
    .class_size = sizeof(MyClass),
    .class_init = my_class_init,
};

typedef struct DerivedClass {
    MyClass parent_class;

    MyDoSomething parent_do_something;
} DerivedClass;

static void derived_do_something(MyState *obj)
{
    DerivedClass *dc = DERIVED_GET_CLASS(obj);

    // do something here
    dc->parent_do_something(obj);
    // do something else here
}

static void derived_class_init(ObjectClass *oc, void *data)
{
    MyClass *mc = MY_CLASS(oc);
    DerivedClass *dc = DERIVED_CLASS(oc);

    dc->parent_do_something = mc->do_something;
    mc->do_something = derived_do_something;
}

static const TypeInfo derived_type_info = {
    .name = TYPE_DERIVED,
    .parent = TYPE_MY,
    .class_size = sizeof(DerivedClass),
    .class_init = derived_class_init,
};

热插拔

由于类的初始化过程不能失败,因此设备一般会有两个辅助函数 realizeunrealize 来专门处理动态设备的创建。在调用 realize 函数时,如果设备无法成功创建,则应设置 Error ** 指针。否则,在 realize 函数成功返回后,设备对象将被添加到 QOM 树中,并对客户机可见。与 realize 函数功能相反的是 unrealize 函数,主要负责在系统完成设备的使用之后清理释放资源。

QEMU 中所有设备都可以通过 C 代码实例化,但是只有部分设备可以通过命令行或者 QEMU Monitor 动态创建。同样,只有部分设备支持热插拔,即可以在创建后拔下,这需要给出明确的 unrealize 函数实现。另外,只有当设备的父总线注册了 HotplugHandler 时,设备才能被顺利拔出。

总结

本文聚焦 QEMU 中面向对象的设备管理机制,阐述了引入对象模型的必要性,介绍了 QOM 基本功能和顶层设计,分析了 ObjectObjectClass 的结构与联系,梳理了面向对象的设备管理中设备的层次关系,总结归纳了使用 QOM 接口管理设备的一般方法。在下一篇文章中,我们将继续深入,分析 QOM 面向对象特性的底层实现。

参考资料