QOM 简介
QOM(QEMU Object Model) 是 QEMU 的一个模块,用于描述虚拟机的结构,包括虚拟机的 CPU、内存、硬盘、网络、输入输出设备等。QEMU 为了方便整个系统的构建,实现了自己的一套的面向对象机制,也就是 QOM(QEMU Object Model)。它能够方便的表示各个设备(Device)与总线(Bus)之间的关系。
这个模型主要包含四个结构体:
- Object: 是所有对象的 基类 Base Object
- ObjectClass: 是所有类对象的基类
- TypeInfo:是用户用来定义一个
Type
的工具型的数据结构 - TypeImpl:TypeInfo 抽象数据结构,
TypeInfo
的属性与TypeImpl
的属性对应
在 QEMU 里要初始化一个对象需要完成四步:
- 将
TypeInfo
注册TypeImpl
- 实例化
Class
(ObjectClass) - 实例化
Object
- 添加
Property
如何描述硬件
一个板子上有很多硬件:芯片,LED、按键、LCD、触摸屏、网卡等等。芯片里面也有很多部件,比如 CPU、GPIO、SD 控制器、中断控制器等等。
这些硬件,或是部件,各有不同。怎么描述它们?
每一个都使用一个 TypeInfo
结构体来描述,TypeInfo
是用户用来定义一个 Type 的工具型的数据结构。它包含了很多成员变量,这些成员合在一起描述了一个设备类型。
// include/qom/object.h
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;
};
这个结构体我们在刚刚也提到,他在图里是独立的,在注册的时候会将它的信息都传给 Typeimpl 结构体。
我们以 Timer 为例,我们要添加一个 Timer 外设,首先就要定义一个 Typeinfo 结构体。他在代码中像这样。我们只看 name,这里用一个宏赋值,这个宏是个我们定义的字符串,它唯一标识了这个硬件。这些结构体在运行时会被注册进程序里,保存在一个链表中备用,为什么是备用,因为不是每一个硬件都会被用到。
// hw/timer/dw_timer.c
static const TypeInfo dw_timer_info = {
.name = TYPE_DW_TIMER,
.parent = TYPE_SYS_BUS_DEVICE,
.instance_size = sizeof(DWTimerState),
.instance_init = dw_timer_init,
.class_init = dw_timer_class_init,
};
这些结构体在运行时会被注册进程序里,保存在一个链表中备用,为什么是备用,因为不是每一个硬件都会被用到。
如何注册硬件
什么是注册,说白了就是将一些可能需要的信息添加到系统中,在系统运行时能够随时调用到。就拿 Timer 来说,现在将一些信息添加到了列表,系统运行起来时我可以随时从链表中取出 Timer 这个设备的信息,用来实例化一个 Timer,但是我没有注册 Timer,也就是没有将其加入到链表,那我后期就无法找到它。
怎么注册这些TypeInfo
结构体呢?在实现的源码中有这个函数 dw_timer_register_types()
,他是用来注册 Timer 这个设备的。
我们追根溯源,调用过程如下,
- 分配一个
TypeImpl
结构体,使用Typeinfo
来设置它 - 把
TypeImpl
加入链表:type_table
在 QEMU 里面,有一个全局的哈希表 type_table
,用来存放所有定义的类。在 type_new
里面,我们先从全局表里面根据名字 type_table_lookup
查找找这个类。
- 如果找到,说明这个类曾经被注册过,就报错;
- 如果没有找到,说明这是一个新的类,则将
Typeinfo
里面信息填到TypeImpl
里面。type_table_add
会将这个类注册到全局的表里面。
以上的过程可以用上图来表示。Typeinfo
通过 type_new()
生成一个对应的 TypeImpl
类型,并以 name
为关键字添加到名为 type_table
的一个 hash table 中。
什么时候注册这些设备呢?不需要我们去调用注册函数,以 Timer 为例,在 hw/timer/dw_timer.c
中有如下代码,一般在最后一行:
type_init(dw_timer_register_types)
F12
找到这个宏定义,我们追根溯源,调用过程如下
type_init()
-> module_init()
-> register_module_init()
type_init(dw_timer_register_types)
#define type_init(function) module_init(function, MODULE_INIT_QOM)
#define module_init(function, type) \
static void __attribute__((constructor)) do_qemu_init_ ## function(void) \
{ \
register_module_init(function, type); \
}
void register_module_init(void (*fn)(void), module_init_type type)
{
ModuleEntry *e; //构造 ModuleEntry
ModuleTypeList *l; //构造链表
e = g_malloc0(sizeof(*e));
e->init = fn; //设置初始化函数,fn 即 sifive_gpio_register_types
e->type = type;
l = find_type(type);
QTAILQ_INSERT_TAIL(l, e, node);//将 ModuleEntry 插入链表尾
}
type_init
是个宏定义,调用了__attribute__((constructor))
函数,我们知道这个 C 语言中位数不多的在main
函数执行前,执行的函数。函数中调用了register_module_init
注册函数,说明在main
函数执行前,已经注册好硬件了。该函数将一个新的ModuleEntry
加到链表里。
注意,注册的只是个函数,并不是注册了设备。也就是已上过程,只是把一个 ModuleEntry
放到了一个链表里,这个 ModuleEntry
带了两个信息,一个是函数,一个是类型。这个函数就是我们真正的注册注册函数。
已上过程大概是如下所示:
那什么时候还真正注册设备呢,我们就得回到主函数,它有以下调用流程,在 module_call_init
中,我们会找到 MODULE_INIT_QOM
这种类型对应的 ModuleTypeList
找出列表中所有的 ModuleEntry
,然后调用每个 ModuleEntry
的 init
函数。
// softmmu/runstate.c
module_call_init(MODULE_INIT_QOM);
// utils/module.csoftmmu/runstate.c
void module_call_init(module_init_type type)
{
ModuleTypeList *l;
ModuleEntry *e;
// 找到 MODULE_INIT_QOM 这种类型对应的 ModuleTypeList
l = find_type(type);
QTAILQ_FOREACH(e, l, node) {
e->init();
}
}
初始化设备
到这里我们需要注意,我们在注册设备的时候虽然将设备从 Typeinfo
变成了 TypeImpl
,把 Typeinfo
里的信息都复制到了 TypeImpl
,但是 class_init
还没有被调用,也即这个类现在还处于纸面的状态。
什么时候才真正初始化这个类呢,这得等在用到它的时候。我们在一块板子上才会用到一个设备。我们使用的是 Sifive-e 这个板子,准确来说我们用的不是这个板子,我们只是在原先的代码上做了修改。
为了方便描述,就当是用的 sifive-e 这个板子。在实现的源码里,有 object_initialize_child
函数,跟踪一下调用流程可以看到最后在 type_initialize
函数中初始化了类。同时我们也看到在 object_init_with_type
函数中实例化了类。这个稍后再讲。
// hw/riscv/sifive_e.cstatic void sifive_e_soc_init(Object *obj)
{
MachineState *ms = MACHINE(qdev_get_machine());
SiFiveESoCState *s = RISCV_E_SOC(obj);
.
.
. object_initialize_child(obj, "timer", &s->timer,
TYPE_DW_TIMER);
}
object_initialize_child(obj, name, &s->timer, TYPE_DW_TIMER);
object_initialize_child_internal()
object_initialize_child_with_props()
object_initialize_child_with_propsv()
object_initialize()
object_initialize_with_type()
type_initialize()
{
if (ti->class_init) {
ti->class_init(ti->class, ti->class_data);
}
}
object_init_with_type()
{
if (ti->instance_init) {
ti->instance_init(obj);
}
}
在调用 class_init
函数时,其实就是调用的设备模块下的 dw_timer_class_init
,这个函数中又是一些配置,尤其是 realize
函数的配置。还有一些属性的配置,如 Timer 的频率。
到这里,我们才有有了一个真正意义上的设备类。
hw/timer/dw_timer.c
static void dw_timer_class_init(ObjectClass *klass, void *data)
{
// 这里又是一些配置,尤其是回调函数的配置
DeviceClass *dc = DEVICE_CLASS(klass);
dc->reset = dw_timer_reset;
// 设置 Timer 基本属性如频率等
device_class_set_props(dc, dw_timer_properties);
dc->vmsd = &vmstate_dw_timer;
dc->realize = dw_timer_realize;
}
实例化设备
说白了初始化过程就是在配置各种结构体成员的过程,比如刚刚的初始化过程就是在配置 DeviceClass
这个类的各个成员。实际上我们还没有真正实例化 Timer,我们还不能使用它。
我们只有在实例化后才能使用它,也就是之前提到的 instance_init()
。但是在 QEMU 中要实例化一个设备,不仅仅需要调用 instance_init
,还需要调用刚刚初始化时设置的 realize
函数。
static const TypeInfo dw_timer_info = {
.name = TYPE_DW_TIMER,
.parent = TYPE_SYS_BUS_DEVICE,
.instance_size = sizeof(DWTimerState),
.instance_init = dw_timer_init,
.class_init = dw_timer_class_init,
};
// hw/timer/dw_timer.c
static void dw_timer_init(Object *obj)
{
DWTimerState *s = DWTIMER(obj);
// 为这段内存注册回调函数
memory_region_init_io(&s->iomem, obj, &dw_timer_ops, s,
"dw_timer", 0x2000);
sysbus_init_mmio(SYS_BUS_DEVICE(obj), &s->iomem);
}
这两个函数的功能很像,具体细节差异我也还没弄明白,但是需要注意的是 instance_init
一定要在 realize
之前完成,并且没有错误。否则将无法完成实例化。
// hw/timer/dw_timer.c
static void dw_timer_realize(DeviceState *dev, Error **errp)
{
DWTimerState *s = DWTIMER(dev);
sysbus_init_irq(SYS_BUS_DEVICE(dev), &s->irq);
for (int i = 0; i < n; i++) {
s->timer[i] = timer_new_ns(QEMU_CLOCK_VIRTUAL, dw_timer_interrupt, s);
}
}
instance_init
这个函数主要完成的工作就是为一段内存绑定了读写函数,为什么要这么做,我们再往下看。
如何操作设备
设备创建完成了,那 QEMU 是如何模拟设备的行为的?这也是 QEMU 驱动开发最重要的一步,因为以上的部分是实现设备所必须的,我们只需要参考其他已经实现的模块,修改成我们的信息即可。
但是每个 IP 的寄存器不同,他们的功能也就不同,这是我们真正需要实现的内容。我们知道写驱动其实就是操作各个 IP 的寄存器,以实现想要的功能。对应到 QEMU 中,就成了在操作各个寄存器时,我们要在 QEMU 中将驱动寄存器的功能先模拟出来,再返回给驱动程序。
以 Timer 为例我想要获取 TimerNLoadCount
的值,真实硬件有这个寄存器保存了值,但是 QEMU 上我们就得维护一个变量去保存这个值。在需要的时候能读取到。比如代码里比较重要的参数是 offset
,这个参数是基于外设基地址的偏移,其实就是寄存器的偏移量。比如我们查看 Timer 的手册,TimerNLoadCount
偏移量为 0,所以当我们在驱动中读取地址为 0x2000000
时,代码就会走到这里,因为我们维护了一个 timer_n_load_count
变量,所以我直接将这个变量当前值返回即可,这就是这个寄存器的值。我们要写这个寄存器也同理,我们需要更新 timer_n_load_count
这个变量。
// hw/timer/dw_timer.c
static uint64_t dw_timer_read(void *opaque, hwaddr offset,
unsigned size)
{
DWTimerState *s = opaque;
int index = 0;
switch (offset) {
case TimerNLoadCount:
case 1*0x14:
case 2*0x14:
index = offset / 0x14;
return s->timer_n_load_count[index];
.
.
.
}
static void dw_timer_write(void *opaque, hwaddr offset,
uint64_t val64, unsigned size)
{
DWTimerState *s = opaque;
uint32_t value = val64;
int index = 0;
int change = 0; switch (offset) {
case TimerNLoadCount:
case 1*0x14:
case 2*0x14:
index = (offset) / 0x14;
s->timer_n_load_count[index] = value;
set_alarm_time(s,index);
return;
.
.
.
}
读写函数写好了,需要给谁调用呢。我们刚刚提到了,这是个回调函数,我们需要给一段内存注册这个回调函数。如代码所示。我们给 Timer iomem 绑定了读写函数。具体哪一段地址还没定,但是我们定了 0x2000
这么长一段。我觉得这里应该是最高位的一个寄存器偏移量。因为再高就没啥用了,或者就是 SoC 里定的寄存器空间大小 0x1000
。这里应该是为了图省事写的一个值。
// hw/timer/dw_timer.c
static const MemoryRegionOps dw_timer_ops = {
.read = dw_timer_read,
.write = dw_timer_write,
.endianness = DEVICE_NATIVE_ENDIAN,
};
static void dw_timer_init(Object *obj)
{
DWTimerState *s = DWTIMER(obj);
// 为这段内存注册回调函数
memory_region_init_io(&s->iomem, obj, &dw_timer_ops, s,
"dw_timer", 0x2000);
sysbus_init_mmio(SYS_BUS_DEVICE(obj), &s->iomem);
}
下面在hw/riscv/sifive_e.c
里会映射寄存器空间到 QEMU 的内存空间。
// hw/riscv/sifive_e.c
sysbus_mmio_map(SYS_BUS_DEVICE(&s->timer), 0, memmap[SIFIVE_E_DEV_TIMER].base);