QEMU 源码分析 - 虚拟外设创建

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
// 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,这里用一个宏赋值,这个宏是个我们定义的字符串,它唯一标识了这个硬件。这些结构体在运行时会被注册进程序里,保存在一个链表中备用,为什么是备用,因为不是每一个硬件都会被用到。

1
2
3
4
5
6
7
8
// 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 中有如下代码,一般在最后一行:

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; //构造 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,然后调用每个 ModuleEntryinit 函数。

1
2
// softmmu/runstate.c
module_call_init(MODULE_INIT_QOM);
1
2
3
4
5
6
7
8
9
10
11
// 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 函数中实例化了类。这个稍后再讲。

1
2
3
4
5
6
7
8
9
// 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);
}
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;
    // 设置 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 函数。

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,
};
// 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 之前完成,并且没有错误。否则将无法完成实例化。

1
2
3
4
5
6
7
8
9
// 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 这个变量。

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
// 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。这里应该是为了图省事写的一个值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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 的内存空间。

1
2
// hw/riscv/sifive_e.c
sysbus_mmio_map(SYS_BUS_DEVICE(&s->timer), 0, memmap[SIFIVE_E_DEV_TIMER].base);

参考

  1. QEMU 中基于 QOM 的 VFIO 类的定义 - EwanHai - 博客园
QEMU 源码分析-外设模拟(以 GPIO 为例) 解决一台电脑配置两个 GIT 账户
You need to set install_url to use ShareThis. Please set it in _config.yml.

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×