QEMU 设备模型简析(二):面向对象的设备管理
前言
上一篇文章介绍了 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 的每一个设备会实例化一个 Object
,Object
的成员是每个设备独有的内容:
/* include/qom/object.h: 153 */
struct Object
{
/* private: */
ObjectClass *class;
ObjectFree *free;
GHashTable *properties;
uint32_t ref;
Object *parent;
};
Object
的第一个成员是指向 ObjectClass
的指针。由于 C 语言保证结构体的第一个成员始终从该结构体首地址开始,只要任何子对象将其父对象作为第一个成员,就可以直接将其转换为 Object
。
QOM 中 ObjectClass
和 Object
的关系,可以像上一篇文章中一样理解为设备类型与具体设备的关系,一个 ObjectClass
可以对应多个 Object
,而 一个 Object
只会指向一个 ObjectClass
。也可以将 ObjectClass
理解为设备驱动,Object
理解为设备。ObjectClass
所描述的主要是某一类设备的通用操作接口,而且 ObjectClass
也会实例化一个实体,这个实体是这一类设备所共用的;而每一个实际的设备都对应一个 Object
,每个 Object
又会有一个指针指向 ObjectClass
。
下面以 virtio-net 设备为例,分析 QEMU 对象模型的继承和派生关系:
- QOM 最底层的基类是
ObjectClass
,对应的对象是Object
- 设备的类
DeviceClass
,对应的对象是DeviceState
- PCI 设备在设备类的基础上派生出了
PCIDeviceClass
,对应的对象是PCIDevice
- virtio-pci 设备又在 PCI 设备的基础上派生出
VirtioPCIClass
,对应的对象是VirtIOPCIProxy
- virtio-net 设备在 VIRTIO-PCI 设备的基础上进一步派生出新的类,这里需要注意的是 virtio-net 设备相比 virtio-pci 设备并不需要增加新的内容,所以这一层派生出的依旧是
VirtioPCIClass
,但是对应的对象依然需要增加网络设备特有的内容,因此会派生出新结构体VirtIONetPCI
本质上 C 语言中派生的实现形式就是结构体的包含嵌套,派生类层层包含父类,具体关系如下图所示:
正如上文在分析 Object
时介绍的那样,为了更好的在派生结构体之间互相引用,通常把被引用的结构体,也就是父类放在自己的第一个字段,这样就可以很方便地通过首地址加偏移量的方式进行对象类型转换。就拿上图中的 VirtIONetPCI
结构体来说,它的各级父类的 VirtioPCIProxy
、PCIDevice
、DeviceState
以及 Object
指针指向的就是自己的首地址。
除了 ObjectClass
和 Object
这两个结构体之外,QOM 中还有两个重要的数据结构,就是上一篇文章中已经介绍过的 TypeInfo
和 TypeImpl
。TypeInfo
是与 ObjectClass
和 Object
并列的结构,每一类设备不仅对应一种 ObjectClass
和 Object
,而且还需要对应一个 TypeInfo
结构。在 QOM 中,TypeInfo
是统一的,并不是 ObjectClass
和 Object
那样自底向上层层派生的结构。TypeInfo
是一个对外的接口,其目的是收集用户创建设备的必要信息,以实例化出相应的 ObjectClass
和 Object
。QOM 内部维护了一个全局的 TypeTmpl
哈希表,当 QMP 命令生产一个对象的时候,会从该表中找到对应的 TypeImpl
结构,然后根据 TypeImpl
的内容去初始化化相应的 ObjectClass
和 Object
。需要注意的是,如果已经注册过相同类型的设备,则已有 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
都有一个与之关联的 ObjectClass
,ObjectClass
是动态实例化的,但任何给定 Object
都只有一个 ObjectClass
实例。对于每个新的设备类型,都要定义由 ObjectClass
动态转换为 MyDeviceClass
的方法,也会定义由 Object
动态转换为 MyDevice
的方法。以下涉及的宏 OBJECT_GET_CLASS
、OBJECT_CLASS_CHECK
和 OBJECT_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
之前,必须先初始化对应的 ObjectClass
。ObjectClass
的初始化顺序是首先初始化父类,当父类对象初始化完成后,它将被复制到当前 ObjectClass
中,剩余都将被清零。这样做的目的在于,该 ObjectClass
能够自动继承父类已初始化的任何虚拟函数指针。
如果我们在定义新类型中,实现了父类的虚函数,那么需要定义新的 class 的初始化函数,并且在 TypeInfo
数据结构中,给 TypeInfo
的 class_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,
};
热插拔
由于类的初始化过程不能失败,因此设备一般会有两个辅助函数 realize
和 unrealize
来专门处理动态设备的创建。在调用 realize
函数时,如果设备无法成功创建,则应设置 Error **
指针。否则,在 realize
函数成功返回后,设备对象将被添加到 QOM 树中,并对客户机可见。与 realize
函数功能相反的是 unrealize
函数,主要负责在系统完成设备的使用之后清理释放资源。
QEMU 中所有设备都可以通过 C 代码实例化,但是只有部分设备可以通过命令行或者 QEMU Monitor 动态创建。同样,只有部分设备支持热插拔,即可以在创建后拔下,这需要给出明确的 unrealize
函数实现。另外,只有当设备的父总线注册了 HotplugHandler
时,设备才能被顺利拔出。
总结
本文聚焦 QEMU 中面向对象的设备管理机制,阐述了引入对象模型的必要性,介绍了 QOM 基本功能和顶层设计,分析了 Object
和 ObjectClass
的结构与联系,梳理了面向对象的设备管理中设备的层次关系,总结归纳了使用 QOM 接口管理设备的一般方法。在下一篇文章中,我们将继续深入,分析 QOM 面向对象特性的底层实现。
参考资料
- 《QEMU/KVM 源码解析与应用》李强,机械工业出版社
- QOM 设备管理机制概述
- The QEMU Object Model (QOM)