diff --git a/articles/20230822-qemu-system-device-model-part2.md b/articles/20230822-qemu-system-device-model-part2.md new file mode 100644 index 0000000000000000000000000000000000000000..114a0969522d8691b1c94c29cb58349796711381 --- /dev/null +++ b/articles/20230822-qemu-system-device-model-part2.md @@ -0,0 +1,294 @@ +> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.2-rc2 - [urls]
+> Author: jl-jiang
+> Date: 2023/08/22
+> 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 设备模型简析(二):面向对象的设备管理 + +## 前言 + +上一篇文章介绍了 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`,类的成员是这类对象通用的内容: + +```c +/* 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` 的成员是每个设备独有的内容: + +```c +/* 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 对象模型的继承和派生关系: + +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 语言中派生的实现形式就是结构体的包含嵌套,派生类层层包含父类,具体关系如下图所示: + +![inherit.svg](images/qemu-system-device-model-part2/inherit.svg) + +正如上文在分析 `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 对象模型创建新的设备类型的一般方法: + +```c +#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` 一并注册: + +```c +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` 文件中定义: + +```c +#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 在需要时正确加载模块: + +```c +module_obj(TYPE_MY_DEVICE); +``` + +### 设备类型初始化 + +在初始化 `Object` 之前,必须先初始化对应的 `ObjectClass`。`ObjectClass` 的初始化顺序是首先初始化父类,当父类对象初始化完成后,它将被复制到当前 `ObjectClass` 中,剩余都将被清零。这样做的目的在于,该 `ObjectClass` 能够自动继承父类已初始化的任何虚拟函数指针。 + +如果我们在定义新类型中,实现了父类的虚函数,那么需要定义新的 class 的初始化函数,并且在 `TypeInfo` 数据结构中,给 `TypeInfo` 的 `class_init` 字段赋予该初始化函数的函数指针: + +```c +#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` 成员,每个方法还需要一个封装函数,以方便调用: + +```c +#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); +} +``` + +### 派生类 + +当我们需要从一个类创建一个派生类时,如果需要重写该类原有的虚拟方法,派生类中,可以增加相关的属性将类原有的虚拟函数指针保存,然后给虚拟函数赋予新的函数指针,保证父类原有的虚拟函数指针不会丢失: + +```c +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 设备管理机制概述][001] +- [The QEMU Object Model (QOM)][002] + +[001]: https://blog.csdn.net/leiyanjie8995/article/details/124613572 +[002]: https://www.qemu.org/docs/master/devel/qom.html diff --git a/articles/images/qemu-system-device-model-part2/inherit.svg b/articles/images/qemu-system-device-model-part2/inherit.svg new file mode 100644 index 0000000000000000000000000000000000000000..13e6cef650ac03e5099a78900083a90755382c4b --- /dev/null +++ b/articles/images/qemu-system-device-model-part2/inherit.svg @@ -0,0 +1,4 @@ + + + +


 

 

  VirtIOPCIClass
...


 

  PCIDeviceClass
...


  DeviceClass
DeviceClass...
  ObjectClass
  ObjectClass


 

 

 

  VortIONetPCI
...


 

 

  VirtIOPCIProxy
...


 

  PCIDevice
...


  DeviceState
DeviceState...
  Object
  Object
Text is not SVG - cannot display
\ No newline at end of file