序言

qemu 的强大不仅在于其能够模拟各种各样的设备如cpu、内存、网卡、磁盘等,而且还在于它精妙的架构模式也称为qemu object model。这种QOM的编程模式或者说框架使得各个开发者非常方便而且快速的在qemu当中添加各种设备模型,这篇文章就以kvm虚拟化当中最常见的virtio设备为例来讲讲QOM。

QOM讲解

qom 从本质上来讲它仍然是一个面向对象的编程模式,因为qemu是c语言来实现的,所拟相关的对象载体都是structure。在qemu当中,所有的设备在经过抽象之后都可以看成是一个object,这个object有两个属性:type和objectclass。object和objectclass可以理解为qemu当中所有设备的基类,而type则定义了每种object的相关初始化的具体方式。下面我们来具体看一下type的模板

struct TypeImpl
{
    const char *name;

    size_t class_size;

    size_t instance_size;

    void (*class_init)(ObjectClass *klass, void *data);
    void (*class_base_init)(ObjectClass *klass, void *data);

    void *class_data;

    void (*instance_init)(Object *obj);
    void (*instance_post_init)(Object *obj);
    void (*instance_finalize)(Object *obj);

    bool abstract;

    const char *parent;
    TypeImpl *parent_type;

    ObjectClass *class;

    int num_interfaces;
    InterfaceImpl interfaces[MAX_INTERFACES];
};

qemu当中设备的实现,从本质上来讲就是实现该设备的type当中如上所示的相关callback函数,以及定义该设备具体的instance(object的子类)和class(objectclass的子类),另外从上面也可以看到每个type也都会有自己的parent。如果只是基于这些,可能各位看官在认知上还是比较模糊。那下面我们以virtio-net设备为例,看看它是如何通过QOM这个架构初始化起来的。

QOM class和instance初始化

首先如果要使用virtio-net则在qemu命令行里需要加入如下opts sample:

-device virtio-net-pci,netdev=xx,mac=xx,bus=pci.0,addr=xx

然后在 qemu main的函数当中通过如下逻辑对相关的device进行初始化

qemu_opts_foreach(qemu_find_opts("device"),
                      device_init_func, NULL, &error_fatal);

接着通过device_init_func->qdev_device_add 这个调用栈来进行设备初始化。下面我们看一下qdev_device_add的核心的逻辑

DeviceClass *dc;
const char *driver, *path;
DeviceState *dev = NULL;
BusState *bus = NULL;
...... 

driver = qemu_opt_get(opts, "driver");

......

 dc = qdev_get_device_class(&driver, errp);

........
path = qemu_opt_get(opts, "bus");
.......

 /* create device */
dev = DEVICE(object_new(driver));

.......

我们先列举一部分核心实现,这里的driver就是上面的virtio-net-pci。为了更好的来说明后面的逻辑我们先把virtio-net-pci的type,instance, class各自的关联描述一下。

  • type
---------------------------------------------------------------------------->parent
TYPE_VIRTIO_NET_PCI->TYPE_VIRTIO_PCI->TYPE_PCI_DEVICE->TYPE_DEVICE->TYPE_OBJECT

上面的每一种type都会调用相应的type注册函数,将所应的type info注册到qemu的MODULE_INIT_QOM 的qlist当中。还是以virtio-net-pci 为例子

static const VirtioPCIDeviceTypeInfo virtio_net_pci_info = {
    .base_name             = TYPE_VIRTIO_NET_PCI,
    .generic_name          = "virtio-net-pci",
    .transitional_name     = "virtio-net-pci-transitional",
    .non_transitional_name = "virtio-net-pci-non-transitional",
    .instance_size = sizeof(VirtIONetPCI),
    .instance_init = virtio_net_pci_instance_init,
    .class_init    = virtio_net_pci_class_init,
};

static void virtio_net_pci_register(void)
{
    virtio_pci_types_register(&virtio_net_pci_info);
}

通过virtio_net_pci_register来进行信息注册,然后最终会调用 type_init(virtio_net_pci_register) 初始化这个注册。这个type_init函数相当关键,其具体实现如下:

#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);                                   \
}
static ModuleTypeList *find_type(module_init_type type)
{
    init_lists();

    return &init_type_list[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);
}

在c语言里面,通过__attribute__((constructor) 来修饰的代码是可以在main函数运行之前执行的,也就是说在qemu main函数相关逻辑之前会完成所有type在QOM list上的注册。然后通过下面的调用栈完成所有type 的具体注册逻辑(包含了type的创建)

qemu_init->module_call_init->virtio_net_pci_register->type_register
                                                        ->type_register_internal
                                                            ->type_new
  • instance
-----------------------------------------包含---------------------->
VirtIONetPCI->VirtIOPCIProxy->PCIDevice->DeviceState->Object
  • class

关于class这一块,需要注意的是如果子type没有具体的类则直接使用 parent type的class。

VirtioPCIClass->PCIDeviceClass->DeviceClass->ObjectClass

理完以上的逻辑关系之后,我们接着相关的实现往下走:

dc = qdev_get_device_class(&driver, errp);

有了上面的先验知识这个函数看起来就比较容易,其核心逻辑就是通过driver这里是virtio-net-pci找到其所属的type,然后对type进行初始化并返回这个type的class。这里重点讲一下type_initialize,它采用的是循环嵌套逻辑先初始化完成的父类之后才实现最终的子类。这里的初始化主要class_initclass_base_init。而且会把初始完的父类的内容赋给子类,具体赋值逻辑在

memcpy(ti->class, parent->class, parent->class_size);

注意,上面也提到如果子type里面没有设置具体的class,则子type的class_init的input就是其parent type的class,所以parent type的class_init里面所做的一些初始化赋值可能会在子type的class_init函数被覆盖。接着qdev_device_add 核心逻辑来到

dev = DEVICE(object_new(driver))

先看object_new这个函数,其具体的调用链如下

object_new->object_new_with_type->type_initialize
                                ->object_initialize_with_type

type_initialize 在 get device class的已经执行过,所以最终实现的核心逻辑在object_initialize_with_type 。在这个函数当中也是通过循环嵌套的方式来调用instance_initinstance_post_init

virtio-net realized

virtio-net设备实例化的完成是通过qdev_device_add 当中的这个函数来触发的

object_property_set_bool(OBJECT(dev), true, "realized", &err)

先来看一下这个函数的具体的调用链

object_property_set_bool->object_property_set_qobject->object_property_set

->object_property_find->object_class_property_find
ObjectProperty *object_class_property_find(ObjectClass *klass, const char *name,
                                           Error **errp)
{
    ObjectProperty *prop;
    ObjectClass *parent_klass;

    parent_klass = object_class_get_parent(klass);
    if (parent_klass) {
        prop = object_class_property_find(parent_klass, name, NULL);
        if (prop) {
            return prop;
        }
    }

    prop = g_hash_table_lookup(klass->properties, name);
    if (!prop) {
        error_setg(errp, "Property '.%s' not found", name);
    }
    return prop;
}

从上面的实现逻辑来看,依然是循环嵌套逻辑从子类一直到父类,直到找到有设置过name为"realized" 特性的类。通过走读代码可以发现在DeviceClassclass_init 函数实现里面是有设置过的,具体如下

device_class_init->
    object_class_property_add_bool(class, "realized",
                                   device_get_realized, device_set_realized,
                                   &error_abort);

因此实现逻辑会直接调用到device_set_realized,这个函数当中调用的核心实现为

DeviceClass *dc = DEVICE_GET_CLASS(dev);

......

dc->realize(dev, &local_err);
.......

再来分析一下这个realize callback具体的实现。再次走读相关的代码,可以发现相关的realize 赋值有两处:一处是在PCIDeviceClassclass_init 函数

static void pci_device_class_init(ObjectClass *klass, void *data)
{
    DeviceClass *k = DEVICE_CLASS(klass);

    k->realize = pci_qdev_realize;   //realize函数
    k->unrealize = pci_qdev_unrealize;
    k->bus_type = TYPE_PCI_BUS;
    device_class_set_props(k, pci_props);
}

另外一处在 VirtioPCIClassclass_init

static void virtio_pci_class_init(ObjectClass *klass, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);
    PCIDeviceClass *k = PCI_DEVICE_CLASS(klass);
    VirtioPCIClass *vpciklass = VIRTIO_PCI_CLASS(klass);

    device_class_set_props(dc, virtio_pci_properties);
    k->realize = virtio_pci_realize;
    k->exit = virtio_pci_exit;
    k->vendor_id = PCI_VENDOR_ID_REDHAT_QUMRANET;
    k->revision = VIRTIO_PCI_ABI_VERSION;
    k->class_id = PCI_CLASS_OTHERS;
    device_class_set_parent_realize(dc, virtio_pci_dc_realize,
                                    &vpciklass->parent_dc_realize); //
    dc->reset = virtio_pci_reset;
}

device_class_set_parent_realize 的核心逻辑是将dc 的realize赋值为 virtio_pci_dc_realize ,然后将vpciklass->parent_dc_realize 设置为dc的初始值即pci_qdev_realize

static void virtio_pci_dc_realize(DeviceState *qdev, Error **errp)
{
    VirtioPCIClass *vpciklass = VIRTIO_PCI_GET_CLASS(qdev);
    VirtIOPCIProxy *proxy = VIRTIO_PCI(qdev);
    PCIDevice *pci_dev = &proxy->pci_dev;

    if (!(proxy->flags & VIRTIO_PCI_FLAG_DISABLE_PCIE) &&
        virtio_pci_modern(proxy)) {
        pci_dev->cap_present |= QEMU_PCI_CAP_EXPRESS;
    }

    vpciklass->parent_dc_realize(qdev, errp);
}

由于父类初始化在前子类初始化在后(子类覆盖了父类的实现),因此 virtio-net 实例化的调用链到目前为止如下所示

device_set_realized->virtio_pci_dc_realize->pci_qdev_realize

接着往下看pci_qdev_realize的核心实现

static void pci_qdev_realize(DeviceState *qdev, Error **errp)
{
    PCIDevice *pci_dev = (PCIDevice *)qdev;
    PCIDeviceClass *pc = PCI_DEVICE_GET_CLASS(pci_dev);
    ObjectClass *klass = OBJECT_CLASS(pc);

    ......

   if (pc->realize) {
        pc->realize(pci_dev, &local_err);
        if (local_err) {
            error_propagate(errp, local_err);
            do_pci_unregister_device(pci_dev);
            return;
        }
    }

    .......
}

上面的实现当中调用了 PCIDeviceClassrealize , 它的初始化是在 VirtioPCIClassclass_init 初始化的

k->realize = virtio_pci_realize;

接着看virtio_pci_realize 的具体实现

static void virtio_pci_realize(PCIDevice *pci_dev, Error **errp)
{
    VirtIOPCIProxy *proxy = VIRTIO_PCI(pci_dev);
    VirtioPCIClass *k = VIRTIO_PCI_GET_CLASS(pci_dev);
    Error *local_err = NULL;
    .......

    if (k->realize) {
        k->realize(proxy, &local_err);
        if (local_err) {
            goto out;
        }
    }

    .......
}

其调用的是VirtioPCIClassrealize 实现,而这个函数的赋值是在virtio_net_pci_class_init 里面完成的,其callback为virtio_net_pci_realize

static void virtio_net_pci_realize(VirtIOPCIProxy *vpci_dev, Error **errp)
{
    DeviceState *qdev = DEVICE(vpci_dev);
    VirtIONetPCI *dev = VIRTIO_NET_PCI(vpci_dev);
    DeviceState *vdev = DEVICE(&dev->vdev);
    PCIDevice *pci_dev = &vpci_dev->pci_dev;
    VirtIONet *net = &dev->vdev;

    ......

    object_property_set_bool(OBJECT(vdev), "realized", true, errp);

    .......
}

又看到这个比较熟悉的函数了object_property_set_bool, 先来看这个vdev也就是 VirtIONet 相关QOM结构。

  • type
------------------------------------------------------------------------>parent
TYPE_VIRTIO_NET->TYPE_VIRTIO_DEVICE->TYPE_DEVICE->TYPE_OBJECT
  • class
---------------------------------------------------------------->
VirtioDeviceClass->DeviceClass->ObjectClass
  • instance
---------------------------------------------------------->包含
VirtIONet->VirtIODevice->DeviceState->Object

object_property_set_bool 的具体实现当中会对上面所列举的class进行init,那instance是在哪里init的呢?如果你仔细的阅读过virtio-net-pci 相关初始化代码,你会发现它是在这里面完成的:

virtio_net_pci_instance_init->virtio_instance_init_common->object_initialize_child->object_initialize

理清virtio-net的class和instance的初始化之后,接着看realize 函数的调用。其入口还是在device_set_realized 函数,紧接着它会调用dc->realize 函数即virtio_device_realize ,然后会再调到VirtioDeviceClass的具体实现函数即vdc->realize具体函数为virtio_net_device_realize 。故整体调用链如下

device_set_realized->virtio_device_realize->virtio_net_device_realize

总结

qemu QOM的虽然精妙,但是理解和分析起来确实不太容易。建议大家仔细品一下上面的分析。同时,为了让大家更好的更好理解QOM,后续也会有更多这方面的文章分享给大家。


Published

Category

articles

Tags