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 的工具型的数据结构。它包含了很多成员变量,这些成员合在一起描述了一个设备类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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,这里用一个宏赋值,这个宏是个我们定义的字符串,它唯一标识了这个硬件。这些结构体在运行时会被注册进程序里,保存在一个链表中备用,为什么是备用,因为不是每一个硬件都会被用到。
1 2 3 4 5 6 7 8 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
中有如下代码,一般在最后一行:
1 type_init(dw_timer_register_types)
F12
找到这个宏定义,我们追根溯源,调用过程如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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; ModuleTypeList *l; e = g_malloc0(sizeof (*e)); e->init = fn; e->type = type; l = find_type(type); QTAILQ_INSERT_TAIL(l, e, node); }
type_init
是个宏定义,调用了__attribute__((constructor))
函数,我们知道这个 C 语言中位数不多的在main
函数执行前,执行的函数。函数中调用了register_module_init
注册函数,说明在main
函数执行前,已经注册好硬件了。该函数将一个新的ModuleEntry
加到链表里。
注意,注册的只是个函数,并不是注册了设备。也就是已上过程,只是把一个 ModuleEntry
放到了一个链表里,这个 ModuleEntry
带了两个信息,一个是函数,一个是类型。这个函数就是我们真正的注册注册函数。
已上过程大概是如下所示:
那什么时候还真正注册设备呢,我们就得回到主函数,它有以下调用流程,在 module_call_init
中,我们会找到 MODULE_INIT_QOM
这种类型对应的 ModuleTypeList
找出列表中所有的 ModuleEntry
,然后调用每个 ModuleEntry
的 init
函数。
1 2 module_call_init(MODULE_INIT_QOM);
1 2 3 4 5 6 7 8 9 10 11 void module_call_init (module_init_type type) { ModuleTypeList *l; ModuleEntry *e; 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
函数中实例化了类。这个稍后再讲。
1 2 3 4 5 6 7 8 9 { MachineState *ms = MACHINE(qdev_get_machine()); SiFiveESoCState *s = RISCV_E_SOC(obj); . . . object_initialize_child(obj, "timer" , &s->timer, TYPE_DW_TIMER); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 的频率。
到这里,我们才有有了一个真正意义上的设备类。
1 2 3 4 5 6 7 8 9 10 11 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; 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
函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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, };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
之前完成,并且没有错误。否则将无法完成实例化。
1 2 3 4 5 6 7 8 9 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
这个变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 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
。这里应该是为了图省事写的一个值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 的内存空间。
1 2 sysbus_mmio_map(SYS_BUS_DEVICE(&s->timer), 0 , memmap[SIFIVE_E_DEV_TIMER].base);
参考
QEMU 中基于 QOM 的 VFIO 类的定义 - EwanHai - 博客园
评论