QEMU源码全解析42 —— Machine(12)

接前一篇文章:QEMU源码全解析41 —— Machine(11)

本文内容参考:

《趣谈Linux操作系统》 —— 刘超,极客时间

QEMU/KVM》源码解析与应用 —— 李强,机械工业出版社

特此致谢!

上一回针对于“Machine”系列第3~10篇文章开始梳理其脉络,梳理了type_init()这一条线,本文讲梳理type_register()这一条线,并对整个流程进行总结。

先再次贴出

DEFINE_I440FX_MACHINE(v8_1, "pc-i440fx-8.1", NULL,
                      pc_i440fx_8_1_machine_options);

的相关代码:

static void pc_init_v8_1(MachineState *machine)
{
        void (*compat)(MachineState *m) = (NULL);
        if (compat) {
            compat(machine);
        }
        pc_init1(machine, TYPE_I440FX_PCI_HOST_BRIDGE, \
                 TYPE_I440FX_PCI_DEVICE);
}
 
static void pc_machine_v8_1_class_init(ObjectClass *oc, void *data)
{
        MachineClass *mc = MACHINE_CLASS(oc);
        pc_i440fx_8_1_machine_options(mc);
        mc->init = pc_init_v8_1;
}
static const TypeInfo pc_machine_type_v8_1 = {
        .name       = "pc-i440fx-8.1" TYPE_MACHINE_SUFFIX,
        .parent     = TYPE_PC_MACHINE,
        .class_init = pc_machine_v8_1_class_init,
};
static void pc_machine_init_v8_1(void)
{
        type_register(&pc_machine_type_v8_1);
}
type_init(pc_machine_init_v8_1)

type_register函数在qom/object.c中,代码如下:

TypeImpl *type_register(const TypeInfo *info)
{
    assert(info->parent);
    return type_register_internal(info);
}

type_register_internal函数就在上边,代码如下:

static TypeImpl *type_register_internal(const TypeInfo *info)
{
    TypeImpl *ti;
    ti = type_new(info);

    type_table_add(ti);
    return ti;
}

 type_new函数也在qom/object.c中,代码如下:

static TypeImpl *type_new(const TypeInfo *info)
{
    TypeImpl *ti = g_malloc0(sizeof(*ti));
    int i;

    g_assert(info->name != NULL);

    if (type_table_lookup(info->name) != NULL) {
        fprintf(stderr, "Registering `%s' which already exists\n", info->name);
        abort();
    }

    ti->name = g_strdup(info->name);
    ti->parent = g_strdup(info->parent);

    ti->class_size = info->class_size;
    ti->instance_size = info->instance_size;
    ti->instance_align = info->instance_align;

    ti->class_init = info->class_init;
    ti->class_base_init = info->class_base_init;
    ti->class_data = info->class_data;

    ti->instance_init = info->instance_init;
    ti->instance_post_init = info->instance_post_init;
    ti->instance_finalize = info->instance_finalize;

    ti->abstract = info->abstract;

    for (i = 0; info->interfaces && info->interfaces[i].type; i++) {
        ti->interfaces[i].typename = g_strdup(info->interfaces[i].type);
    }
    ti->num_interfaces = i;

    return ti;
}

type_table_add函数也在qom/object.c中,代码如下:

static void type_table_add(TypeImpl *ti)
{
    assert(!enumerating_types);
    g_hash_table_insert(type_table_get(), (void *)ti->name, ti);
}

每一个Module既然要模拟某种设备,那么就应该定义一种类型TypeImpl来表示这个设备。这其实是一种面向对象编程的思路,只不过这里用的是纯C语言的实现,因此需要变相实现一下类和对象。

static void pc_machine_init_v8_1(void)
{
        type_register(&pc_machine_type_v8_1);
}
type_init(pc_machine_init_v8_1)

pc_machine_init_v8_1函数会注册pc_machine_type_v8_1,可以认为这样就动态定义了一个类。

static const TypeInfo pc_machine_type_v8_1 = {
        .name       = "pc-i440fx-8.1" TYPE_MACHINE_SUFFIX,
        .parent     = TYPE_PC_MACHINE,
        .class_init = pc_machine_v8_1_class_init,
};

这个类的名字是“"pc-i440fx-8.1" TYPE_MACHINE_SUFFIX”,即"pc-i440fx-8.1-machine";这个类有父类TYPE_PC_MACHINE(即"generic-pc-machine");这个类的初始化应该调用函数pc_machine_v8_1_class_init。

这里的调用链为:

pc_machine_init_v8_1

        type_register

                type_register_internal

static void pc_machine_init_v8_1(void)
{
        type_register(&pc_machine_type_v8_1);
}
​static TypeImpl *type_register_internal(const TypeInfo *info)
{
    TypeImpl *ti;
    ti = type_new(info);

    type_table_add(ti);
    return ti;
}

在type_register_internal函数中,会根据pc_machine_type_v8_1这个TypeInfo ,创建一个TypeImpl来表示这个新注册的类,也就是说,TypeImpl才是真正想要声明的那个class。分别来看一下TypeInfo结构和TypeImpl结构的定义:

struct TypeInfo的定义在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;
};

struct TypeImpl的定义在qom/object.c中,如下:

struct TypeImpl
{
    const char *name;

    size_t class_size;

    size_t instance_size;
    size_t instance_align;

    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];
};

由定义可见,TypeInfo结构和TypeImpl结构还是很相近的。

在QEMU中,有一个全局的哈希表type_table,用来存放所有定义的类。在type_new函数中,先从type_table表中根据名字查找某个类(此处是pc_machine_type_v8_1)。如果找到,说明这个类已经被注册过了,报错并终止当前进程;如果没有找到,则说明这是一个新的类,那么就将TypeInfo里边的信息填到TypeImpl中。

static TypeImpl *type_new(const TypeInfo *info)
{
    TypeImpl *ti = g_malloc0(sizeof(*ti));
    int i;

    g_assert(info->name != NULL);

    if (type_table_lookup(info->name) != NULL) {
        fprintf(stderr, "Registering `%s' which already exists\n", info->name);
        abort();
    }

    ti->name = g_strdup(info->name);
    ti->parent = g_strdup(info->parent);

    ti->class_size = info->class_size;
    ti->instance_size = info->instance_size;
    ti->instance_align = info->instance_align;

    ti->class_init = info->class_init;
    ti->class_base_init = info->class_base_init;
    ti->class_data = info->class_data;

    ti->instance_init = info->instance_init;
    ti->instance_post_init = info->instance_post_init;
    ti->instance_finalize = info->instance_finalize;

    ti->abstract = info->abstract;

    for (i = 0; info->interfaces && info->interfaces[i].type; i++) {
        ti->interfaces[i].typename = g_strdup(info->interfaces[i].type);
    }
    ti->num_interfaces = i;

    return ti;
}

 之后,type_register_internal函数会调用type_table_add函数,将这个类注册到全局的表中。

​static TypeImpl *type_register_internal(const TypeInfo *info)
{
    TypeImpl *ti;
    ti = type_new(info);

    type_table_add(ti);
    return ti;
}

到这里,class_init还没有被调用,也即这个类还处于纸面的状态。

此处与Java中的反射机制有些类似。在Java中,对于一个类,首先写代码的时候要写一个class xxx的定义,编译好后就放在.class文件中,这也是处于纸面的状态。然后,Java中会有一个Class对象,用于读取和表示这个纸面上的class xxx,从而生成真正的对象。

在QEMU中,也会有相类似的过程。class_init会生成XXXClass,相当于Java中的Class xxx;TypeImpl中还会有一个instance_init函数,相当于构造函数,用于根据XXXClass生成Object,这就相当于Java反射中最终创建的对象。和构造函数对应的还有instance_finalize,相当于析构函数。

分析完了type_register这一支,正式开始解析主流程。

在QEMU的老版本中,主函数main中直接调用select_machine函数,而在新版本中,则是如下调用流程:

main()

        qemu_main()

                qemu_init()

                        qemu_create_machine

                                select_machine()

qemu_create_machine函数在softmmu/vl.c中,代码如下:

static void qemu_create_machine(QDict *qdict)
{
    MachineClass *machine_class = select_machine(qdict, &error_fatal);
    object_set_machine_compat_props(machine_class->compat_props);

    current_machine = MACHINE(object_new_with_class(OBJECT_CLASS(machine_class)));
    object_property_add_child(object_get_root(), "machine",
                              OBJECT(current_machine));
    object_property_add_child(container_get(OBJECT(current_machine),
                                            "/unattached"),
                              "sysbus", OBJECT(sysbus_get_default()));

    if (machine_class->minimum_page_bits) {
        if (!set_preferred_target_page_bits(machine_class->minimum_page_bits)) {
            /* This would be a board error: specifying a minimum smaller than
             * a target's compile-time fixed setting.
             */
            g_assert_not_reached();
        }
    }

    cpu_exec_init_all();
    page_size_init();

    if (machine_class->hw_version) {
        qemu_set_hw_version(machine_class->hw_version);
    }

    /*
     * Get the default machine options from the machine if it is not already
     * specified either by the configuration file or by the command line.
     */
    if (machine_class->default_machine_opts) {
        QDict *default_opts =
            keyval_parse(machine_class->default_machine_opts, NULL, NULL,
                         &error_abort);
        qemu_apply_legacy_machine_options(default_opts);
        object_set_properties_from_keyval(OBJECT(current_machine), default_opts,
                                          false, &error_abort);
        qobject_unref(default_opts);
    }
}

其中第1行代码就是select_machine函数,代码片段如下:

    MachineClass *machine_class = select_machine(qdict, &error_fatal);

顾名思义,select_machine函数的作用是选择一个MachineClass,其可能由用户指定,如果用户未指定,则采用系统默认。如果是后者,QEMU最新版本号对应的机器类型为默认设置。由于笔者的源码为qemu-8.1.4,因此默认机器类型是pc-i440fx-8.1-machine。而这就对应于:

static const TypeInfo pc_machine_type_v8_1 = {
        .name       = "pc-i440fx-8.1" TYPE_MACHINE_SUFFIX,
        .parent     = TYPE_PC_MACHINE,
        .class_init = pc_machine_v8_1_class_init,
};

static void pc_machine_init_v8_1(void)
{
        type_register(&pc_machine_type_v8_1);
}

select_machine函数同样在softmmu/vl.c中,代码如下:

static MachineClass *select_machine(QDict *qdict, Error **errp)
{
    const char *optarg = qdict_get_try_str(qdict, "type");
    GSList *machines = object_class_get_list(TYPE_MACHINE, false);
    MachineClass *machine_class;
    Error *local_err = NULL;
 
    if (optarg) {
        machine_class = find_machine(optarg, machines);
        qdict_del(qdict, "type");
        if (!machine_class) {
            error_setg(&local_err, "unsupported machine type");
        }
    } else {
        machine_class = find_default_machine(machines);
        if (!machine_class) {
            error_setg(&local_err, "No machine specified, and there is no default");
        }
    }
 
    g_slist_free(machines);
    if (local_err) {
        error_append_hint(&local_err, "Use -machine help to list supported machines\n");
        error_propagate(errp, local_err);
    }
    return machine_class;
}

如上面所讲,在select_machine函数中,有两种方式可以生成MachineClass:一种方式是调用find_machine函数,通过解析QEMU命令行参数生成MachineClass,即用户指定方式;另一种方式是通过find_default_machine函数找一个默认的MachineClass,即系统默认方式。

无论是用户指定还是系统默认方式,都得先调用object_class_get_list函数获得一个MachineClass列表,然后在里边找。代码片段如下:

    GSList *machines = object_class_get_list(TYPE_MACHINE, false);

object_class_get_list函数在qom/object.c中,代码如下:

GSList *object_class_get_list(const char *implements_type,
                              bool include_abstract)
{
    GSList *list = NULL;
 
    object_class_foreach(object_class_get_list_tramp,
                         implements_type, include_abstract, &list);
    return list;
}

object_class_foreach函数在同文件中,代码如下:

void object_class_foreach(void (*fn)(ObjectClass *klass, void *opaque),
                          const char *implements_type, bool include_abstract,
                          void *opaque)
{
    OCFData data = { fn, implements_type, include_abstract, opaque };
 
    enumerating_types = true;
    g_hash_table_foreach(type_table_get(), object_class_foreach_tramp, &data);
    enumerating_types = false;
}

在全局表type_table_get()中,对于每一项TypeImpl,都执行object_class_foreach_tramp。object_class_foreach_tramp函数在qom/object.c中。代码如下:

static void object_class_foreach_tramp(gpointer key, gpointer value,
                                       gpointer opaque)
{
    OCFData *data = opaque;
    TypeImpl *type = value;
    ObjectClass *k;
 
    type_initialize(type);
    k = type->class;
 
    if (!data->include_abstract && type->abstract) {
        return;
    }
 
    if (data->implements_type && 
        !object_class_dynamic_cast(k, data->implements_type)) {
        return;
    }
 
    data->fn(k, data->opaque);
}

在object_class_foreach_tramp函数中,会调用type_initialize函数,该函数在同文件中,代码如下:

static void type_initialize(TypeImpl *ti)
{
    TypeImpl *parent;

    if (ti->class) {
        return;
    }

    ti->class_size = type_class_get_size(ti);
    ti->instance_size = type_object_get_size(ti);
    /* Any type with zero instance_size is implicitly abstract.
     * This means interface types are all abstract.
     */
    if (ti->instance_size == 0) {
        ti->abstract = true;
    }
    if (type_is_ancestor(ti, type_interface)) {
        assert(ti->instance_size == 0);
        assert(ti->abstract);
        assert(!ti->instance_init);
        assert(!ti->instance_post_init);
        assert(!ti->instance_finalize);
        assert(!ti->num_interfaces);
    }
    ti->class = g_malloc0(ti->class_size);

    parent = type_get_parent(ti);
    if (parent) {
        type_initialize(parent);
        GSList *e;
        int i;

        g_assert(parent->class_size <= ti->class_size);
        g_assert(parent->instance_size <= ti->instance_size);
        memcpy(ti->class, parent->class, parent->class_size);
        ti->class->interfaces = NULL;

        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);
        }

        for (i = 0; i < ti->num_interfaces; i++) {
            TypeImpl *t = type_get_by_name(ti->interfaces[i].typename);
            if (!t) {
                error_report("missing interface '%s' for object '%s'",
                             ti->interfaces[i].typename, parent->name);
                abort();
            }
            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;
                }
            }

            if (e) {
                continue;
            }

            type_initialize_interface(ti, t, t);
        }
    }

    ti->class->properties = g_hash_table_new_full(g_str_hash, g_str_equal, NULL,
                                                  object_property_free);

    ti->class->type = ti;

    while (parent) {
        if (parent->class_base_init) {
            parent->class_base_init(ti->class, ti->class_data);
        }
        parent = type_get_parent(parent);
    }

    if (ti->class_init) {
        ti->class_init(ti->class, ti->class_data);
    }
}

在type_initialize函数中(最后一部分),会调用class_init,将纸面上的class也即TypeImpl变为ObjectClass。ObjectClass是所有类的祖先,MachineClass是它的子类。在这里,肯定可以找到之前注册过的TypeImpl,并调用其class_init函数。

因而,pc_machine_##suffix##class_init(本例中是pc_machine_v8_1_class_init)会被调用。

static void pc_machine_v8_1_class_init(ObjectClass *oc, void *data)
{
        MachineClass *mc = MACHINE_CLASS(oc);
        pc_i440fx_8_1_machine_options(mc);
        mc->init = pc_init_v8_1;
}

在此函数中,pc_i440fx_machine_options才真正由pc_i440fx_8_1machine_options调用,从而初始化MachineClass。

pc_i440fx_8_1machine_options函数在hw/i386/pc_piix.c中,代码如下:

static void pc_i440fx_8_1_machine_options(MachineClass *m)
{
    pc_i440fx_machine_options(m);
    m->alias = "pc";
    m->is_default = true;
}

pc_i440fx_machine_options函数就在上边,代码如下:

static void pc_i440fx_machine_options(MachineClass *m)
{
    PCMachineClass *pcmc = PC_MACHINE_CLASS(m);
    pcmc->pci_root_uid = 0;
    pcmc->default_cpu_version = 1;

    m->family = "pc_piix";
    m->desc = "Standard PC (i440FX + PIIX, 1996)";
    m->default_machine_opts = "firmware=bios-256k.bin";
    m->default_display = "std";
    m->default_nic = "e1000";
    m->no_parallel = !module_object_class_by_name(TYPE_ISA_PARALLEL);
    machine_class_allow_dynamic_sysbus_dev(m, TYPE_RAMFB_DEVICE);
    machine_class_allow_dynamic_sysbus_dev(m, TYPE_VMBUS_BRIDGE);
}

pc_machine_v8_1_class_init函数接下来将MachineClass的init函数设置为pc_init##suffix。

    mc->init = pc_init_v8_1;

因此,在select_machine函数执行完毕后,就有一个MachineClass了。

MachineClass的定义在include/qemu/typedefs.h中,如下:

typedef struct MachineClass MachineClass;

struct MachineClass的定义在include/hw/boards.h中,如下:

struct MachineClass {
    /*< private >*/
    ObjectClass parent_class;
    /*< public >*/

    const char *family; /* NULL iff @name identifies a standalone machtype */
    char *name;
    const char *alias;
    const char *desc;
    const char *deprecation_reason;

    void (*init)(MachineState *state);
    void (*reset)(MachineState *state, ShutdownCause reason);
    void (*wakeup)(MachineState *state);
    int (*kvm_type)(MachineState *machine, const char *arg);

    BlockInterfaceType block_default_type;
    int units_per_default_bus;
    int max_cpus;
    int min_cpus;
    int default_cpus;
    unsigned int no_serial:1,
        no_parallel:1,
        no_floppy:1,
        no_cdrom:1,
        no_sdcard:1,
        pci_allow_0_address:1,
        legacy_fw_cfg_order:1;
    bool is_default;
    const char *default_machine_opts;
    const char *default_boot_order;
    const char *default_display;
    const char *default_nic;
    GPtrArray *compat_props;
    const char *hw_version;
    ram_addr_t default_ram_size;
    const char *default_cpu_type;
    bool default_kernel_irqchip_split;
    bool option_rom_has_mr;
    bool rom_file_has_mr;
    int minimum_page_bits;
    bool has_hotpluggable_cpus;
    bool ignore_memory_transaction_failures;
    int numa_mem_align_shift;
    const char **valid_cpu_types;
    strList *allowed_dynamic_sysbus_devices;
    bool auto_enable_numa_with_memhp;
    bool auto_enable_numa_with_memdev;
    bool ignore_boot_device_suffixes;
    bool smbus_no_migration_support;
    bool nvdimm_supported;
    bool numa_mem_supported;
    bool auto_enable_numa;
    bool cpu_cluster_has_numa_boundary;
    SMPCompatProps smp_props;
    const char *default_ram_id;

    HotplugHandler *(*get_hotplug_handler)(MachineState *machine,
                                           DeviceState *dev);
    bool (*hotplug_allowed)(MachineState *state, DeviceState *dev,
                            Error **errp);
    CpuInstanceProperties (*cpu_index_to_instance_props)(MachineState *machine,
                                                         unsigned cpu_index);
    const CPUArchIdList *(*possible_cpu_arch_ids)(MachineState *machine);
    int64_t (*get_default_cpu_node_id)(const MachineState *ms, int idx);
    ram_addr_t (*fixup_ram_size)(ram_addr_t size);
};

回到qemu_create_machine函数中,

static void qemu_create_machine(QDict *qdict)
{
    MachineClass *machine_class = select_machine(qdict, &error_fatal);
    object_set_machine_compat_props(machine_class->compat_props);

    current_machine = MACHINE(object_new_with_class(OBJECT_CLASS(machine_class)));
    object_property_add_child(object_get_root(), "machine",
                              OBJECT(current_machine));
    object_property_add_child(container_get(OBJECT(current_machine),
                                            "/unattached"),
                              "sysbus", OBJECT(sysbus_get_default()));

    if (machine_class->minimum_page_bits) {
        if (!set_preferred_target_page_bits(machine_class->minimum_page_bits)) {
            /* This would be a board error: specifying a minimum smaller than
             * a target's compile-time fixed setting.
             */
            g_assert_not_reached();
        }
    }

    cpu_exec_init_all();
    page_size_init();

    if (machine_class->hw_version) {
        qemu_set_hw_version(machine_class->hw_version);
    }

    /*
     * Get the default machine options from the machine if it is not already
     * specified either by the configuration file or by the command line.
     */
    if (machine_class->default_machine_opts) {
        QDict *default_opts =
            keyval_parse(machine_class->default_machine_opts, NULL, NULL,
                         &error_abort);
        qemu_apply_legacy_machine_options(default_opts);
        object_set_properties_from_keyval(OBJECT(current_machine), default_opts,
                                          false, &error_abort);
        qobject_unref(default_opts);
    }
}

在select_machine函数执行完毕后,即获得了一个MachineClass之后,接下来来到以下代码片段:

    current_machine = MACHINE(object_new_with_class(OBJECT_CLASS(machine_class)));

MACHINE函数的代码在include/hw/boards.h中,如下:

static inline G_GNUC_UNUSED MachineState *MACHINE(const void *obj)
{
    return OBJECT_CHECK(MachineState, obj, TYPE_MACHINE);
}

OBJECT_CHECK宏展开后的函数代码如下:

static inline G_GNUC_UNUSED MachineState *MACHINE(const void *obj)
{
    return ((MachineState*)object_dynamic_cast_assert(OBJECT(obj), ("machine"), __FILE__, __LINE__, __func__));
}

现在要回过头来看qemu_create_machine函数(softmmu/vl.c中)调用MACHINE函数时,传递给它的实参:object_new(object_class_get_name(OBJECT_CLASS(machine_class)))。

现在object_new_with_class以及object_new函数成为了关注焦点。这两个函数都在qom/object.c中,代码分别如下:

Object *object_new(const char *typename)
{
    TypeImpl *ti = type_get_by_name(typename);
 
    return object_new_with_type(ti);
}
Object *object_new_with_class(ObjectClass *klass)
{
    return object_new_with_type(klass->type);
}

可以看到,甭管是哪一个函数,最终都会调用到object_new_with_type函数。

object_new_with_type函数也在qom/object.c中,代码如下:

static Object *object_new_with_type(Type type)
{
    Object *obj;
    size_t size, align;
    void (*obj_free)(void *);

    g_assert(type != NULL);
    type_initialize(type);

    size = type->instance_size;
    align = type->instance_align;

    /*
     * Do not use qemu_memalign unless required.  Depending on the
     * implementation, extra alignment implies extra overhead.
     */
    if (likely(align <= __alignof__(qemu_max_align_t))) {
        obj = g_malloc(size);
        obj_free = g_free;
    } else {
        obj = qemu_memalign(align, size);
        obj_free = qemu_vfree;
    }

    object_initialize_with_type(obj, size, type);
    obj->free = obj_free;

    return obj;
}

回到object_new函数中。TypeImpl的instance_init会被调用,创建一个对象。current_machine就是这个对象,其类型是MachineState。

​Object *object_new(const char *typename)
{
    TypeImpl *ti = type_get_by_name(typename);
 
    return object_new_with_type(ti);
}

至此,兜兜转转一大圈,相关体系结构的对象才创建完毕。整体流程如下图所示(图片援引《趣谈Linux系统》50 | 计算虚拟化之CPU(上):如何复用集团的人力资源?):

欲知后事如何,且看下回分解。

相关推荐

  1. QEMU解析 —— virtio(12

    2024-01-30 10:16:01       49 阅读
  2. QEMU解析 —— virtio(10

    2024-01-30 10:16:01       65 阅读
  3. QEMU解析 —— virtio(6)

    2024-01-30 10:16:01       71 阅读
  4. QEMU解析 —— virtio(8)

    2024-01-30 10:16:01       49 阅读
  5. QEMU解析 —— virtio(27)

    2024-01-30 10:16:01       37 阅读
  6. QEMU解析 —— PCI设备模拟(6)

    2024-01-30 10:16:01       61 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-01-30 10:16:01       98 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-01-30 10:16:01       106 阅读
  3. 在Django里面运行非项目文件

    2024-01-30 10:16:01       87 阅读
  4. Python语言-面向对象

    2024-01-30 10:16:01       96 阅读

热门阅读

  1. 【Qt】QInputDialog setGeometry: Unable to set geometry 问题

    2024-01-30 10:16:01       63 阅读
  2. 【Qt 多线程+opencv 读取和显示图像】

    2024-01-30 10:16:01       53 阅读
  3. QT C++语言格式化输出wchar_t * 中文乱码

    2024-01-30 10:16:01       51 阅读
  4. 什么是模板方法模式?它的实现方式有哪些?

    2024-01-30 10:16:01       55 阅读
  5. Python流程控制语句

    2024-01-30 10:16:01       53 阅读
  6. whatsapp 相关(六) -frida hook file

    2024-01-30 10:16:01       41 阅读
  7. 【Spring Boot 3】异步线程任务

    2024-01-30 10:16:01       53 阅读
  8. pytorch nearest upsample整数型tensor

    2024-01-30 10:16:01       60 阅读
  9. [pytorch] 定义自己的dataloader

    2024-01-30 10:16:01       56 阅读
  10. C#学习笔记_字符串常用方法

    2024-01-30 10:16:01       48 阅读