QOM模型
Qemu
自己提供了一套面向对象编程模型:QOM(QEMU Object Module)
,可以把其想像为与C++
的面向对象编程类似。
why
Qemu
中几乎所有的硬件,无论是cpu
,总线,内存还是各类device
,都是用这套方法实现的,其重要性可见一斑,那么我们为什么要使用QOM
呢?一是因为Qemu
是一款多架构硬件仿真软件,所以我们需要模拟出不同架构的cpu
,这些cpu
无疑会有不少共通的地方,也会有很多不同的地方,使用面向对象的方法无疑对模拟这些cpu
模型带来极大的便利,类比到各类device
也是一个道理,二是因为qemu
模拟的是一个完整的计算机,所以只模拟设备是不够的,还需要实现设备间的互通和信息交互,所以为了方便模拟一个device
会通过bus
与其他的device
相连接,一个device
上可以通过不同的bus
端口连接到其他的device
,而其他的device
也可以进一步通过bus
与其他的设备连接,同时一个bus
上也可以连接多个device
等情形,使用面向对象的QOM
模型是快捷方便的。
how
基础结构体
那么我们如何在qemu
中添加一个我们自定义的device
呢?官方给出的例子位于hw/misc/pci-testdev.c
和hw/misc/edu.c
四个重要的数据结构:
TypeInfo
:是用户用来定义一个Type
的工具型的数据结构。TypeImpl
:对数据类型的抽象数据结构,TypeInfo
的属性与TypeImpl
的属性对应。ObjectClass
:是所有类对象的基类,仅仅保存了一个整数type
。1
2
3
4
5
6
7
8
9
10
11
12
13
14//include/qom/object.h
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
:是所有对象的基类Base Object
,第一个成员变量为指向ObjectClass
的指针。1
2
3
4
5
6
7
8
9
10//include/qom/object.h
struct Object
{
/*< private >*/
ObjectClass *class;
ObjectFree *free;
GHashTable *properties;
uint32_t ref;
Object *parent;
};
QOM
创建新类型时需要指定其对象类OjectClass
和对象Object
,这两者通过TypeInfo
结构体指定,基本结构定义在 include/qom/object.h
,文件中的注释超级详细,不仅有对数据结构的字段说明和QOM
模型的用法,还有很多简单的例子,TypeImpl
定义在 qom/object.c
中,但是莫得注释。。。
1 | //include/qom/object.h |
1 | //qom/object.c |
自定义一个设备主要分为四个流程:
- 将
TypeInfo
注册为TypeImpl
- 初始化
Class
- 实例化
Object
- 添加
Property
将TypeInfo
注册为TypeImpl
用户定义了一个TypeInfo
,然后调用 type_register(TypeInfo)
或者 type_register_static(TypeInfo)
函数,就会生成相应的TypeImpl
实例,将这个TypeInfo
注册到全局的TypeImpl
的hash
表中。TypeInfo
的属性与TypeImpl
的属性对应,实际上qemu
就是通过用户提供的TypeInfo
创建的TypeImpl
的对象。
拿include/qom/object.h
中第一阶段的例子来看:
1 | //include/qom/object.h |
在my_device_info
中得字段name
定义了我们将来启动此设备时候传参-device
后面跟的值。
type_init
宏位于include/qemu/module.h
,其发生在qemu
的main
函数之前。
1 | //include/qemu/module.h |
这一过程的目的就是分配并创建TypeImpl
结构,使用TypeInfo
数据赋值,之后插入到一个hash
表之中,这个hash
表以ti->name
,也就是info->name
为key,value就是根据TypeInfo
生成的TypeImpl
。
在qemu
启动阶段vl.c:main()
,pci_testdev_register_types()
函数将会在module_call_init(MODULE_INIT_QOM)
中执行,通过遍历qom
队列中的module entry
。
初始化Class
之后的分析主要根据hw/misc/pci-testdev.c
来分析,其中模拟了一个pci-test
设备:
继承链:
首先我们要知道ObjectClass
是所有类的基类,对于pci
设备来说,ObjectClass
有以下继承链:
PCIDeviceClass
=>DeviceClass
=>ObjectClass
1 | //include/qom/object.h |
1 | //include/hw/xtensa/qdev-core.h |
1 | //include/hw/pci/pci.h |
接着上一大步,现在我们已经有了一个value为TypeImpl
的哈希表。下一步就是初始化表中的每个type
了,这一步可以看成是Class
的初始化,可以理解成每一个type
对应了一个class
,接下来会初始化Class
。
具体函数分析:
在初始化每个type
时候,调用到的是type_initialize
函数。ObjectClass
的分配和初始化就在此函数中实现,此外ObjectClass
和Type
的关联操作也在此函数中实现。
1 | /* include/qom/object.h */ |
1 | static TypeImpl *type_get_parent(TypeImpl *type) |
在type_initialize
中,会递归地对TypeImpl
中的parent
成员(TypeImpl
)递归调用type_initialize
,然后将创建出来的相应 ObjectClass
拷贝到自己class
的最前面。
类对象的第一个成员是parent_class
,由于父类对象会拷到子类对象的最前面,因此可以认为其指向父类的对象,如此构成链状的继承链,最终指向基类对象ObjectClass
。
可以发现Type
同样存在一条继承链,对于pci
设备而言:
TYPE_PCI_TEST_DEV(自定义)
=>TYPE_PCI_DEVICE
=>TYPE_DEVICE
=>TYPE_OBJECT
class_init
为我们自己自定义的初始化class
的函数,可以类比为C++
中的构造函数,其参数为自身,在定义TypeInfo
时填入:
1 | // hw/misc/pci-testdev.c |
DEVICE_CLASS
和PCI_DEVICE_CLASS
两个宏由OBJECT_CLASS_CHECK
封装:
1 | /* include/hw/qdev-core.h */ |
流程概况:
1 | type_initialize |
实例化Object
用户定义的PCI Type
的ObjectClass
实例的构造函数调用在type_register_static()
调用时即可完成,而Type
的Object
实例只有在QEMU
命令行中添加-device
选项时才会创建。
上面已经说到了Type hash table
的构造以及Class
的初始化,现在来说Object
实例的创建。
继承链:
首先还是继承链,对于基础的pci
设备来说有以下继承链:
PCIDevice
=>DeviceState
=>Object
1 | //include/qom/object.h |
1 | //include/hw/xtensa/qdev-core.h |
1 | //include/hw/pci/pci.h |
Object
继承类之间的转换依然是靠宏来实现:
1 | /* include/hw/qdev-core.h */ |
具体函数分析:
object_new(typename)
函数是实例化instance
的入口:
1 | // qom/object.c |
继续看object_new_with_type
函数:
1 | /* include/qom/object.h */ |
继续看object_initialize_with_type
函数:
1 | // qom/object.c |
继续看object_init_with_type
函数:
1 | // qom/object.c |
instance
的初始化的核心过程到这就结束了,虽然instance_init
是字面意义上的实例构造函数,但实际上,经过instance_init
函数初始化设备对应instance
后,这个instance
是不能直接使用的。其真正初始化逻辑的大头都放在realize(如:k->realize = pci_testdev_realize)
中做,比如创建对应的memory region
,挂载到对应bus
上等等。只有在realize
后,设备才算真正构造完成,可以拿来使用,所以实例的创建主要由realize
函数实现,且不同于type
和class
的构造,instance
是根据实际需要创建的,只有在命令行指定了设备或者是热插一个设备之后才会有实例的创建。
realize
函数是设备实例化callback/回调
函数。qemu
的设备初始化分为两步,一个是设备类型中定义的构造函数(instance_init
),创建设备(object_new
)时调用,另外一个是这里的realize
函数,在设备的realied
属性被设置为true
时调用,realize
函数被调用设备才是真正的被初始化并变的可用。
此外需要留意class
和instance
之间是通过instance
的class
字段联系在一起的。
流程概况:
1 | main => configure_accelerator => accel_init_machine(acc, ms) |
添加Property
在QEMU
中每个对象或设备都有一些属性,这些属性有可能是设备的某种状态,也有可能是某个标志等。设备的属性是通过其类型(ObjectClass
的properties
成员)来定义的,但是具体的属性的值是保存在对象Object
的properties
成员的。
关系整理
主要来梳理一下TypeInfo
,TypeImpl
,ObjectClass
,Object
之间的关系,因为TypeImpl
是中间工具变量,所以将其与TypeInfo
看为一类:
主要有三组:
最基础组,是所有设备的起点:
1 | static TypeInfo object_info = { |
二组,基础设备组:
1 | // hw/core/qdev.c |
三组,pci
基础设备组:
1 | // hw/pci/pci.c |
用一张我自己画的图片来整理一下在动态初始化时TypeImpl
,ObjectClass
和Object
之间的关系:(PS:Object
和ObjectClass
里的指针是从其class
元素发出的)
感悟&参考
QOM
是一个很庞大的工程,虽然只学习了一点皮毛,但是已经借鉴了很多前辈的文章,这里只是浅显地分析了一下pcidevice
相关的一小部分,还有bus
相关的部分有时间再填坑,但这也只是qemu
这个庞大软件的一小部分而已。。。不禁感叹自己的弱小与无力orz。
吾尝终日而思矣,不如须臾之所学也。
感觉我们在学习qemu
相关的知识时一定要利用好其开源的特性,这点十分重要,不懂的地方没有比读源码更快更正确的解决方式了。
借鉴了些前辈的文章,十分感激:
https://www.binss.me/blog/qemu-note-of-qemu-object-model/
https://blog.csdn.net/sungeshilaoda/article/details/97890633
https://blog.csdn.net/sungeshilaoda/article/details/97890633