diff --git a/articles/20230830-qemu-system-device-model-part3.md b/articles/20230830-qemu-system-device-model-part3.md new file mode 100644 index 0000000000000000000000000000000000000000..489fb965a608b786148236282534df01302d5d9d --- /dev/null +++ b/articles/20230830-qemu-system-device-model-part3.md @@ -0,0 +1,304 @@ +> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc2 - [urls refs autocorrect]
+> Author: jl-jiang
+> Date: 2023/08/30
+> Revisor: Bin Meng
+> Project: [RISC-V Linux 内核剖析](https://gitee.com/tinylab/riscv-linux)
+> Proposal: [【老师提案】QEMU 系统模拟模式分析](https://gitee.com/tinylab/riscv-linux/issues/I61KIY)
+> Sponsor: PLCT Lab, ISCAS + +# QEMU 设备模型简析(三):QOM 设计实现 + +## 前言 + +在 [上一篇文章][002] 中我们聚焦 QEMU 中面向对象的设备管理机制,阐述了引入对象模型的必要性,介绍了 QOM 基本功能和顶层设计,本文将在此基础上进一步深入,详细分析 QOM 对象系统的设计思路以及实现方式。 + +## 关键结构 + +通过之前的分析,我们已经知道 QOM 有如下几大关键数据结构:`Object`、`ObjectClass`、`TypeInfo` 和 `TypeImpl`,他们的关系如下图所示: + +![datastructure.svg](assets/datastructure.svg) + +通过上图不难看出,QOM 对象模型的核心结构是 `Object`,这个结构体十分简洁,只保存了有关类型和父类的信息。`ObjectClass` 结构体就比较复杂,保存指向类型信息 `TypeImpl` 结构体的指针,保证了能够获取有关类型的信息。`TypeImpl` 存储着一个类型的信息,包括类型名称、类型大小、是否抽象类、父类名称、父类类型指针、`ObjectClass` 等。`TypeInfo` 结构体没有直接与其他任何数据结构产生直接关联,通过前面的分析我们已经知道,这是面向开发者的一个工具结构,主要用于在注册类型的时候提供类型的基本信息,在类型注册伊始,QEMU 会自动生成对应的 `TypeImpl` 结构体,保存类型的全部信息。 + +## 面向对象特性 + +首先,我们需要回顾一下面向对象的基本特征: + +- 封装 +- 接口 +- 继承 +- 析构 +- 静态成员 +- 多态 +- 动态类型装换 + +下面我们将详细分析上述特性在 QOM 对象系统中的具体实现。 + +### 封装 + +在分析 QOM 如何实现封装之前,我们需要再次回顾 `Object` 结构体的定义: + +```c +/* include/qom/object.h: 153 */ + +struct Object +{ + /* private: */ + ObjectClass *class; + ObjectFree *free; + GHashTable *properties; + uint32_t ref; + Object *parent; +}; +``` + +这里需要关注的是第一行的注释,它表示结构体中的所有属性都是私有的,只能被类的内部成员访问和修改。但是,仅靠 C 语言中的结构体是无法实现对私有变量的访问控制的,因此 QEMU 在 `Object` 中引入了属性表,即 `properties` 指针,它指向一张哈希表,该表含了 `Object` 中的所有可以访问、修改的数据和函数其中每一个键值对表示 `property` 的名称以及指向相应 `ObjectProperty` 结构体的指针。下面给出 `ObjectProperty` 结构体的实现代码: + +```c +/* include/qom/object.h: 88 */ + +struct ObjectProperty +{ + char *name; + char *type; + char *description; + ObjectPropertyAccessor *get; + ObjectPropertyAccessor *set; + ObjectPropertyResolve *resolve; + ObjectPropertyRelease *release; + ObjectPropertyInit *init; + void *opaque; + QObject *defval; +}; +``` + +可以看到,`ObjectProperty` 结构体包含了这个属性的名称、类型、描述、读写方法以及解析和释放函数,还包括这个 `property` 特有的属性,使用 `opaque` 指针来表示。QOM 通过 `ObjectProperty` 结构体将对象的每个数据都保存在这样一个单元之中,再利用一个哈希表实现对对象所有数据的统一管理,进而实现了数据封装。当用户需要向 `Object` 中增加属性时,需要调用 `object_property_add` 函数: + +```c +/* qom/object.c: 1257 */ + +ObjectProperty * +object_property_add(Object *obj, const char *name, const char *type, + ObjectPropertyAccessor *get, + ObjectPropertyAccessor *set, + ObjectPropertyRelease *release, + void *opaque) +{ + return object_property_try_add(obj, name, type, get, set, release, + opaque, &error_abort); +} +``` + +该函数通过进一步调用 `object_property_try_add` 函数向 `properties` 所指向的哈希表中插入了一个新的属性: + +```c +/* qom/object.c: 1206 */ + +ObjectProperty * +object_property_try_add(Object *obj, const char *name, const char *type, + ObjectPropertyAccessor *get, + ObjectPropertyAccessor *set, + ObjectPropertyRelease *release, + void *opaque, Error **errp) +{ + ObjectProperty *prop; + size_t name_len = strlen(name); + + if (name_len >= 3 && !memcmp(name + name_len - 3, "[*]", 4)) { + int i; + ObjectProperty *ret = NULL; + char *name_no_array = g_strdup(name); + + name_no_array[name_len - 3] = '\0'; + for (i = 0; i < INT16_MAX; ++i) { + char *full_name = g_strdup_printf("%s[%d]", name_no_array, i); + + ret = object_property_try_add(obj, full_name, type, get, set, + release, opaque, NULL); + g_free(full_name); + if (ret) { + break; + } + } + g_free(name_no_array); + assert(ret); + return ret; + } + + if (object_property_find(obj, name) != NULL) { + error_setg(errp, "attempt to add duplicate property '%s' to object (type '%s')", + name, object_get_typename(obj)); + return NULL; + } + + prop = g_malloc0(sizeof(*prop)); + + prop->name = g_strdup(name); + prop->type = g_strdup(type); + + prop->get = get; + prop->set = set; + prop->release = release; + prop->opaque = opaque; + + g_hash_table_insert(obj->properties, prop->name, prop); + return prop; +} +``` + +### 继承 + +在面向对象编程中主要包括三种继承形式: + +- **可视继承:** QEMU 中可视继承主要用于处理图形界面的相关问题,这里不做深入讨论 +- **实现继承:** 子类能够直接使用基类的属性和方法而无需重新编写代码 +- **接口继承:** 子类仅使用基类的属性和方法名称,属性和代码的具体内容需要重新编写代码实现 + +对于实现继承,QOM 通过结构体的包含关系完成。在 QEMU 中我们创建一个新类时,会实现两个数据结构:类的数据结构 `ObjectClass` 和对象的数据结构 `Object`,由于这两个结构体中的第一个成员变量就是父类(对象),那么只要通过 “指针 + 偏移量” 就可以直接使用父类的属性和方法,完成了实现继承的功能。 + +对于接口继承,QEMU 中定义了专门的接口结构: + +```c +/* include/qom/object.h: 514 */ + +struct InterfaceClass +{ + ObjectClass parent_class; + /* private: */ + ObjectClass *concrete_class; + Type interface_type; +}; +``` + +在 QOM 中一个类可以实现多个接口,也就是接口继承。`ObjectClass` 结构体中与接口继承相关的属性是 `interfaces`,它指向一条链表,链表中的每个元素都是一个指向 `InterfaceClass` 的指针,再通过其中的 `interface_type` 指针指向一个 `TypeImpl` 结构体,我们可以通过给该指针指向的 `TypeImpl` 结构体中的函数指针赋值,从而达到实现对应接口的目的。 + +### 多态 + +多态是同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。为了实现多态,QOM 实现了一个非常重要的功能,即动态强制类型转换(dynamic cast)。我们可以使用相关的函数,将一个 `Object` 的指针在运行时 cast 为子类对象的指针,可以将一个 `ObjectClass` 的指针在运行时 cast 为子类的指针。这样就可以调用子类中定义的函数指针来完成相应的功能。动态 cast 功能主要由 `object_class_dynamic_cast` 函数实现: + +```c +/* qom/object.c: 908 */ + +ObjectClass *object_class_dynamic_cast(ObjectClass *class, + const char *typename) +{ + ObjectClass *ret = NULL; + TypeImpl *target_type; + TypeImpl *type; + + if (!class) { + return NULL; + } + + /* A simple fast path that can trigger a lot for leaf classes. */ + type = class->type; + if (type->name == typename) { + return class; + } + + target_type = type_get_by_name(typename); + if (!target_type) { + /* target class type unknown, so fail the cast */ + return NULL; + } + + if (type->class->interfaces && + type_is_ancestor(target_type, type_interface)) { + int found = 0; + GSList *i; + + for (i = class->interfaces; i; i = i->next) { + ObjectClass *target_class = i->data; + + if (type_is_ancestor(target_class->type, target_type)) { + ret = target_class; + found++; + } + } + + /* The match was ambiguous, don't allow a cast */ + if (found > 1) { + ret = NULL; + } + } else if (type_is_ancestor(type, target_type)) { + ret = class; + } + + return ret; +} +``` + +### 析构 + +QOM 的通过 “引用计数” 来判断何时调用析构函数删除对象,`Object` 的结构体中有一个专门用于对 `Object` 引用的计数变量 `ref`,如果 `ref` 的值减少为 0,就意味着系统不会继续使用这个对象了,那么就可以对相应的内存空间等进行回收操作: + +```c +/* qom/object.c: 1192 */ + +void object_unref(void *objptr) +{ + Object *obj = OBJECT(objptr); + if (!obj) { + return; + } + g_assert(obj->ref > 0); + + /* parent always holds a reference to its children */ + if (qatomic_fetch_dec(&obj->ref) == 1) { + object_finalize(obj); + } +} +``` + +在注册类型时,可以通过定义 `TypeInfo` 结构体中的 `instance_finalize` 实现自定义析构函数,对于引用计数为 0 的 `Object` 进行垃圾回收操作。QOM 提供了默认析构函数: + +```c +/* qom/object.c: 688 */ + +static void object_finalize(void *data) +{ + Object *obj = data; + TypeImpl *ti = obj->class->type; + + object_property_del_all(obj); + object_deinit(obj, ti); + + g_assert(obj->ref == 0); + g_assert(obj->parent == NULL); + if (obj->free) { + obj->free(obj); + } +} +``` + +`Object` 数据结构中有一个 `ObjectFree *` 类型的函数指针 `free`,当 `Object` 的引用计数为 0 时,就会调用这个函数进行垃圾回收: + +```c +/* qom/object.c: 677 */ + +static void object_deinit(Object *obj, TypeImpl *type) +{ + if (type->instance_finalize) { + type->instance_finalize(obj); + } + + if (type_has_parent(type)) { + object_deinit(obj, type_get_parent(type)); + } +} +``` + +该函数会调用 `TypeImpl` 中的实例析构函数。如果存在父类,则会递归继续调用父类的实例析构函数。这里之所以需要调用父类实例析构函数是因为一个 `Object` 结构体的第一个成员变量就是父类对象的实例,因此当我们需要对对象析构时,不仅要调用当前类的析构方法,也需要调用父类的析构方法将结构体中的第一个成员进行析构。 + +## 总结 + +QOM 实现了一套较为完备的对象管理系统,包括自动化的对象注册、完善的对象管理以及巧妙的动态类型转换,本文梳理了 QOM 实现中几大关键数据结构之间的联系,详细分析了封装、多态、继承以及析构等面向对象特性在 QOM 中的集体实现,与之前的文章相呼应,打通了 QEMU 设备模型从使用到实现的全过程逻辑链条。 + +## 参考资料 + +- [Kvmforum14-qom.pdf][003] +- [QOM 实现分析][001] + +[001]: http://blog.chinaunix.net/uid-28541347-id-5784376.html +[002]: https://gitee.com/tinylab/riscv-linux/blob/master/articles/20230822-qemu-system-device-model-part2.md +[003]: https://www.linux-kvm.org/images/9/90/Kvmforum14-qom.pdf diff --git a/articles/assets/datastructure.svg b/articles/assets/datastructure.svg new file mode 100644 index 0000000000000000000000000000000000000000..2c04f83618c68c9afd305c1be1de2d8fd524e189 --- /dev/null +++ b/articles/assets/datastructure.svg @@ -0,0 +1,4 @@ + + + +
type
type
interfaces
interfaces
...
...
properties
properties
ObjectClass
ObjectClass
class
class
free
free
properties
properties
ref
ref
parent
parent
Object
Object
name
name
type
type
...
...
defval
defval
ObjectProperty
ObjectProperty
name
name
class
class
...
...
interfaces
interfaces
TypeImpl
TypeImpl
name
name
class
class
...
...
interfaces
interfaces
TypeInfo
TypeInfo
name
name
type
type
...
...
defval
defval
ObjectProperty
ObjectProperty
...
...
properties
properties
Text is not SVG - cannot display
\ No newline at end of file diff --git a/articles/images/qemu-system-device-model-part3/datastructure.svg b/articles/images/qemu-system-device-model-part3/datastructure.svg new file mode 100644 index 0000000000000000000000000000000000000000..2c04f83618c68c9afd305c1be1de2d8fd524e189 --- /dev/null +++ b/articles/images/qemu-system-device-model-part3/datastructure.svg @@ -0,0 +1,4 @@ + + + +
type
type
interfaces
interfaces
...
...
properties
properties
ObjectClass
ObjectClass
class
class
free
free
properties
properties
ref
ref
parent
parent
Object
Object
name
name
type
type
...
...
defval
defval
ObjectProperty
ObjectProperty
name
name
class
class
...
...
interfaces
interfaces
TypeImpl
TypeImpl
name
name
class
class
...
...
interfaces
interfaces
TypeInfo
TypeInfo
name
name
type
type
...
...
defval
defval
ObjectProperty
ObjectProperty
...
...
properties
properties
Text is not SVG - cannot display
\ No newline at end of file