QEMU 设备模型简析(一):生命周期
前言
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];
};
通过观察不难发现 TypeInfo
和 TypeImpl
两个结构体的结构相似,绝大多数成员变量都相同,那么为什么注册过程中不直接使用,而要重新复制一遍呢?这与 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
结构体,可以发现其中有一个名为 class
的 ObjectClass
类型的成员变量,所谓初始化其实就是初始化这个 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
完成实例化工作。抽象地看,设备实例化主要完成三件事:首先,建立 Object
和 ObjectClass
之间的关联;然后,递归调用父类型的 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
函数将 DeviceState
的 realized
设置为 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 对象模型以及面向对象的设备管理方法。
参考资料
- The QEMU Object Model (QOM)
- understanding_qemu
- 浅谈 QEMU 的对象系统
- 《QEMU/KVM 源码解析与应用》李强,机械工业出版社