QOM学习笔记

简介

本文从新手的角度来切入和学习qemu object model,由于是循序渐进的学习,很多地方也没有分析到,因此暂时不做最终结论和完整的总结,只是结合最近给i6300esb和virtio-balloon的开发和学习经验来分析。如果是开发经验丰富的人需要做完整汇总,请参考reference章节的文档1。注:此处‘新手’指的可以熟练的使用qemu,libvirt等软件,具有初步的相关开发经验。本文基于开源qemu-2.8.0。

1.设备注册

1.1.TypeInfo

切入点就从这里开始,所有的设备,总线等都有一个TypeInfo用来描述类型信息,先不需要深究这个细节,只需要知道,这是最原始的骨架,有了code1-1所示的代码,将i6300esb_register_types函数通过type_init宏进行调用,qemu就会认为,此外设已经插入到它应当插入的位置,并且拥有了name = TYPE_WATCHDOG_I6300ESB_DEVICE,这个插入的位置如何确定,现在暂时不管,此处留意一下TypeInfo中的parent属性即可,字面上可以看出这个设备是一个pci设备;TypeInfo结构体还有许多属性,此处大多数都没用到,所以不管。

/* hw/watchdog/wdt_i6300esb.c */
/* code 1-1 */
static const TypeInfo i6300esb_info = {
    .name          = TYPE_WATCHDOG_I6300ESB_DEVICE,
    .parent        = TYPE_PCI_DEVICE,
    .instance_size = sizeof(I6300State),
    .class_init    = i6300esb_class_init,
};

static void i6300esb_register_types(void)
{
    watchdog_add_model(&model);
    type_register_static(&i6300esb_info);
}

type_init(i6300esb_register_types)

1.2.type_init

再来看看type_init做了什么?这里就是一个宏,进行了字符串拼接,生成一个函数,此处为do_qemu_init_i6300esb_register_types,最后调用register_module_init函数,将i6300esb_register_types添加到一个全局list里面,这个list由find_type函数来提供。

//include/qemu/module.h
//code 1-2
#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);                                   \
}
//util/module.c
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;
    /* 这里的list是全局变量,一个type只有一份,此处的type是MODULE_INIT_QOM,和TypeInfo的type是两回事,此处的ModuleEntry */
    /* 才是对应了TypeInfo */
    l = find_type(type);
	/* 将新定义的module加入到该全局数组中 */
    QTAILQ_INSERT_TAIL(l, e, node);
}

static ModuleTypeList *find_type(module_init_type type)
{
    init_lists();

    return &init_type_list[type];
}

static void init_lists(void)
{
    static int inited;
    int i;

    if (inited) {
        return;
    }

    for (i = 0; i < MODULE_INIT_MAX; i++) {
        QTAILQ_INIT(&init_type_list[i]);
    }

    QTAILQ_INIT(&dso_init_list);

    inited = 1;
}

至此,register函数已经被添加到全局list中了,这些函数肯定会被调用,调用的时间为main函数开始后,
module_call_init(MODULE_INIT_QOM);中,,这里会在QTAILQ_FOREACH中遍历上面QTAILQ_INSERT_TAIL添加的ModuleEntry,其中包含了设备注册函数i6300esb_register_types,并且这里面就会调用type_register_static函数会把TypeInfo转为TypeImpl,然后加入到一个table中,过程如code 1-4所示

/* util/module.c */
//code 1-3
void module_call_init(module_init_type type)
{
    ModuleTypeList *l;
    ModuleEntry *e;

    l = find_type(type);

    QTAILQ_FOREACH(e, l, node) {
    /* 逐个调用设备注册函数,包括i6300esb_register_types */
        e->init();
    }
}
//code 1-4
static TypeImpl *type_register_internal(const TypeInfo *info)
{
    TypeImpl *ti;
    /*根据TypeInfo的信息生成TypeImpl,这两个结构体几乎一样,唯一不同的是,TypeImpl多了*/
    /*ObjectClass *class和TypeImpl *parent_type,具体作用第2章会提到*/
    ti = type_new(info);

    type_table_add(ti);
    return ti;
}

static void type_table_add(TypeImpl *ti)
{
    assert(!enumerating_types);
    /*获取全局hash表,并插入新的type和name*/
    g_hash_table_insert(type_table_get(), (void *)ti->name, ti);
}

1.3.测试

对上一节拼接成的do_qemu_init_i6300esb_register_types函数打断点,启动qemu,由于这个函数有__attribute__((constructor))属性,因此会在main函数执行之前就执行,所有的外设都进行这样的操作,这样在main函数执行的时候,就可以遍历这个list,对注册的外设进行初始化了。

Breakpoint 1, register_module_init (fn=0x5555557a6ebc <qmp_init_marshal>, type=MODULE_INIT_QAPI) at util/module.c:68
68	    e = g_malloc0(sizeof(*e));
(gdb) del 1
(gdb) c
Continuing.

Breakpoint 2, do_qemu_init_i6300esb_register_types () at hw/watchdog/wdt_i6300esb.c:465
465	type_init(i6300esb_register_types)
(gdb) bt
#0  0x0000555555ae383a in do_qemu_init_i6300esb_register_types () at hw/watchdog/wdt_i6300esb.c:465
#1  0x0000555555c78e4d in __libc_csu_init ()
#2  0x00007fffdc6b2365 in __libc_start_main (main=0x5555558f5760 <main>, argc=57, argv=0x7fffffffdce8, init=0x555555c78e00 <__libc_csu_init>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffdcd8)
    at ../csu/libc-start.c:225
#3  0x0000555555766fb9 in _start () 

virtio_balloon_info也是用相同的方式注册进qemu的全局list,但是和i6300esb有一些不同,i6300esb_register_types在代码中只出现在本章节提到的地方,而virtio_register_types会出现很多次,很多使用了virtio的外设都会用同样的关键字,还不知道这样有没有什么特别的意义,或者对后面初始化时候的代码结构有什么影响,先暂时不管,不影响此处理解。后面会同时使用i6300esb和virtio-balloon进行对比分析。

2.初始化

2.1.type初始化

上一章介绍了TypeImpl是由一个TypeInfo转化而来的,TypeInfo主要是开发者自定义新类型的时候填充的信息,可以覆盖父类型的对应的内容,而TypeImpl就是具体完成type的整套继承和多态机制的载体。本节用virtio-balloon-device来举例说明这个流程。这里对virtio_balloon_info进行初始化分析,会调用type_initialize函数,可以在gdb中设置条件断点b type_initialize if strcmp(ti->name,“virtio-balloon-device”)==0

/* code 2-1*/
/* qom/object.c*/
static void type_initialize(TypeImpl *ti)
{
    TypeImpl *parent;
    /*这里就是TypeImpl多出TypeInfo的内容之一,代表了自身所属的class,如果已经存在,就说明*/
    /*该类型已经初始化完毕 ,可以直接返回*/
    if (ti->class) {
        return;
    }
    /*获取相关信息,如果获取不到,就嵌套逐级从父类去获取*/
    ti->class_size = type_class_get_size(ti);
    ti->instance_size = type_object_get_size(ti);
    /*给当前TypeImpl实例分配内存空间*/
    ti->class = g_malloc0(ti->class_size);
    /*找到父类型,如果存在父类型,则初始化父类型*/
    parent = type_get_parent(ti);
    if (parent) {
        /*初始化parent类型 */
        type_initialize(parent);
        GSList *e;
        int i;
        /*parent类型肯定要比当前类型的size要小,否则无法实现继承*/
        g_assert_cmpint(parent->class_size, <=, ti->class_size);
        /*把父类型的内容拷贝到当前类型,这样就实现了继承*/
        memcpy(ti->class, parent->class, parent->class_size);
        /*interface是实现多继承的方式,此处没涉及到,暂时不分析*/
        ti->class->interfaces = NULL;
        /* properties并不会继承,在这里重新指向了新申请的空间,把上面继承的给覆盖了*/
        ti->class->properties = g_hash_table_new_full(
            g_str_hash, g_str_equal, g_free, object_property_free);
        /*这里没有多继承,不涉及interface*/
        for (e = parent->class->interfaces; e; e = e->next) {
            InterfaceClass *iface = e->data;
            ObjectClass *klass = OBJECT_CLASS(iface);

            type_initialize_interface(ti, iface->interface_type, klass->type);
        }
        /*这里没有多继承,没有进行interface的代码调试和验证,只有单纯阅读代码以及类比java */
        /*用interface实现多继承的方法进行分析,java中一个类可以实现多个无关的接口,接口也 */
        /* 可以继承另一个接口。*/
        /*第一个for循环,就对应了一个类可以实现多个无关的接口,先不管‘无关’*/
        for (i = 0; i < ti->num_interfaces; i++) {
  	  	    /*一个interface就对应了一个type*/
            TypeImpl *t = type_get_by_name(ti->interfaces[i].typename);
            /*这个循环就是把ti的interface数组遍历一遍,看当前的interface是不是其他 */
            /*interface的父类型,只要找到了一个是的,就跳出内侧循环 */
            for (e = ti->class->interfaces; e; e = e->next) {
                TypeImpl *target_type = OBJECT_CLASS(e->data)->type;

                if (type_is_ancestor(target_type, t)) {
                    break;
                }
            }
/*然后判断这个子类型是否被初始化了,如果初始化了,说明本父类interface的type肯定已经初始化过了,*/
/*不需要再初始化了,实际上,只要找到了子类型,e肯定不为空,只有当循环结束的时候e才为空,说明*/
/*找不到与之关联的类型,因此肯定要初始化*/
            if (e) {
                continue;
            }
	/*初始化该interface,这内部的type是组合型的type,由本type和本type的interface拼接而成,*/
	/*然后加入到 ti->class->interfaces中*/
            type_initialize_interface(ti, t, t);
        }
    } else {
    /*没有parent,例如object类型,直接申请一块空间作为properties*/
    /*properties是用来存放实例的各种定制化特性的,这部分并不会继承*/
        ti->class->properties = g_hash_table_new_full(
            g_str_hash, g_str_equal, g_free, object_property_free);
    }
    /*此ti的实际类型就是自己*/
    ti->class->type = ti;
    /*逐级初始化父类的class_base_init函数,如果存在的话,此处没有,无视*/
    while (parent) {
        if (parent->class_base_init) {
            parent->class_base_init(ti->class, ti->class_data);
        }
        parent = type_get_parent(parent);
    }
    /*初始化当前类型的class_init,此处是virtio_balloon_class_init,下面看看class_init函数做了什么*/
    if (ti->class_init) {
        ti->class_init(ti->class, ti->class_data);
    }
}

/*
* hw/virtio/virtio-balloon.c
*/
static void virtio_balloon_class_init(ObjectClass *klass, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);
    VirtioDeviceClass *vdc = VIRTIO_DEVICE_CLASS(klass);

    dc->props = virtio_balloon_properties;
    dc->vmsd = &vmstate_virtio_balloon;
    set_bit(DEVICE_CATEGORY_MISC, dc->categories);
    vdc->realize = virtio_balloon_device_realize;
    vdc->unrealize = virtio_balloon_device_unrealize;
    vdc->reset = virtio_balloon_device_reset;
    vdc->get_config = virtio_balloon_get_config;
    vdc->set_config = virtio_balloon_set_config;
    vdc->get_features = virtio_balloon_get_features;
    vdc->set_status = virtio_balloon_set_status;
    vdc->vmsd = &vmstate_virtio_balloon_device;
}
/*可以看到,class的初始化,是整个TypeImpl区别于TypeInfo最关键的地方,TypeInfo只提供了基本的描述信息,而真正承载对象的实例的初始化*/
/*都在ObjectClass里面*/

通过调试信息可以看到,TypeImpl的继承顺序为"virtio-balloon-device"->“virtio-device”->“device”->“object”,因此到了object的时候,就没有parent了,并且以后再遇到其他的type需要初始化,并且parent的继承遇到了已经初始化过的type,都不会再进行初始化,说明一个type在内存中只有一份ObjectClass内存拷贝,也就是一个实例,整个type类型是以object类型为根的。代码中的properties的作用是可以给开发者一个机制来添加自定义的成员变量,并且可以对这些变量自行设置get和set函数,这样就不会和父类产生冲突。提问:如果不区分TypeInfo和TypeImpl,只用TypeImpl,会发生什么???

2.2.添加外设

2.2.1.qdev_device_add

上一章介绍的type的初始化,本章就开始介绍具体dev的初始化了,2.2.1节主要是介绍一下添加外设的全貌,之后的章节,再对细节进行分析

//vl.c
//code 2-2
if (qemu_opts_foreach(qemu_find_opts("device"),
                          device_init_func, NULL, NULL)) {
        exit(1);
    }

main函数中,解析qemu启动参数的时候,根据-device选项,会循环调用device_init_func函数,这里面会调用qdev_device_add函数对每一个device进行添加,此处qemu启动的相关参数为-device virtio-balloon-pci,id=balloon0,bus=pci.0,addr=0x5, 下面删除一些非核心代码,对理解没有什么影响。

/* 
qdev-monitor.c
code 2-3
*/
DeviceState *qdev_device_add(QemuOpts *opts, Error **errp)
{
    DeviceClass *dc;
    const char *driver, *path;
    DeviceState *dev;
    BusState *bus = NULL;
    Error *err = NULL;
    /* 根据qemu参数取出,就是一个字符串"virtio-balloon-pci"*/
    driver = qemu_opt_get(opts, "driver");
    if (!driver) {
        error_setg(errp, QERR_MISSING_PARAMETER, "driver");
        return NULL;
    }

    /* 根据上面的字符串名,找到DeviceClass,这个主要是用来下面对比总线类型有没有问题的 */
    dc = qdev_get_device_class(&driver, errp);
    if (!dc) {
        return NULL;
    }

    /* 解析总线号,找到总线,其实总线BusState也就是一个Object做了扩展 */
    path = qemu_opt_get(opts, "bus");
    if (path != NULL) {
        bus = qbus_find(path, errp);
        if (!bus) {
            return NULL;
        }
        /*这里会判断总线类型是否能做转换,如果可以转换,则不做任何操作,只有涉及多继承和interface相关的时候才会转换,此处没有
        涉及,就不深究了*/
        if (!object_dynamic_cast(OBJECT(bus), dc->bus_type)) {
            error_setg(errp, "Device '%s' can't go on %s bus",
                       driver, object_get_typename(OBJECT(bus)));
            return NULL;
        }
     ...  

    /* 根据"virtio-balloon-pci",创建一个device,其实是先创建一个Object类型的变量,再类型转为DeviceState,此处是关键*/
    dev = DEVICE(object_new(driver));
    /* 如果bus初始化成功,就把设备挂在bus上面,所谓设置继承关系,就是child和parent的特定指针指向对方,并且实现一些property的访问机制,核心都是走的object_property_add函数的倒数第二个参数opaque指针 */
    if (bus) {
        qdev_set_parent_bus(dev, bus);
    }

    qdev_set_id(dev, qemu_opts_id(opts));

   ...

    dev->opts = opts;
    /* 这里也是关键的一步,这个函数调用之后,dev会逐级初始化,true代表调用设备的realize实现,如果是false,则将已有的实现给剔除,目前还没有对false的情况进行分析*/
    object_property_set_bool(OBJECT(dev), true, "realized", &err);
    if (err != NULL) {
        error_propagate(errp, err);
        dev->opts = NULL;
        object_unparent(OBJECT(dev));
        object_unref(OBJECT(dev));
        return NULL;
    }
    return dev;
}

2.2.2.qdev_get_device_class

/* 
qdev-monitor.c
code 2-4
*/
static DeviceClass *qdev_get_device_class(const char **driver, Error **errp)
{
    ObjectClass *oc;
    DeviceClass *dc;
    const char *original_name = *driver;

    oc = object_class_by_name(*driver);
    ...
    dc = DEVICE_CLASS(oc);
  

    return dc;
}

核心函数是object_class_by_name,根据dirver的名字,查找到该名字对应的ObjectClass,因为任何对象,都有一个class

/*qom/object.c
code 2-5
*/
ObjectClass *object_class_by_name(const char *typename)
{
    /*在前面1.2节code 1-4的地方添加的type,在这里查找出来*/
    TypeImpl *type = type_get_by_name(typename);

    if (!type) {
        return NULL;
    }
    /* 调用初始化函数对type进行初始化,这里就是个保险的作用,这里由于前面已经初始化过了,*/
    /*所以里面没有做什么直接返回了 */
    type_initialize(type);

    return type->class;
}

2.2.3.object_new

dev = DEVICE(object_new(driver));
	|--object_new
	    |--TypeImpl *ti = type_get_by_name(typename);
	    |--object_new_with_type(ti);

这里是初始化设备最关键的地方,DEVICE宏是一个简单的宏,里面会对类型做校验。提问:DEVICE凭什么知道object_new出来的ojb是一个device,并且具有DeviceState类型的空间大小???

/*
code 2-6
*/
Object *object_new_with_type(Type type)
{
    Object *obj;

    g_assert(type != NULL);
    /*初始化type,此处已经初始化了,应该是为了保险*/
    type_initialize(type);
    /*分配Object所需要的空间*/
    obj = g_malloc(type->instance_size);
    /*顺着该函数往下走*/
    object_initialize_with_type(obj, type->instance_size, type);
    obj->free = g_free;

    return obj;
}

核心就在这个流程,object_init_with_type会顺着parent的type递归调用,这样obj实例对象就会一级级被初始化,层次越浅,被初始化的内容越多,因为最深层次的初始化,肯定是最简单的Object类型,obj总共只有一份内存拷贝。而obj的size,在最外层的时候就已经分配了,如code 2-6所示,源头也是来自TypeInfo的定义,并且是跟DeviceState类型在struct首字节对齐的,因此可以任意拓展。

object_initialize_with_type
    |--object_init_with_type(obj, type);
        |--ti->instance_init(obj);=>virtio_balloon_pci_instance_init
            |--virtio_instance_init_common(obj, &dev->vdev, sizeof(dev->vdev),TYPE_VIRTIO_BALLOON);
            |--object_property_add_alias(obj, "guest-stats", OBJECT(&dev->vdev), "guest-stats", &error_abort);
            |--object_property_add_alias(obj, "guest-stats-polling-interval",
                              OBJECT(&dev->vdev),
                              "guest-stats-polling-interval", &error_abort);

这样就进入了virtio_balloon_pci_instance_init函数,这里面会涉及到两个函数,一个是virtio_instance_init_common,一个是object_property_add_alias,前一个是最关键的,第二个参数dev->vdev传入virtio_instance_init_common函数之后,在内部再次调用初始化函数,最终会调用到virtio_balloon_instance_init,这样"virtio-balloon-device"就初始化完毕了,然后调用object_property_add_child(proxy_obj, “virtio-backend”, OBJECT(vdev), NULL);让"virtio-balloon-device"和"virtio-balloon-pci"关联起来,整个balloon的基本结构就初始化完成了,后面还要进行一些初始化,和bus关联起来,暂时不管细节了,基本上都是各种object_property_add,然后组合到数第二个参数opaque,传入不同的参数,与此同时,set和get函数不同。本文不是深究virtio-balloon设备的功能,因此具体这些函数做的操作有什么意义没有过多的分析,主要是通过走一遍流程,展现qemu的面向对象机制是如何实现初始化和继承。

3.Qemu Visitor

3.1.整体介绍

由于qemu实现了一套面向对象机制,所有的数据都是Object,那么对数据的访问,也同样需要一套抽象化的接口,这里就拿qmp接口访问虚拟机内存信息2来举例,直观的分析qemu visitor的含义

[root@localhost libvirt-python-3.2.0]# virsh dommemstat 3
actual 2097152
last_update 0
rss 55676

此处由于没有开启内存气泡功能,因此返回的信息不完整,但是不影响流程和接口分析。流程分析还是按照完整的信息来分析。
首先,该命令会在libvirt中生成一段qmp请求,这段请求会发给qemu monitor的处理程序

{\"execute\":\"qom-get\",\"arguments\":{\"path\":\"//machine/i440fx/pci.0/child[9]\",\"property\":\"guest-stats\"},\"id\":\"libvirt-31\"}

qemu mainloop中收到了这个请求,就进行处理,会调用函数

/*
* code 3-1
*/
void qmp_marshal_qom_get(QDict *args, QObject **ret, Error **errp)
{
    Error *err = NULL;
    QObject *retval;
    Visitor *v;
    q_obj_qom_get_arg arg = {0};

    v = qobject_input_visitor_new(QOBJECT(args), true);
    visit_start_struct(v, NULL, NULL, 0, &err);
    if (err) {
        goto out;
    }
    visit_type_q_obj_qom_get_arg_members(v, &arg, &err);
    if (!err) {
        visit_check_struct(v, &err);
    }
    visit_end_struct(v, NULL);
    if (err) {
        goto out;
    }

    retval = qmp_qom_get(arg.path, arg.property, &err);
    if (err) {
        goto out;
    }

    qmp_marshal_output_any(retval, ret, &err);

out:
    error_propagate(errp, err);
    visit_free(v);
    v = qapi_dealloc_visitor_new();
    visit_start_struct(v, NULL, NULL, 0, NULL);
    visit_type_q_obj_qom_get_arg_members(v, &arg, NULL);
    visit_end_struct(v, NULL);
    visit_free(v);
}

代码较多,实际上此处只用看两个函数,是起关键作用的,qmp_qom_get和qmp_marshal_output_any,前者是实际读取数据,并且返回给retval变量,后者是将retval变量作为输入,进行处理后返回给ret变量,ret变量作为输出,被调用函数返回,最终在handle_qmp_command中通过monitor_json_emitter(mon, rsp)将结果返回。其他的有visitor关键字的函数就是实现了一个统一的接口,能让retval转化为ret,在此处,retval和ret从代码看都是QObject,效果不明显,但是更底层的代码可能有int,float,char之类的类型,或者自定义类型,这样就明显了,因此本文不分析这些地方,而是直接分析最底层的函数,这样可以展现qemu visitor比较完整的用法。

3.2.获取数据

3.2.1.主体函数

/*
* code 3-2
* qmp.c:270
*/
QObject *qmp_qom_get(const char *path, const char *property, Error **errp)
{
    Object *obj;
	/*
	* property代表此处要取得的内容,此处是"guest-stats"
	*/
    obj = object_resolve_path(path, NULL);
    /*
     * 这里path传入"//machine/i440fx/pci.0/child[9]",是qemu管理设备的一种编址方式
     * 此处不需要深究,这里代表了我们实验的virtio-balloon设备,返回设备的结构体obj
     * */
    if (!obj) {
        error_set(errp, ERROR_CLASS_DEVICE_NOT_FOUND,
                  "Device '%s' not found", path);
        return NULL;
    }

    return object_property_get_qobject(obj, property, errp);
}

从代码中可以看到,property="guest-stats"是会逐级往更深层的函数进行传参的,并且中途的代码逻辑由于面向对象的逐级封装,比较抽象,因此全部略过,想理解为什么会套这么多层,需要结合前两章节来阅读,一句话解释就是QOM模型对设备抽象了很多层,到最内层才是具体的设备。因此直接根据调用关系,走到最内层函数:

/*
* code 3-3
* hw/virtio/virtio-balloon.c
*/
static void balloon_stats_get_all(Object *obj, Visitor *v, const char *name,
                                  void *opaque, Error **errp)
{
    Error *err = NULL;
    VirtIOBalloon *s = opaque;
    int i;
    /*
    * 先分析一下传参,name还是之前上面传下来的"guest-stats"
    * Visitor的类型是上面传下来的,已经知道是QObjectOutputVisitor类型,这个类型定义了一个对
    * QObject数据类型访问的统一输出接口,里面有个result指针,获取的内容都是放在这个指针里面
    * 返回的,同理,其他地方的StringOutputVisitor也是对string类型的数据访问做了通用接口
    */
    visit_start_struct(v, name, NULL, 0, &err);
    if (err) {
        goto out;
    }
    visit_type_int(v, "last-update", &s->stats_last_update, &err);
    if (err) {
        goto out_end;
    }

    visit_start_struct(v, "stats", NULL, 0, &err);
    if (err) {
        goto out_end;
    }
    for (i = 0; i < VIRTIO_BALLOON_S_NR; i++) {
        visit_type_uint64(v, balloon_stat_names[i], &s->stats[i], &err);
        if (err) {
            goto out_nested;
        }
    }
    visit_check_struct(v, &err);
out_nested:
    visit_end_struct(v, NULL);

    if (!err) {
        visit_check_struct(v, &err);
    }
out_end:
    visit_end_struct(v, NULL);
out:
    error_propagate(errp, err);
}

从代码中可以看到,有一个visit_start_struct函数,就有一个visit_end_struct与之对应,并且是层层嵌套的,这样就让人联想到栈结构,事实上这里确实用到了栈。

3.2.2.初遇visit_start_struct函数

visit_start_struct的核心是v->start_struct(v, name, obj, size, &err);此处是qobject_output_start_struct

/*
* code 3-4
* qapi/qobject-output-visitor.c
*/
static void qobject_output_start_struct(Visitor *v, const char *name,
                                        void **obj, size_t unused, Error **errp)
{
	/* 把visitor做类型转换,转换成QObjectOutputVisitor类型 */
    QObjectOutputVisitor *qov = to_qov(v);
    /* 创建一个新的QDict类型变量,这个是给入栈用的,这里一个visitor就维护了一个栈结构,每次 */
    /* start,就会创建一个新的dict,当作存放后续内容的容器,因此一开始是空的 */
    QDict *dict = qdict_new();
	/* 该函数是把即将读取的数据结构的地址,放入当前的栈顶元素,如果之前就存在,则刷新相关信息 */
    qobject_output_add(qov, name, dict);
    /*  */
    qobject_output_push(qov, dict, obj);
}

#define qobject_output_add(qov, name, value) \
    qobject_output_add_obj(qov, name, QOBJECT(value))
#define qobject_output_push(qov, value, qapi) \
    qobject_output_push_obj(qov, QOBJECT(value), qapi)

下面详细分析add和push函数

  • qobject_output_add_obj
/*
* code 3-5
* qapi/qobject-output-visitor.c
*/
static void qobject_output_add_obj(QObjectOutputVisitor *qov, const char *name,
                                   QObject *value)
{
    /* 获取当前visitor的栈顶元素 */
    QStackEntry *e = QSLIST_FIRST(&qov->stack);
    /* 取出栈顶元素的value,是个指针 */
    QObject *cur = e ? e->value : NULL;
	/* 如果cur为空,说明是空栈,则将root指针设置为value,这里要标记一个root的原因是因为,后续 */
	/* 的数据都是一层层嵌套的的,只要找到了最外层的源头,就可以一层层访问内部的数据,但是这个源 */
	/* 头是栈底,不符合栈的访问方式,无法一次性访问到,因此使用空间换时间的方式,做了一个记录 */
    if (!cur) {
        /* Don't allow reuse of visitor on more than one root */
        assert(!qov->root);
        qov->root = value;
    } else {
    	/* 这一次其实不走这里,因为第一次cur为空,第二次才走这里,见后面的小节 */
        switch (qobject_type(cur)) {
        /* 根据栈顶的value的类型来选择处理方式,此处是QTYPE_QDICT */
        case QTYPE_QDICT:
            assert(name);
            /* cur其实是个QDict类型的指针,该函数是将value,加入到这个QDict中,关键字为name */
            /* 此处的name就是外面传进来的"guest-stats",如果之前cur中存在这个键值对了,就 */
            /* 刷新内容,否则就添加进去,这里就是把一个QDict放入一个QDict,嵌套了一层 */
            qdict_put_obj(qobject_to_qdict(cur), name, value);
            break;
        case QTYPE_QLIST:
            assert(!name);
            qlist_append_obj(qobject_to_qlist(cur), value);
            break;
        default:
            g_assert_not_reached();
        }
    }
}

  • qobject_output_push_obj
/*
* code 3-6
* qapi/qobject-output-visitor.c
*/
static void qobject_output_push_obj(QObjectOutputVisitor *qov, QObject *value,
                                    void *qapi)
{
	/* 申请一个新的栈元素 */
    QStackEntry *e = g_malloc0(sizeof(*e));

    assert(qov->root);
    assert(value);
    e->value = value;
    e->qapi = qapi;
    /* 这里就是简单将要访问的value入栈 */
    QSLIST_INSERT_HEAD(&qov->stack, e, node);
}

可以看到,整个start函数做的事情就是用value创建一个新的QStackEntry类型的栈元素,在入栈之前先以键值对“name:value”的形式加入到旧栈顶的QDict中,然后将QStackEntry入栈,成为新的栈顶。

3.2.3.visit_type_int函数

可以看到,start函数结束后,调用了visit_type_int(v, “last-update”, &s->stats_last_update, &err);
此处的&s->stats_last_update是真正附带了数据的,来源是定时采样,和此处无关,此处只管读取它的内容:

/*
* code 3-7
*/
void visit_type_int(Visitor *v, const char *name, int64_t *obj, Error **errp)
{
    assert(obj);
    trace_visit_type_int(v, name, obj);
    v->type_int64(v, name, obj, errp);
}
/* 真正调用的是这个函数 */
static void qobject_output_type_int64(Visitor *v, const char *name,
                                      int64_t *obj, Error **errp)
{
	/* name是"last-update",obj是&s->stats_last_update,为待读取的内容 */
    QObjectOutputVisitor *qov = to_qov(v);
    /* 这里可以看到,和start函数不同的是,只需要把键值对"last-update:&s->stats_last_update"加入到 */
    /* 栈顶的QDict中,并不需要重新入栈 */
    qobject_output_add(qov, name, qint_from_int(*obj));
}

3.2.4.再遇visit_start_struct函数

之后又继续调用了visit_start_struct(v, “stats”, NULL, 0, &err);函数,详细解释见code 3-5的注释,新创建的QDict对象会以键值"stats"的形式加入到上一次的栈顶,并且自身再入栈。之后,在一个for循环中,会反复调用visit_type_uint64(v, balloon_stat_names[i], &s->stats[i], &err);
都不需要入栈,只需要加入到"stats"栈顶的QDict中。

3.2.5.visit_end_struct

/*
* code 3-8
*/
void visit_end_struct(Visitor *v, void **obj)
{
    trace_visit_end_struct(v, obj);
    v->end_struct(v, obj);
}
/* 实际调用函数,每次end都是一次出栈操作,并且传参obj并不重要,没有作用,因为最外层传的是NULL */
static void qobject_output_end_struct(Visitor *v, void **obj)
{
    QObjectOutputVisitor *qov = to_qov(v);
    QObject *value = qobject_output_pop(qov, obj);
    assert(qobject_type(value) == QTYPE_QDICT);
}

/* Pop a value off the stack of QObjects being built, and return it. */

static QObject *qobject_output_pop(QObjectOutputVisitor *qov, void *qapi)
{
    QStackEntry *e = QSLIST_FIRST(&qov->stack);
    QObject *value;

    assert(e);
    assert(e->qapi == qapi);
    QSLIST_REMOVE_HEAD(&qov->stack, node);
    value = e->value;
    assert(value);
    g_free(e);
    return value;
}

从代码中可以看出,出栈仅仅是把栈给清空了,然后做了一个简单的校验,并没有记录或者返回实质数据,那么真正的数据在哪?那就是qobject_output_add_obj提到的qov->root,清空栈的操作虽然清除掉栈元素的内存,但是栈元素里面存的是数据的地址,并不是数据本身,而数据的最上层QDict一开始就存到qov->root中了,因此数据还在,而且是完整的数据。

3.2.6.结论

qemu visitor的设计技巧性很强,每次start,都会取出visitor的栈顶元素,然后把新建立的QDict加入进去(先判断是否存在,例如guest-stats,存在就更新value值,只要visitor重新start,就要更新),然后后面在调用push的时候,新建一个栈元素,把这个QDict入栈,
相当于每次start,栈的层级就会变深,然后层次越深的栈元素,记载的内容更多,最近的一次start后面所有的visit,都是在这一层的栈顶元素中进行。因为其他的visit函数并没有push这个过程,只有push了,栈才会变深。至于什么时候需要start函数,从上面3.2.1-3.2.4可以看出,凡是访问复杂类型以及同类型多个数量的数据的时候,调用一次start函数,该函数会使用一个QDict来存放这些数据,只要是需要一个新的QDict在栈顶,并且在新栈顶入栈之前,这个新的QDict还添加到了旧的栈顶的QDict里面,此处代码中keyword名字是“guest-stats”,所以每一个更内层的栈元素,其实是包含了外层的栈元素的信息的(像俄罗斯套娃一样),而且要注意一点,这里存的全部都是地址,因此,在栈顶修改的内容,也会体现到栈底元素。全部处理完毕之后,把栈中内容都出栈,而且销毁掉,但是销毁的是保存地址的指针,并不是地址本身的内容,所以内容其实还在,只需要最root的那个就行了,因为root是套娃,所有信息都在里面。这种多层次结构就保证了任何类型的数据都可以通过这个逻辑来封装,最后返回给上层。

Q&A

问题1:如果不区分TypeInfo和TypeImpl,只用TypeImpl,会发生什么?

回答:暂时不知道。

问题2:2.2.3节中,DEVICE凭什么知道object_new出来的ojb是一个device,并且具有DeviceState类型的空间大小?

回答:见2.2.3节,code2-6分析。

问题3:qemu visitor能否不用栈去实现组装数据的逻辑?

回答:目前看来是不行的。分析如下:首先,这套逻辑拼接出来的数据是树型结构,因此,问题转化为此处能否不用栈实现树型结构的数据拼接,个人认为是很困难的。第一种情况:如果用队列,不具备后入先出的逻辑,如果有树0>1,1->2,1->3,树根0先进队列,随后1进入队列,QDict1可以加入QDict0,并且成为队列最新的QDict,此时QDict2也加入队列,并且成为QDict1的一部分,到此都没问题,直到3需要加入队列,3理应加入QDict1中,但是现在队列节点的顺序是QDict0,QDict1,QDict3,2没有任何办法和QDict1产生联系,除非将QDict0出队列,这样就没办法使用树根节点记录所有数据了,因为树根都出队列抛弃了。第二种情况:不使用队列或者栈之类的数据结构,纯粹人工按顺序添加,这样任何非基本类型的节点,例如0和1,都得人工维护一个QDict变量,当数据非常复杂的时候,代码可读性会非常差,有很多显式定义的变量。

Reference


  1. QEMU(1) - QOM ↩︎

  2. qemu添加hmp和qmp接口 ↩︎

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

养乌龟的hx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值