前言QEMU 是一个通用的、开源的模拟器，能够通过纯软件方式实现硬件的虚拟化，模拟外部硬件，为用户提供抽象、虚拟的硬件环境，可以模拟各种硬件架构并运行多种操作系统。QEMU 的初始化是一个复杂的过程，由 qemu_init 函数负责完成，该过程主要包括参数解析、主循环初始化、机器创建、设备初始化等流程，本文将以 8.0.0 版本的 QEMU RISC-V (qemu-system-riscv64) 为例，分析 virt 机器创建及初始化机制。本文使用以下命令在 QEMU 中创建一台 virt 机器并启动 5.18 版本的 RISC-V Linux 内核：qemu-system-riscv64 -M virt -m 256M -nographic \ -kernel linux-kernel/arch/riscv/boot/Image \ -drive file=rootfs.img,format=raw,id=hd0 \ -device virtio-blk-device,drive=hd0 \ -append "root=/dev/vda rw console=ttyS0"机器创建的工作由函数 qemu_create_machine 完成，而机器初始化则由 qemu_apply_legacy_machine_options 和 qemu_apply_machine_options 完成，如果命令行参数没有配置 --preconfig 选项，则 qmp_x_exit_preconfig 也会承担一部分的机器初始化工作。待机器初始化完成后，QEMU 会调用机器类型对应的初始化方法对机器实例进行初始化。QEMU 机器创建及初始化的基本流程如下图所示：机器创建QEMU 在完成命令行参数的解析以及主循环的初始化后，会根据命令行指定的机器类型调用函数 qemu_create_machine 进行机器创建：/* softmmu/vl.c: 2011 */ 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); } }qemu_create_machine 函数首先调用 select_machine 函数检查命令行指定的机器类型是否在 QEMU 支持的机器列表中：/* softmmu/vl.c: 1577 */ 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 首先调用 object_class_get_list 函数选出由该机器类型所派生出的各种具体机器型号并储存在链表 machines 中：/* qom/object.c: 1155 */ 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; }如果 machines 列表不为空，则意味着命令行指定的机器类型位于 QEMU 的支持列表中，则 select_machine 会继续调用 find_machine 函数找到对应机器类型并返回：/* softmmu/vl.c: 799 */ static MachineClass *find_machine(const char *name, GSList *machines) { GSList *el; for (el = machines; el; el = el->next) { MachineClass *mc = el->data; if (!strcmp(mc->name, name) || !g_strcmp0(mc->alias, name)) { return mc; } } return NULL; }接下来 QEMU 会调用 object_set_machine_compat_props 设置 virt 机器的全局属性，然后之前选择出的机器类型创建一个该类的实例 current_machine，这里的 current_machine 是一个 MachineState 类型的指针：/* include/hw/boards.h: 324 */ struct MachineState { /* < private > */ Object parent_obj; /* < public > */ void *fdt; char *dtb; char *dumpdtb; int phandle_start; char *dt_compatible; bool dump_guest_core; bool mem_merge; bool usb; bool usb_disabled; char *firmware; bool iommu; bool suppress_vmdesc; bool enable_graphics; ConfidentialGuestSupport *cgs; HostMemoryBackend *memdev; /* * convenience alias to ram_memdev_id backend memory region * or to numa container memory region */ MemoryRegion *ram; DeviceMemoryState *device_memory; ram_addr_t ram_size; ram_addr_t maxram_size; uint64_t ram_slots; BootConfiguration boot_config; char *kernel_filename; char *kernel_cmdline; char *initrd_filename; const char *cpu_type; AccelState *accelerator; CPUArchIdList *possible_cpus; CpuTopology smp; struct NVDIMMState *nvdimms_state; struct NumaState *numa_state; };QEMU 使用 MachineState 结构体对机器的参数和运行状态进行描述，这里的 current_machine 就描述了新创建的 virt 机器，对 virt 的各类初始化操作就是对 current_machine 的成员变量进行操作。完成机器的实例化之后，qemu_create_machine 函数通过调用 object_property_add_child 将新创建的机器实例添加到 QEMU 的对象树中，并将默认的系统总线添加为其子对象。最后，QEMU 使用 cpu_exec_init_all 和 page_size_init 等函数对包括 IO 空间、内存映射、页面大小在内的一系列运行环境进行初始化，最终完成机器的创建，退出 qemu_create_machine 函数。机器参数初始化机器参数初始化环节主要是根据命令函参数解析的结果设置新创建的机器实例的参数，具体工作由 qemu_apply_legacy_machine_options 和 qemu_apply_machine_options 函数完成。qemu_apply_legacy_machine_options 函数负责设置不在 MachineState 结构体中的属性，主要包括 accel, kernel-irqchip, memory-backend 等命令行选项：/* softmmu/vl.c: 1649 */ static void qemu_apply_legacy_machine_options(QDict *qdict) { const char *value; QObject *prop; keyval_dashify(qdict, &error_fatal); /* Legacy options do not correspond to MachineState properties. */ ... prop = qdict_get(qdict, "memory"); if (prop) { have_custom_ram_size = qobject_type(prop) == QTYPE_QDICT && qdict_haskey(qobject_to(QDict, prop), "size"); } }以本文开头的 QEMU 启动命令为例，其中使用 -m 256M 选项指定了机器内存大小，qemu_apply_legacy_machine_options 在遍历参数过程中会设置布尔变量 have_custom_ram_size 值为 true，这一变量在之后调用 qemu_resolve_machine_memdev 函数解析和设置虚拟机内存后端的过程中用来判断用户是否指定自定义的 RAM 大小。qemu_apply_machine_options 函数主要目的是将参数解析提供的机器选项应用到当前创建的虚拟机上：/* softmmu/vl.c: 1845 */ static void qemu_apply_machine_options(QDict *qdict) { object_set_properties_from_keyval(OBJECT(current_machine), qdict, false, &error_fatal); if (semihosting_enabled(false) && !semihosting_get_argc()) { /* fall back to the -kernel/-append */ semihosting_arg_fallback(current_machine->kernel_filename, current_machine->kernel_cmdline); } if (current_machine->smp.cpus > 1) { replay_add_blocker("smp"); } }qemu_apply_machine_options 函数会调用 object_set_properties_from_keyval 函数将 qdict 中的键值对设置为 current_machine 对象的属性，并在结束执行前检查机器是否启用 semi-host 模式以及是否配置了多个 CPU。机器实例初始化如果命令行参数启用了 --preconfig 选项，那么 QEMU 将会在完成创建初始虚拟机之前暂停，进入交互式配置状态并允许通过 QEMU Machine Protocol（QMP）进行一些配置。在本文中，没有配置 --preconfig 选项，因此 qemu_init 函数在初始化工作进入尾声时会调用 qmp_x_exit_preconfig 函数以避免 QEMU 执行暂停：/* softmmu/vl.c: 2602 */ void qmp_x_exit_preconfig(Error **errp) { if (phase_check(PHASE_MACHINE_INITIALIZED)) { error_setg(errp, "The command is permitted only before machine initialization"); return; } qemu_init_board(); qemu_create_cli_devices(); qemu_machine_creation_done(); if (loadvm) { load_snapshot(loadvm, NULL, false, NULL, &error_fatal); } if (replay_mode != REPLAY_MODE_NONE) { replay_vmstate_init(); } if (incoming) { Error *local_err = NULL; if (strcmp(incoming, "defer") != 0) { qmp_migrate_incoming(incoming, &local_err); if (local_err) { error_reportf_err(local_err, "-incoming %s: ", incoming); exit(1); } } } else if (autostart) { qmp_cont(NULL); } }其中 qemu_init_board 函数会进一步调用 machine_run_board_init 完成机器实例（主板）的一些初始化操作，包括设置内存、检查 CPU 类型、初始化加速器和其他相关的设置：/* hw/core/machine.c: 1307 */ void machine_run_board_init(MachineState *machine, const char *mem_path, Error **errp) { MachineClass *machine_class = MACHINE_GET_CLASS(machine); ObjectClass *oc = object_class_by_name(machine->cpu_type); CPUClass *cc; /* This checkpoint is required by replay to separate prior clock reading from the other reads, because timer polling functions query clock values from the log. */ replay_checkpoint(CHECKPOINT_INIT); if (!xen_enabled()) { /* On 32-bit hosts, QEMU is limited by virtual address space */ if (machine->ram_size > (2047 << 20) && HOST_LONG_BITS == 32) { error_setg(errp, "at most 2047 MB RAM can be simulated"); return; } } if (machine->memdev) { ram_addr_t backend_size = object_property_get_uint(OBJECT(machine->memdev), "size", &error_abort); if (backend_size != machine->ram_size) { error_setg(errp, "Machine memory size does not match the size of the memory backend"); return; } } else if (machine_class->default_ram_id && machine->ram_size && numa_uses_legacy_mem()) { if (!create_default_memdev(current_machine, mem_path, errp)) { return; } } if (machine->numa_state) { numa_complete_configuration(machine); if (machine->numa_state->num_nodes) { machine_numa_finish_cpu_init(machine); } } if (!machine->ram && machine->memdev) { machine->ram = machine_consume_memdev(machine, machine->memdev); } /* If the machine supports the valid_cpu_types check and the user * specified a CPU with -cpu check here that the user CPU is supported. */ if (machine_class->valid_cpu_types && machine->cpu_type) { int i; for (i = 0; machine_class->valid_cpu_types[i]; i++) { if (object_class_dynamic_cast(oc, machine_class->valid_cpu_types[i])) { /* The user specificed CPU is in the valid field, we are * good to go. */ break; } } if (!machine_class->valid_cpu_types[i]) { /* The user specified CPU is not valid */ error_report("Invalid CPU type: %s", machine->cpu_type); error_printf("The valid types are: %s", machine_class->valid_cpu_types[0]); for (i = 1; machine_class->valid_cpu_types[i]; i++) { error_printf(", %s", machine_class->valid_cpu_types[i]); } error_printf("\n"); exit(1); } } /* Check if CPU type is deprecated and warn if so */ cc = CPU_CLASS(oc); if (cc && cc->deprecation_note) { warn_report("CPU model %s is deprecated -- %s", machine->cpu_type, cc->deprecation_note); } if (machine->cgs) { /* * With confidential guests, the host can't see the real * contents of RAM, so there's no point in it trying to merge * areas. */ machine_set_mem_merge(OBJECT(machine), false, &error_abort); /* * Virtio devices can't count on directly accessing guest * memory, so they need iommu_platform=on to use normal DMA * mechanisms. That requires also disabling legacy virtio * support for those virtio pci devices which allow it. */ object_register_sugar_prop(TYPE_VIRTIO_PCI, "disable-legacy", "on", true); object_register_sugar_prop(TYPE_VIRTIO_DEVICE, "iommu_platform", "on", false); } accel_init_interfaces(ACCEL_GET_CLASS(machine->accelerator)); machine_class->init(machine); phase_advance(PHASE_MACHINE_INITIALIZED); }在上述代码中需要关注的是通过 machine_class->init 来调用机器类型的初始化方法对机器实例 machine 进行初始化，通过动态调试我们可以发现，这里实际调用的是 virt_machine_init 函数：/* hw/riscv/virt.c: 1332 */ static void virt_machine_init(MachineState *machine) { const MemMapEntry *memmap = virt_memmap; RISCVVirtState *s = RISCV_VIRT_MACHINE(machine); MemoryRegion *system_memory = get_system_memory(); MemoryRegion *mask_rom = g_new(MemoryRegion, 1); char *soc_name; DeviceState *mmio_irqchip, *virtio_irqchip, *pcie_irqchip; int i, base_hartid, hart_count; int socket_count = riscv_socket_count(machine); /* Check socket count limit */ ... /* Initialize sockets */ ... /* register system main memory (actual RAM) */ memory_region_add_subregion(system_memory, memmap[VIRT_DRAM].base, machine->ram); /* boot rom */ memory_region_init_rom(mask_rom, NULL, "riscv_virt_board.mrom", memmap[VIRT_MROM].size, &error_fatal); memory_region_add_subregion(system_memory, memmap[VIRT_MROM].base, mask_rom); /* * Init fw_cfg. Must be done before riscv_load_fdt, otherwise the * device tree cannot be altered and we get FDT_ERR_NOSPACE. */ s->fw_cfg = create_fw_cfg(machine); rom_set_fw(s->fw_cfg); /* SiFive Test MMIO device */ sifive_test_create(memmap[VIRT_TEST].base); /* VirtIO MMIO devices */ ... /* load/create device tree */ if (machine->dtb) { machine->fdt = load_device_tree(machine->dtb, &s->fdt_size); if (!machine->fdt) { error_report("load_device_tree() failed"); exit(1); } } else { create_fdt(s, memmap); } s->machine_done.notify = virt_machine_done; qemu_add_machine_init_done_notifier(&s->machine_done); }virt_machine_init 函数是 QEMU 中 RISC-V 架构机器 virt 的初始化函数，它首先会检查插槽数量，初始化插槽并每个插槽创建一个中断控制器，然后根据 32 位或 64 位设置 PCIe 内存映射参数，接着注册系统主内存并初始化引导 ROM。此后，会初始化 virt 机器的一系列的设备，包括 VirtIO MMIO 设备、PCIe 设备、平台总线、串行设备、RTC 和闪存设备等，最后加载或创建设备树并设置机器初始化完成通知符告知 QEMU 机器（主板）初始化完成。当机器初始化工作全部完成后，由 qemu_machine_creation_done 函数执行一系列后置的检查和状态同步操作，通知 QEMU 机器初始化工作全部完成且通过检查，执行流程可以进入下一阶段。总结本文梳理了 QEMU 机器创建及初始化的基本流程，并以 RISC-V 架构的 virt 平台为例，从机器类型选择、机器参数初始化和机器实例初始化三个环节切入，深入分析了 QEMU 机器创建机制的技术细节，打通了 QEMU 中虚拟机创建流程的内在逻辑。参考资料《QEMU/KVM 源码解析与应用》李强，机械工业出版社QEMU 启动方式分析（3）: QEMU 代码与 RISCV 'virt' 平台 ZSBL 分析QEMU RISC-V virt 平台分析QEMU QMP Reference Manual‘virt’ Generic Virtual Platform
前言在上一篇文章中我们聚焦 QEMU 中面向对象的设备管理机制，阐述了引入对象模型的必要性，介绍了 QOM 基本功能和顶层设计，本文将在此基础上进一步深入，详细分析 QOM 对象系统的设计思路以及实现方式。关键结构通过之前的分析，我们已经知道 QOM 有如下几大关键数据结构：Object、ObjectClass、TypeInfo 和 TypeImpl，他们的关系如下图所示：通过上图不难看出，QOM 对象模型的核心结构是 Object，这个结构体十分简洁，只保存了有关类型和父类的信息。ObjectClass 结构体就比较复杂，保存指向类型信息 TypeImpl 结构体的指针，保证了能够获取有关类型的信息。TypeImpl 存储着一个类型的信息，包括类型名称、类型大小、是否抽象类、父类名称、父类类型指针、ObjectClass 等。TypeInfo 结构体没有直接与其他任何数据结构产生直接关联，通过前面的分析我们已经知道，这是面向开发者的一个工具结构，主要用于在注册类型的时候提供类型的基本信息，在类型注册伊始，QEMU 会自动生成对应的 TypeImpl 结构体，保存类型的全部信息。面向对象特性首先，我们需要回顾一下面向对象的基本特征：封装接口继承析构静态成员多态动态类型装换下面我们将详细分析上述特性在 QOM 对象系统中的具体实现。封装在分析 QOM 如何实现封装之前，我们需要再次回顾 Object 结构体的定义：/* include/qom/object.h: 153 */ struct Object { /* private: */ ObjectClass *class; ObjectFree *free; GHashTable *properties; uint32_t ref; Object *parent; };这里需要关注的是第一行的注释，它表示结构体中的所有属性都是私有的，只能被类的内部成员访问和修改。但是，仅靠 C 语言中的结构体是无法实现对私有变量的访问控制的，因此 QEMU 在 Object 中引入了属性表，即 properties 指针，它指向一张哈希表，该表含了 Object 中的所有可以访问、修改的数据和函数其中每一个键值对表示 property 的名称以及指向相应 ObjectProperty 结构体的指针。下面给出 ObjectProperty 结构体的实现代码：/* include/qom/object.h: 88 */ struct ObjectProperty { char *name; char *type; char *description; ObjectPropertyAccessor *get; ObjectPropertyAccessor *set; ObjectPropertyResolve *resolve; ObjectPropertyRelease *release; ObjectPropertyInit *init; void *opaque; QObject *defval; };可以看到，ObjectProperty 结构体包含了这个属性的名称、类型、描述、读写方法以及解析和释放函数，还包括这个 property 特有的属性，使用 opaque 指针来表示。QOM 通过 ObjectProperty 结构体将对象的每个数据都保存在这样一个单元之中，再利用一个哈希表实现对对象所有数据的统一管理，进而实现了数据封装。当用户需要向 Object 中增加属性时，需要调用 object_property_add 函数：/* qom/object.c: 1257 */ ObjectProperty * object_property_add(Object *obj, const char *name, const char *type, ObjectPropertyAccessor *get, ObjectPropertyAccessor *set, ObjectPropertyRelease *release, void *opaque) { return object_property_try_add(obj, name, type, get, set, release, opaque, &error_abort); }该函数通过进一步调用 object_property_try_add 函数向 properties 所指向的哈希表中插入了一个新的属性：/* qom/object.c: 1206 */ ObjectProperty * object_property_try_add(Object *obj, const char *name, const char *type, ObjectPropertyAccessor *get, ObjectPropertyAccessor *set, ObjectPropertyRelease *release, void *opaque, Error **errp) { ObjectProperty *prop; size_t name_len = strlen(name); if (name_len >= 3 && !memcmp(name + name_len - 3, "[*]", 4)) { int i; ObjectProperty *ret = NULL; char *name_no_array = g_strdup(name); name_no_array[name_len - 3] = '\0'; for (i = 0; i < INT16_MAX; ++i) { char *full_name = g_strdup_printf("%s[%d]", name_no_array, i); ret = object_property_try_add(obj, full_name, type, get, set, release, opaque, NULL); g_free(full_name); if (ret) { break; } } g_free(name_no_array); assert(ret); return ret; } if (object_property_find(obj, name) != NULL) { error_setg(errp, "attempt to add duplicate property '%s' to object (type '%s')", name, object_get_typename(obj)); return NULL; } prop = g_malloc0(sizeof(*prop)); prop->name = g_strdup(name); prop->type = g_strdup(type); prop->get = get; prop->set = set; prop->release = release; prop->opaque = opaque; g_hash_table_insert(obj->properties, prop->name, prop); return prop; }继承在面向对象编程中主要包括三种继承形式：可视继承： QEMU 中可视继承主要用于处理图形界面的相关问题，这里不做深入讨论实现继承： 子类能够直接使用基类的属性和方法而无需重新编写代码接口继承： 子类仅使用基类的属性和方法名称，属性和代码的具体内容需要重新编写代码实现对于实现继承，QOM 通过结构体的包含关系完成。在 QEMU 中我们创建一个新类时，会实现两个数据结构：类的数据结构 ObjectClass 和对象的数据结构 Object，由于这两个结构体中的第一个成员变量就是父类（对象），那么只要通过 “指针 + 偏移量” 就可以直接使用父类的属性和方法，完成了实现继承的功能。对于接口继承，QEMU 中定义了专门的接口结构：/* include/qom/object.h: 514 */ struct InterfaceClass { ObjectClass parent_class; /* private: */ ObjectClass *concrete_class; Type interface_type; };在 QOM 中一个类可以实现多个接口，也就是接口继承。ObjectClass 结构体中与接口继承相关的属性是 interfaces，它指向一条链表，链表中的每个元素都是一个指向 InterfaceClass 的指针，再通过其中的 interface_type 指针指向一个 TypeImpl 结构体，我们可以通过给该指针指向的 TypeImpl 结构体中的函数指针赋值，从而达到实现对应接口的目的。多态多态是同一操作作用于不同的对象，可以有不同的解释，产生不同的执行结果。为了实现多态，QOM 实现了一个非常重要的功能，即动态强制类型转换（dynamic cast）。我们可以使用相关的函数，将一个 Object 的指针在运行时 cast 为子类对象的指针，可以将一个 ObjectClass 的指针在运行时 cast 为子类的指针。这样就可以调用子类中定义的函数指针来完成相应的功能。动态 cast 功能主要由 object_class_dynamic_cast 函数实现：/* qom/object.c: 908 */ ObjectClass *object_class_dynamic_cast(ObjectClass *class, const char *typename) { ObjectClass *ret = NULL; TypeImpl *target_type; TypeImpl *type; if (!class) { return NULL; } /* A simple fast path that can trigger a lot for leaf classes. */ type = class->type; if (type->name == typename) { return class; } target_type = type_get_by_name(typename); if (!target_type) { /* target class type unknown, so fail the cast */ return NULL; } if (type->class->interfaces && type_is_ancestor(target_type, type_interface)) { int found = 0; GSList *i; for (i = class->interfaces; i; i = i->next) { ObjectClass *target_class = i->data; if (type_is_ancestor(target_class->type, target_type)) { ret = target_class; found++; } } /* The match was ambiguous, don't allow a cast */ if (found > 1) { ret = NULL; } } else if (type_is_ancestor(type, target_type)) { ret = class; } return ret; }析构QOM 的通过 “引用计数” 来判断何时调用析构函数删除对象，Object 的结构体中有一个专门用于对 Object 引用的计数变量 ref，如果 ref 的值减少为 0，就意味着系统不会继续使用这个对象了，那么就可以对相应的内存空间等进行回收操作：/* qom/object.c: 1192 */ void object_unref(void *objptr) { Object *obj = OBJECT(objptr); if (!obj) { return; } g_assert(obj->ref > 0); /* parent always holds a reference to its children */ if (qatomic_fetch_dec(&obj->ref) == 1) { object_finalize(obj); } }在注册类型时，可以通过定义 TypeInfo 结构体中的 instance_finalize 实现自定义析构函数，对于引用计数为 0 的 Object 进行垃圾回收操作。QOM 提供了默认析构函数：/* qom/object.c: 688 */ static void object_finalize(void *data) { Object *obj = data; TypeImpl *ti = obj->class->type; object_property_del_all(obj); object_deinit(obj, ti); g_assert(obj->ref == 0); g_assert(obj->parent == NULL); if (obj->free) { obj->free(obj); } }Object 数据结构中有一个 ObjectFree * 类型的函数指针 free，当 Object 的引用计数为 0 时，就会调用这个函数进行垃圾回收：/* qom/object.c: 677 */ static void object_deinit(Object *obj, TypeImpl *type) { if (type->instance_finalize) { type->instance_finalize(obj); } if (type_has_parent(type)) { object_deinit(obj, type_get_parent(type)); } }该函数会调用 TypeImpl 中的实例析构函数。如果存在父类，则会递归继续调用父类的实例析构函数。这里之所以需要调用父类实例析构函数是因为一个 Object 结构体的第一个成员变量就是父类对象的实例，因此当我们需要对对象析构时，不仅要调用当前类的析构方法，也需要调用父类的析构方法将结构体中的第一个成员进行析构。总结QOM 实现了一套较为完备的对象管理系统，包括自动化的对象注册、完善的对象管理以及巧妙的动态类型转换，本文梳理了 QOM 实现中几大关键数据结构之间的联系，详细分析了封装、多态、继承以及析构等面向对象特性在 QOM 中的集体实现，与之前的文章相呼应，打通了 QEMU 设备模型从使用到实现的全过程逻辑链条。参考资料Kvmforum14-qom.pdfQOM 实现分析
QEMU 设备模型简析（二）：面向对象的设备管理
前言上一篇文章介绍了 QEMU 设备模型的生命周期，分析了 QEMU 中设备类型注册、设备类型初始化、设备实例化等环节的基本原理与执行流程，在这个过程中我们不难发现 QEMU 设备模型采用了面向对象的设计思想，本文将以 8.0.0 版本的 QEMU RISC-V (qemu-system-riscv64) 为例，阐述 QEMU 中面向对象的设备管理机制。在 Linux 中一切皆文件，而 QEMU 中模拟的一切实体皆对象。以 CPU 模拟为例，QEMU 中要实现对各种 CPU 架构的模拟，而且对于同一种架构的 CPU，比如 RISC-V 架构，由于功能特性的不同，也会有不同的 CPU 型号。任何型号的 CPU 中都有 CPU 的通用属性，同时也包含各自特有的属性，使用面向对象的设计思想可以非常高效地实现各种型号 CPU 的模拟。此外，在主板上一个设备会通过总线与其他的设备相连接，而其他的设备也可以进一步通过总线与更多的设备连接，同时一个总线上会连接多个设备。这种总线型关系的模拟也可以非常便捷地使用面向对象的模型实现。QEMU 对象模型（QEMU Object Model, QOM）提供了一个通用的面向对象框架，用于注册用户创建的设备类型和实例化这些类型的对象。QOM 提供以下功能：动态的设备类型注册系统支持设备类型的单一继承无状态接口的多重继承将内部成员映射到公开的属性QOM 中最基础的对象是 TYPE_OBJECT，它提供所有对象共有的基本方法。基类ObjectClass 是 QOM 中所有类的基础，每一类对象会实例化一个 ObjectClass，类的成员是这类对象通用的内容：/* include/qom/object.h: 127 */ struct ObjectClass { /* private: */ Type type; GSList *interfaces; const char *object_cast_cache[OBJECT_CLASS_CAST_CACHE]; const char *class_cast_cache[OBJECT_CLASS_CAST_CACHE]; ObjectUnparent *unparent; GHashTable *properties; };Object 是 QOM 中所有对象的基础，QOM 的每一个设备会实例化一个 Object，Object 的成员是每个设备独有的内容：/* include/qom/object.h: 153 */ struct Object { /* private: */ ObjectClass *class; ObjectFree *free; GHashTable *properties; uint32_t ref; Object *parent; };Object 的第一个成员是指向 ObjectClass 的指针。由于 C 语言保证结构体的第一个成员始终从该结构体首地址开始，只要任何子对象将其父对象作为第一个成员，就可以直接将其转换为 Object。QOM 中 ObjectClass 和 Object 的关系，可以像上一篇文章中一样理解为设备类型与具体设备的关系，一个 ObjectClass 可以对应多个 Object，而 一个 Object 只会指向一个 ObjectClass。也可以将 ObjectClass 理解为设备驱动，Object 理解为设备。ObjectClass 所描述的主要是某一类设备的通用操作接口，而且 ObjectClass 也会实例化一个实体，这个实体是这一类设备所共用的；而每一个实际的设备都对应一个 Object，每个 Object 又会有一个指针指向 ObjectClass。下面以 virtio-net 设备为例，分析 QEMU 对象模型的继承和派生关系：QOM 最底层的基类是 ObjectClass，对应的对象是 Object设备的类 DeviceClass，对应的对象是 DeviceStatePCI 设备在设备类的基础上派生出了 PCIDeviceClass，对应的对象是 PCIDevicevirtio-pci 设备又在 PCI 设备的基础上派生出 VirtioPCIClass，对应的对象是 VirtIOPCIProxyvirtio-net 设备在 VIRTIO-PCI 设备的基础上进一步派生出新的类，这里需要注意的是 virtio-net 设备相比 virtio-pci 设备并不需要增加新的内容，所以这一层派生出的依旧是 VirtioPCIClass，但是对应的对象依然需要增加网络设备特有的内容，因此会派生出新结构体 VirtIONetPCI本质上 C 语言中派生的实现形式就是结构体的包含嵌套，派生类层层包含父类，具体关系如下图所示：正如上文在分析 Object 时介绍的那样，为了更好的在派生结构体之间互相引用，通常把被引用的结构体，也就是父类放在自己的第一个字段，这样就可以很方便地通过首地址加偏移量的方式进行对象类型转换。就拿上图中的 VirtIONetPCI 结构体来说，它的各级父类的 VirtioPCIProxy、PCIDevice、DeviceState 以及 Object 指针指向的就是自己的首地址。除了 ObjectClass 和 Object 这两个结构体之外，QOM 中还有两个重要的数据结构，就是上一篇文章中已经介绍过的 TypeInfo 和 TypeImpl。TypeInfo 是与 ObjectClass 和 Object 并列的结构，每一类设备不仅对应一种 ObjectClass 和 Object，而且还需要对应一个 TypeInfo 结构。在 QOM 中，TypeInfo 是统一的，并不是 ObjectClass 和 Object 那样自底向上层层派生的结构。TypeInfo 是一个对外的接口，其目的是收集用户创建设备的必要信息，以实例化出相应的 ObjectClass 和 Object。QOM 内部维护了一个全局的 TypeTmpl 哈希表，当 QMP 命令生产一个对象的时候，会从该表中找到对应的 TypeImpl 结构，然后根据 TypeImpl 的内容去初始化化相应的 ObjectClass 和 Object。需要注意的是，如果已经注册过相同类型的设备，则已有 ObjectClass，无需再次初始化。基于 QOM 的设备管理创建新设备类型使用 QOM 对象模型创建新设备类型的详细内容可以参考 QEMU 官方文档，QOM 各接口的使用说明详见 include/qom/object.h 文件中各函数的注释。这里仅以最简单的设备类型为例，介绍使用 QOM 对象模型创建新的设备类型的一般方法：#include "qdev.h" #define TYPE_MY_DEVICE "my-device" // No new virtual functions: we can reuse the typedef for the // superclass. typedef DeviceClass MyDeviceClass; typedef struct MyDevice { DeviceState parent_obj; int reg0, reg1, reg2; } MyDevice; static const TypeInfo my_device_info = { .name = TYPE_MY_DEVICE, .parent = TYPE_DEVICE, .instance_size = sizeof(MyDevice), }; static void my_device_register_types(void) { type_register_static(&my_device_info); } type_init(my_device_register_types)在上述代码中，我们使用 TypeInfo 结构体描述了新创建的 my-device 设备类型，并定义了设备类型注册函数。这里需要注意的是，在 MyDevice 结构体中，父对象必须是结构体的的第一个成员，以便实现父对象向子对象的投射。my_device_info 结构体的父类是 TYPE_DEVICE，该类是在 QEMU 中所有设备的父类，TYPE_DEVICE 提供一些通用方法来处理 QEMU 设备模型。对于多个静态设备类型，也可以使用辅助宏 DEFINE_TYPES 一并注册：static const TypeInfo device_types_info[] = { { .name = TYPE_MY_DEVICE_A, .parent = TYPE_DEVICE, .instance_size = sizeof(MyDeviceA), }, { .name = TYPE_MY_DEVICE_B, .parent = TYPE_DEVICE, .instance_size = sizeof(MyDeviceB), }, }; DEFINE_TYPES(device_types_info)如上文所述，每个 Object 都有一个与之关联的 ObjectClass，ObjectClass 是动态实例化的，但任何给定 Object 都只有一个 ObjectClass 实例。对于每个新的设备类型，都要定义由 ObjectClass 动态转换为 MyDeviceClass 的方法，也会定义由 Object 动态转换为 MyDevice 的方法。以下涉及的宏 OBJECT_GET_CLASS、OBJECT_CLASS_CHECK 和 OBJECT_CHECK 都在 include/qemu/object.h 文件中定义：#define MY_DEVICE_GET_CLASS(obj) \ OBJECT_GET_CLASS(MyDeviceClass, obj, TYPE_MY_DEVICE) #define MY_DEVICE_CLASS(klass) \ OBJECT_CLASS_CHECK(MyDeviceClass, klass, TYPE_MY_DEVICE) #define MY_DEVICE(obj) \ OBJECT_CHECK(MyDevice, obj, TYPE_MY_DEVICE)另外，如果 ObjectClass 的实现可以作为模块构建，则必须调用 module_obj 函数，以确保 QEMU 在需要时正确加载模块：module_obj(TYPE_MY_DEVICE);设备类型初始化在初始化 Object 之前，必须先初始化对应的 ObjectClass。ObjectClass 的初始化顺序是首先初始化父类，当父类对象初始化完成后，它将被复制到当前 ObjectClass 中，剩余都将被清零。这样做的目的在于，该 ObjectClass 能够自动继承父类已初始化的任何虚拟函数指针。如果我们在定义新类型中，实现了父类的虚函数，那么需要定义新的 class 的初始化函数，并且在 TypeInfo 数据结构中，给 TypeInfo 的 class_init 字段赋予该初始化函数的函数指针：#include "qdev.h" void my_device_class_init(ObjectClass *klass, void *class_data) { DeviceClass *dc = DEVICE_CLASS(klass); dc->reset = my_device_reset; } static const TypeInfo my_device_info = { .name = TYPE_MY_DEVICE, .parent = TYPE_DEVICE, .instance_size = sizeof(MyDevice), .class_init = my_device_class_init, };需要注意的是，引入新的虚拟方法需要 Object 定义自己的结构体，并在 TypeInfo 中添加 .class_size 成员，每个方法还需要一个封装函数，以方便调用：#include "qdev.h" typedef struct MyDeviceClass { DeviceClass parent_class; void (*frobnicate) (MyDevice *obj); } MyDeviceClass; static const TypeInfo my_device_info = { .name = TYPE_MY_DEVICE, .parent = TYPE_DEVICE, .instance_size = sizeof(MyDevice), .abstract = true, // or set a default in my_device_class_init .class_size = sizeof(MyDeviceClass), }; void my_device_frobnicate(MyDevice *obj) { MyDeviceClass *klass = MY_DEVICE_GET_CLASS(obj); klass->frobnicate(obj); }派生类当我们需要从一个类创建一个派生类时，如果需要重写该类原有的虚拟方法，派生类中，可以增加相关的属性将类原有的虚拟函数指针保存，然后给虚拟函数赋予新的函数指针，保证父类原有的虚拟函数指针不会丢失：typedef struct MyState MyState; typedef void (*MyDoSomething)(MyState *obj); typedef struct MyClass { ObjectClass parent_class; MyDoSomething do_something; } MyClass; static void my_do_something(MyState *obj) { // do something } static void my_class_init(ObjectClass *oc, void *data) { MyClass *mc = MY_CLASS(oc); mc->do_something = my_do_something; } static const TypeInfo my_type_info = { .name = TYPE_MY, .parent = TYPE_OBJECT, .instance_size = sizeof(MyState), .class_size = sizeof(MyClass), .class_init = my_class_init, }; typedef struct DerivedClass { MyClass parent_class; MyDoSomething parent_do_something; } DerivedClass; static void derived_do_something(MyState *obj) { DerivedClass *dc = DERIVED_GET_CLASS(obj); // do something here dc->parent_do_something(obj); // do something else here } static void derived_class_init(ObjectClass *oc, void *data) { MyClass *mc = MY_CLASS(oc); DerivedClass *dc = DERIVED_CLASS(oc); dc->parent_do_something = mc->do_something; mc->do_something = derived_do_something; } static const TypeInfo derived_type_info = { .name = TYPE_DERIVED, .parent = TYPE_MY, .class_size = sizeof(DerivedClass), .class_init = derived_class_init, };热插拔由于类的初始化过程不能失败，因此设备一般会有两个辅助函数 realize 和 unrealize 来专门处理动态设备的创建。在调用 realize 函数时，如果设备无法成功创建，则应设置 Error ** 指针。否则，在 realize 函数成功返回后，设备对象将被添加到 QOM 树中，并对客户机可见。与 realize 函数功能相反的是 unrealize 函数，主要负责在系统完成设备的使用之后清理释放资源。QEMU 中所有设备都可以通过 C 代码实例化，但是只有部分设备可以通过命令行或者 QEMU Monitor 动态创建。同样，只有部分设备支持热插拔，即可以在创建后拔下，这需要给出明确的 unrealize 函数实现。另外，只有当设备的父总线注册了 HotplugHandler 时，设备才能被顺利拔出。总结本文聚焦 QEMU 中面向对象的设备管理机制，阐述了引入对象模型的必要性，介绍了 QOM 基本功能和顶层设计，分析了 Object 和 ObjectClass 的结构与联系，梳理了面向对象的设备管理中设备的层次关系，总结归纳了使用 QOM 接口管理设备的一般方法。在下一篇文章中，我们将继续深入，分析 QOM 面向对象特性的底层实现。参考资料《QEMU/KVM 源码解析与应用》李强，机械工业出版社QOM 设备管理机制概述The QEMU Object Model (QOM)
前言QEMU 系统模拟模式能够提供运行虚拟机所需的完整环境，包括 CPU，内存和外部设备等。其中，设备模拟是系统模拟模式的重要组成部分，也是系统模拟模式和用户模拟模式的本质区别之一。设备模型是 QEMU 中用于描述和实现设备模拟的软件结构和框架，它为设备提供了标准化的接口和方法，使得开发者可以轻松地为 QEMU 添加新的设备。本文将以 8.0.0 版本的 QEMU RISC-V (qemu-system-riscv64) 为例，分析介绍 QEMU 设备模型的生命周期。设备类型注册定义QEMU 模拟的每一种设备都必须定义对应的设备类型，这个设备类型在代码中由结构体 TypeInfo 描述：/* include/qom/object.h: 412 */ 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; };该结构体的成员变量比较繁多，目前只需要关注以下两个成员：name： 设备类型名称parent： 父设备类型名称通过这两个变量我们可以发现，QEMU 设备模型中使用名称作为设备类型的唯一标识，并且设备类型之间还存在继承关系。注册完成设备类型的定义后，必须注册这一设备类型，以通知 QEMU 系统可以支持这一类型的设备，为后续添加这一类型的设备准备条件。注册工作由 type_register 函数负责：/* qom/object.c: 140 */ static TypeImpl *type_register_internal(const TypeInfo *info) { TypeImpl *ti; ti = type_new(info); type_table_add(ti); return ti; } TypeImpl *type_register(const TypeInfo *info) { assert(info->parent); return type_register_internal(info); }该函数通过调用 type_new 生成一个 TypeInfo 对应的 TypeImpl 类型，并以 name 为关键字添加到名为 type_table 的一个哈希表中。type_table 是 QEMU 维护的一个全局的类型哈希表，使用类型名字符串索引到具体的 TypeImpl 对象。下面给出 TypeImpl 结构体的定义：/* qom/object.c: 48 */ 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 对象系统的设计理念有关，TypeInfo 结构体是面向 API 使用者的一个工具，使用者只有在注册设备类型的时候会使用到 TypeInfo，提供类型注册所需的必要信息和回调函数，此后 QEMU 会根据 TypeInfo 自动生成对应的 TypeImpl 结构体，至此 TypeInfo 的生命周期就结束了。换句话说，TypeInfo 保存静态的注册数据，而 TypeImpl 保存动态的运行数据，这种设计降低了系统的耦合度，提升了稳定性。下面我们来分析 type_register() 函数的调用时机，这里就需要引申出另一个问题，就是注册函数本身也有 “注册 - 执行” 机制。我们以虚拟网卡设备 e1000 为例进行分析：/* hw/e1000.c: 1822 */ static void e1000_register_types(void) { int i; type_register_static(&e1000_base_info); for (i = 0; i < ARRAY_SIZE(e1000_devices); i++) { const E1000Info *info = &e1000_devices[i]; TypeInfo type_info = {}; type_info.name = info->name; type_info.parent = TYPE_E1000_BASE; type_info.class_data = (void *)info; type_info.class_init = e1000_class_init; type_register(&type_info); } } type_init(e1000_register_types)函数 e1000_register_types 负责执行 e1000 网卡的设备类型注册，注意最后一行代码调用 type_init 函数对 e1000_register_types 函数进行注册，下面给出 type_init 函数的定义：/* include/qemu/module.h: 56 */ #define type_init(function) module_init(function, MODULE_INIT_QOM)type_init 函数实际上是通过宏的形式对 module_init 进行包装：/* include/qemu/module.h: 35 */ #define module_init(function, type) \ static void __attribute__((constructor)) do_qemu_init_ ## function(void) \ { \ register_module_init(function, type); \ }这里我们需要关注的是这个修饰 __attribute__((constructor))，它是 GCC 的一个扩展，用于确保被修饰的函数在 main 函数执行之被调用。仍然以上文的 e1000 网卡为例，调用 type_init 时传入的函数名为 e1000_register_types，这个宏的作用就是生成函数 static void do_qemu_init_e1000_register_types(void)，并保证其在 main 函数之前被调用，以达到自动初始化的目的。下面分析 register_module_init 函数的行为：/* util/module.c: 70 */ 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); }QEMU 维护了一个全局的链表头数组 init_type_list，分别指向不同类型的注册函数链表，具体如下图所示：register_module_init 函数会将待注册的设备类型注册函数，这里的例子中是 e1000_register_types，添加到 MODULE_INIT_QOM 下标所指向的链表尾部，完成注册工作。直到此时，真正的设备类型注册函数都没有执行，到底什么时候进行设备类型的注册呢？在之前的文章中，我们有分析过 QEMU 主事件循环的一般执行流程，main 函数会调用 qemu_init 函数进行初始化，qemu_init 在执行过程中会调用 qemu_init_subsystems 函数，而 qemu_init_subsystems 则会调用 module_call_init 函数，并传入参数 MODULE_INIT_QOM：/* util/module.c: 755 */ void module_call_init(module_init_type type) { ModuleTypeList *l; ModuleEntry *e; if (modules_init_done[type]) { return; } l = find_type(type); QTAILQ_FOREACH(e, l, node) { e->init(); } modules_init_done[type] = true; }这里 module_call_init 函数作用就是找到 MODULE_INIT_QOM 对应的链表，然后依次执行链表项中的 init 成员函数，对于 e1000 网卡而言就是之前通过 register_module_init 注册的 e1000_register_types 函数。到此为止，设备类型注册的逻辑链条就算完整了。设备类型初始化设备类型注册后，在使用之前需要初始化该类型，并生成对应的 ObjectClass 对象。这里回顾一下上文介绍的 TypeImpl 结构体，可以发现其中有一个名为 class 的 ObjectClass 类型的成员变量，所谓初始化其实就是初始化这个 class 成员，负责具体初始化工作的是 type_initialize 函数：/* qom/object.c: 286 */ 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_base_init 和自己的 class_init 完成初始化工作。此外，通过上述代码我们还可以得出两点结论：QEMU 设备管理是一个树型结构QEMU 设备模型采用了面向对象的思想，存在继承关系本文重点关注设备的生命周期，以上两点这里不展开讨论，在后面的文章中会专题分析。设备实例化完成了设备类型的初始化，接着就要实例化设备了，这个过程由 object_initialize 函数实现，而 object_initialize 又通过调用 object_initialize_with_type 完成实例化工作。抽象地看，设备实例化主要完成三件事：首先，建立 Object 和 ObjectClass 之间的关联；然后，递归调用父类型的 instance_init 函数；最后，调用自己的 instance_init 函数。需要注意的是，这种抽象的视角简化了设备实例化流程，也模糊了很多真实场景下的具体细节。实例化函数下面以 DeviceClass 为例详细分析设备的实例化过程。DeviceClass 是 QEMU 设备模型的核心组件，它继承自 ObjectClass，提供了设备的基本行为和属性的模板，是很多设备的父类，其实例化过程很有代表性。下图给出了 DeviceClass 有关类和对象之间的关系：这里我们需要关注的是负责实例化 DeviceClass 的函数 device_initfn：/* hw/core/qdev.c: 652 */ static void device_initfn(Object *obj) { DeviceState *dev = DEVICE(obj); if (phase_check(PHASE_MACHINE_READY)) { dev->hotplugged = 1; qdev_hot_added = true; } dev->instance_id_alias = -1; dev->realized = false; dev->allow_unplug_during_migration = false; QLIST_INIT(&dev->gpios); QLIST_INIT(&dev->clocks); }device_initfn 函数的整体执行逻辑比较简单，主要分为以下四个步骤：设备状态转换： 将传入的 Object 指针转换为 DeviceState 指针。DEVICE 是一个宏，用于从基类 Object 转换到派生类 DeviceState检查机器准备状态： 使用 phase_check 函数检查当前的机器初始化阶段是否为 PHASE_MACHINE_READY。如果是，则设置设备的 hotplugged 属性为 1，表示该设备是在虚拟机已经启动后热插入的。同时，全局变量 qdev_hot_added 被设置为 true，表示有设备被热插入初始化设备属性：instance_id_alias 被设置为 -1，表示没有别名；realized 被设置为 false，表示设备尚未实现；allow_unplug_during_migration 被设置为 false，表示在迁移期间不允许拔出此设备初始化设备的 GPIO 和时钟列表： 使用 QLIST_INIT 宏初始化了设备的 GPIO 和时钟列表realized 属性设置我们注意到 device_initfn 函数将 DeviceState 的 realized 设置为 false，意味着这个设备还没有被实现，那么这个属性究竟是什么时候被设置为 true 的呢？通过分析 QEMU 的启动流程，可以梳理出如下函数调用链：QEMU 在初始化阶段经过一系列函数调用，最终调用 qdev_realize 函数，该函数是 QEMU 设备模型中的一个核心函数，用于激活设备，也就是将设备对应的 DeviceState 结构的 realized 属性设置为 true，表示设备已经被实例化并准备好在模拟环境中运行。在设置 realized 属性之前，函数会执行一些基础的检查，以确保设备尚未被实例化并且没有父总线，然后函数会检查设备总线类型并设置父总线，最后调用 object_property_set_bool 函数设置 realized 属性为 true，完成设备实例化。设备销毁设备销毁的步骤与设备创建过程正好相反，一般包含以下四个步骤：设备反实例化： 停止操作设备并释放占用资源后，通过代码 object_property_set_bool(OBJECT(dev), "realized", false, &error) 把 realized 属性设置为 false，标记设备已经反实例化从总线中移除： 设备通常连接到一个总线，在销毁设备之前，需要将其从父总线中移除销毁设备对象： 调用 object_unref 函数减少设备对象的引用计数，当引用计数降为 0 时，并调用设备对象的析构函数，释放所占内存空间及其他资源反注册设备类型： 有时可能还需要反注册设备类型，注销回调函数，清理释放为设备分配的各种资源总而言之，设备的销毁过程可以视作设备添加过程的逆操作，除了顺序相反之外，执行流程基本相似，设备销毁的细节也不是本文关注的重点，这里仅介绍主要步骤，不再详细分析。总结本文从设备的生命周期这一角度入手，介绍了设备类型注册、设备类型初始化、设备实例化等环节的基本原理和执行流程，打通了从设备类型表创建到设备销毁的逻辑链条，在一个较为抽象的层面对 QEMU 设备模型进行了初步分析。下一篇文章中，我们将专题介绍 QEMU 对象模型以及面向对象的设备管理方法。参考资料The QEMU Object Model (QOM)understanding_qemu浅谈 QEMU 的对象系统《QEMU/KVM 源码解析与应用》李强，机械工业出版社
前言在上一篇文章中，我们详细分析了 QEMU 事件循环机制的基本组成，包括事件源、状态机回调函数以及 poll 机制，介绍了 QEMU 主事件循环的运行流程，本文将在此基础上进一步深入，以 8.0.0 版本的 QEMU RISC-V (qemu-system-riscv64) 为例，分析 QEMU 下半部（Bottom Halves，BH）机制。在操作系统内核中，下半部是一个延迟执行的函数，在中断上下文之外运行。在 QEMU 中，下半部用于异步事件处理，QEMU 下半部机制利用事件循环机制，向其他模块提供异步调用的接口。因为其它模块的运行可能不在主线程中，但它想把自己注册的函数加入到 QEMU 主线程中运行，这时就可以使用下半部机制。QEMU 下半部功能在一个事件循环中实现，当下半部函数注册到一个事件循环中，这个事件循环在下一次 poll 的时候，就会调用该函数，从而实现异步延迟调用的效果。数据结构QEMU 将下半部机制所有必要信息（关联事件源、名称、回调函数和状态）进行抽象，封装为 QEMUBH 结构体：/* util/async.c: 61 */ struct QEMUBH { AioContext *ctx; const char *name; QEMUBHFunc *cb; void *opaque; QSLIST_ENTRY(QEMUBH) next; unsigned flags; };下面逐一分析该结构体各成员变量的含义与作用：AioContext *ctx： 指向 AioContext 的指针，代表与下半部关联的异步 I/O 上下文。这允许 QEMU 在不同的上下文中处理不同的事件const char *name： 下半部名称QEMUBHFunc *cb： 指向回调函数的指针，当下半部被触发时，这个函数会被调用，这个回调函数通常会处理与下半部关联的事件void *opaque： 指向数据的指针，它可以在回调函数中使用，一般用于为回调函数提供参数或者额外的上下文数据。QSLIST_ENTRY(QEMUBH) next： 一个用于将 QEMUBH 结构体插入到单向链表的宏unsigned flags： 用于存储底部半部的状态和属性的标志位，表示底部半部是否已经被调度或是否已经被删除其中，flags 标志一共有五种：/* util/async.c: 44 */ enum { /* Already enqueued and waiting for aio_bh_poll() */ BH_PENDING = (1 << 0), /* Invoke the callback */ BH_SCHEDULED = (1 << 1), /* Delete without invoking callback */ BH_DELETED = (1 << 2), /* Delete after invoking callback */ BH_ONESHOT = (1 << 3), /* Schedule periodically when the event loop is idle */ BH_IDLE = (1 << 4), };上述五种标志分别对应如下含义：BH_PENDING： 表示下半部已经入队并等待 aio_bh_poll() 的处理，当一个下半部被调度但尚未执行时，设置为该状态BH_SCHEDULED： 表示下半部的回调函数应该被调用，当下半部被调度并准备执行其回调函数时，设置为该状态BH_DELETED： 表示下半部应该被删除，但在删除之前不应该调用其回调函数，这通常在下半部不再需要时使用以确保其回调函数不被执行BH_ONESHOT： 表示下半部应该在调用其回调函数后被删除，一般用于标示只需要执行一次的下半部BH_IDLE： 表示下半部应该在事件循环空闲时被周期性地调度，一般用于标示需要在没有其他活动时执行的任务很有用为什么需要设置 flag 标志？因为 QEMU 下半部的注册和执行是异步的，因此需要有一种方法用来通知执行者下半部应该怎样执行。flag 标志的设置提供了一种方式来控制和管理下半部的行为。例如，可以组合使用这五种标志来确定是否应该执行下半部的回调函数，或者下半部是否应该在执行后被删除。操作挂载QEMU 下半部在一个事件循环中实现，因此新建下半部需要指出基于哪个 AioContext，同时要指定下半部中需要执行什么函数以及函数参数，下半部的创建由 aio_bh_new_full 函数具体执行：/* util/async.c: 139 */ QEMUBH *aio_bh_new_full(AioContext *ctx, QEMUBHFunc *cb, void *opaque, const char *name) { QEMUBH *bh; bh = g_new(QEMUBH, 1); *bh = (QEMUBH){ .ctx = ctx, .cb = cb, .opaque = opaque, .name = name, }; return bh; }下半部创建后需要挂载到相应 AioContext 的下半部列表中，该操作由 aio_bh_enqueue 完成：/* util/async.c: 71 */ static void aio_bh_enqueue(QEMUBH *bh, unsigned new_flags) { AioContext *ctx = bh->ctx; unsigned old_flags; /* * Synchronizes with atomic_fetch_and() in aio_bh_dequeue(), ensuring that * insertion starts after BH_PENDING is set. */ old_flags = qatomic_fetch_or(&bh->flags, BH_PENDING | new_flags); if (!(old_flags & BH_PENDING)) { /* * At this point the bottom half becomes visible to aio_bh_poll(). * This insertion thus synchronizes with QSLIST_MOVE_ATOMIC in * aio_bh_poll(), ensuring that: * 1. any writes needed by the callback are visible from the callback * after aio_bh_dequeue() returns bh. * 2. ctx is loaded before the callback has a chance to execute and bh * could be freed. */ QSLIST_INSERT_HEAD_ATOMIC(&ctx->bh_list, bh, next); } aio_notify(ctx); /* * Workaround for record/replay. * vCPU execution should be suspended when new BH is set. * This is needed to avoid guest timeouts caused * by the long cycles of the execution. */ icount_notify_exit(); }一个 AioContext 上可以有多个下半部挂载，下半部通过 AioContext 的 bh_list 成员以及自身数据结构中的 next 指针被维护成一个单向链表，如下图所示：QEMU 主线程有默认的事件循环 qemu_aio_context，将下半部挂载到主线程的事件循环可以调用封装好的内部接口 qemu_bh_new：/* include/qemu/main-loop.h: 390 */ #define qemu_bh_new(cb, opaque) \ qemu_bh_new_full((cb), (opaque), (stringify(cb)))一般 QEMU 下半部的执行方式是调用 qemu_bh_schedule 函数，将 flag 标志设置为 BH_SCHEDULED，即该下半部应该被计划执行：/* util/async.c: 199 */ void qemu_bh_schedule(QEMUBH *bh) { aio_bh_enqueue(bh, BH_SCHEDULED); }通过这种方式执行的下半部在执行一次以后不会被删除，因此下一次调度执行不需要重新注册。而通过 aio_bh_schedule_oneshot_full 接口创建并执行的下半部会在创建后尽快执行一次，然后就被删除：/* util/async.c: 125 */ void aio_bh_schedule_oneshot_full(AioContext *ctx, QEMUBHFunc *cb, void *opaque, const char *name) { QEMUBH *bh; bh = g_new(QEMUBH, 1); *bh = (QEMUBH){ .ctx = ctx, .cb = cb, .opaque = opaque, .name = name, }; aio_bh_enqueue(bh, BH_SCHEDULED | BH_ONESHOT); }观察代码可以发现，aio_bh_schedule_oneshot_full 和 qemu_bh_schedule 函数主要有两处不同：aio_bh_schedule_oneshot_full 函数能够创建新的下半部，并且挂载时的 flag 标志比 qemu_bh_schedule 函数多一个 BH_ONESHOT。通知QEMU 下半部挂载到对应 AioContext 上并不会被立即执行，只有当事件循环的执行线程 poll 到 fd 准备好之后，才会触发回调执行下半部，如果对应 AioContext 的 fd 一直没有准备就绪，那么 poll 就一直不返回，挂载在上面的下半部就会一直得不到执行。为避免这类问题，QEMU 实现了下半部调度接口，用于通知事件循环调度下半部。下半部调度接口通过 eventfd 实现，evenfd 可以用于线程间通信。QEMU 在 aio_context_new 创建 AioContext 时通过调用 event_notifier_init 函数初始化 EventNotifier，将 EventNotifier 中的 rfd 设置 aio_set_event_notifier 为事件循环要监听的 fd。当一个线程把自己的下半部挂载到 AioContext 上后，向 EventNotifier 中的 wfd 写入内容，另一端监听 rfd 的事件循环就会 poll 到其上有事件，然后触发下半部的调度：/* util/async.c: 542 */ AioContext *aio_context_new(Error **errp) { int ret; AioContext *ctx; ... QSLIST_INIT(&ctx->bh_list); ... ret = event_notifier_init(&ctx->notifier, false); ... aio_set_event_notifier(ctx, &ctx->notifier, false, aio_context_notifier_cb, aio_context_notifier_poll, aio_context_notifier_poll_ready); ... }执行事件循环在 poll 到 fd 准备好之后，会调度 QEMU 定制的状态机回调函数 aio_ctx_dispatch，其主要功能是执行 fd 对应的回调函数，但在此之前会先检查 AioContext 上是否挂载有下半部，如果有则会先执行下半部。aio_ctx_dispatch 和 aio_dispatch 函数在上一篇文章中已经详细分析过，本文不再赘述，这里我们重点关注具体落实下半部调度执行的函数 aio_bh_poll：/* util/async.c: 159 */ int aio_bh_poll(AioContext *ctx) { BHListSlice slice; BHListSlice *s; int ret = 0; /* Synchronizes with QSLIST_INSERT_HEAD_ATOMIC in aio_bh_enqueue(). */ QSLIST_MOVE_ATOMIC(&slice.bh_list, &ctx->bh_list); QSIMPLEQ_INSERT_TAIL(&ctx->bh_slice_list, &slice, next); while ((s = QSIMPLEQ_FIRST(&ctx->bh_slice_list))) { QEMUBH *bh; unsigned flags; bh = aio_bh_dequeue(&s->bh_list, &flags); if (!bh) { QSIMPLEQ_REMOVE_HEAD(&ctx->bh_slice_list, next); continue; } if ((flags & (BH_SCHEDULED | BH_DELETED)) == BH_SCHEDULED) { /* Idle BHs don't count as progress */ if (!(flags & BH_IDLE)) { ret = 1; } aio_bh_call(bh); } if (flags & (BH_DELETED | BH_ONESHOT)) { g_free(bh); } } return ret; }aio_bh_poll 函数 QEMU 下半部机制的核心函数，这里首先定义了两个 BHListSlice 变量和一个返回值 ret。BHListSlice 是一个结构，它包含一个 BHList 类型的列表和一个后继指针 next。接着，函数两个宏将当前 AioContext 的 bh_list 列表中所有下半部移动到一个新的 BHListSlice 中，并将这个 BHListSlice 添加到 ctx->bh_slice_list 的尾部。这样做的目的是保证处理下半部时“先来先处理”的顺序，即新添加的下半部总是在最后得到处理。最后，函数使用 while 循环遍历 bh_slice_list 中所有的 BHListSlice，并更进一步地遍历 BHListSlice 中每一个下半部，对于计划执行并且没有被标记删除的下半部，函数会调度执行，对于一次性的或者是被标记删除的下半部，函数会删除它并释放资源。在此过程中，有关数据结构的关系如下图所示：卸载下半部卸载是通过调用 aio_bh_enqueue 函数更新 flag 标志完成的，设置 BH_DELETED 标志能够使下半部在下一次 dispatch 阶段被删掉并释放对应的内存空间：/* util/async.c: 214 */ void qemu_bh_delete(QEMUBH *bh) { aio_bh_enqueue(bh, BH_DELETED); }禁用当我们希望下半部不再被事件循环调度，但又暂时不想删除这个下半部时可以调用 qemu_bh_cancel 函数实现：/* util/async.c: 206 */ void qemu_bh_cancel(QEMUBH *bh) { qatomic_and(&bh->flags, ~BH_SCHEDULED); }这个函数非常简洁，只有一行代码，使用原子操作来清除 bh->flags 中的 BH_SCHEDULED 标志。BH_SCHEDULED 标志表示这个下半部已经被计划执行，清除这个标志，意味着此后事件循环的 dispatch 时这个下半部不会再被调度执行。总结本文在前两篇文章的基础上，进一步分析了 QEMU 下半部机制，下半部利用事件循环机制，向其他模块提供异步调用的接口。文章梳理了下半部机制的主要数据结构以及它们之间的关系，介绍了 QEMU 下半部机制的执行原理，同时还整理了常见下半部操作的接口和用法。参考资料深入理解 QEMU 事件循环QEMU Internals: Event loops《QEMU/KVM 源码解析与应用》李强，机械工业出版社
前言QEMU 采用了基于事件驱动的架构，其事件循环机制基于 glib 实现，上一篇文章介绍了 glib 事件循环机制，本文将在此基础上以 8.0.0 版本的 QEMU RISC-V (qemu-system-riscv64) 为例深入分析 QEMU 事件循环机制。概述QEMU 的事件循环机制是其核心功能之一，用于协调虚拟机的各种异步操作。通过监听和调度各种事件源，它允许 QEMU 有效地响应外部输入、定时器事件等，从而实现高性能的虚拟化。QEMU 的默认事件循环是位于文件 utils/main-loop.c 文件中的主循环 main-loop，我们也可以使用选项 –object iothread, id=<my_iothread_name> 自己创建事件循环。QEMU 事件源尽管 QEMU 的事件循环机制基于 glib 实现，但是 QEMU 没有直接使用 glib 提供的事件源，而是自定义了一个新的事件源 AioContext，主要有以下两种类型：监听各种事件，例如 iohandler_ctx处理块设备层的异步 I/O 请求，例如默认的 qemu_aio_context 或者模块自己创建的 AioContext/* include/block/aio.h: 124 */ struct AioContext { GSource source; /* Used by AioContext users to protect from multi-threaded access. */ QemuRecMutex lock; /* * Keep track of readers and writers of the block layer graph. * This is essential to avoid performing additions and removal * of nodes and edges from block graph while some * other thread is traversing it. */ BdrvGraphRWlock *bdrv_graph; /* The list of registered AIO handlers. Protected by ctx->list_lock. */ AioHandlerList aio_handlers; /* The list of AIO handlers to be deleted. Protected by ctx->list_lock. */ AioHandlerList deleted_aio_handlers; /* Used to avoid unnecessary event_notifier_set calls in aio_notify; * only written from the AioContext home thread, or under the BQL in * the case of the main AioContext. However, it is read from any * thread so it is still accessed with atomic primitives. * * If this field is 0, everything (file descriptors, bottom halves, * timers) will be re-evaluated before the next blocking poll() or * io_uring wait; therefore, the event_notifier_set call can be * skipped. If it is non-zero, you may need to wake up a concurrent * aio_poll or the glib main event loop, making event_notifier_set * necessary. * * Bit 0 is reserved for GSource usage of the AioContext, and is 1 * between a call to aio_ctx_prepare and the next call to aio_ctx_check. * Bits 1-31 simply count the number of active calls to aio_poll * that are in the prepare or poll phase. * * The GSource and aio_poll must use a different mechanism because * there is no certainty that a call to GSource's prepare callback * (via g_main_context_prepare) is indeed followed by check and * dispatch. It's not clear whether this would be a bug, but let's * play safe and allow it---it will just cause extra calls to * event_notifier_set until the next call to dispatch. * * Instead, the aio_poll calls include both the prepare and the * dispatch phase, hence a simple counter is enough for them. */ uint32_t notify_me; /* A lock to protect between QEMUBH and AioHandler adders and deleter, * and to ensure that no callbacks are removed while we're walking and * dispatching them. */ QemuLockCnt list_lock; /* Bottom Halves pending aio_bh_poll() processing */ BHList bh_list; /* Chained BH list slices for each nested aio_bh_poll() call */ QSIMPLEQ_HEAD(, BHListSlice) bh_slice_list; /* Used by aio_notify. * * "notified" is used to avoid expensive event_notifier_test_and_clear * calls. When it is clear, the EventNotifier is clear, or one thread * is going to clear "notified" before processing more events. False * positives are possible, i.e. "notified" could be set even though the * EventNotifier is clear. * * Note that event_notifier_set *cannot* be optimized the same way. For * more information on the problem that would result, see "#ifdef BUG2" * in the docs/aio_notify_accept.promela formal model. */ bool notified; EventNotifier notifier; QSLIST_HEAD(, Coroutine) scheduled_coroutines; QEMUBH *co_schedule_bh; int thread_pool_min; int thread_pool_max; /* Thread pool for performing work and receiving completion callbacks. * Has its own locking. */ struct ThreadPool *thread_pool; #ifdef CONFIG_LINUX_AIO /* * State for native Linux AIO. Uses aio_context_acquire/release for * locking. */ struct LinuxAioState *linux_aio; #endif #ifdef CONFIG_LINUX_IO_URING /* * State for Linux io_uring. Uses aio_context_acquire/release for * locking. */ struct LuringState *linux_io_uring; /* State for file descriptor monitoring using Linux io_uring */ struct io_uring fdmon_io_uring; AioHandlerSList submit_list; #endif /* TimerLists for calling timers - one per clock type. Has its own * locking. */ QEMUTimerListGroup tlg; int external_disable_cnt; /* Number of AioHandlers without .io_poll() */ int poll_disable_cnt; /* Polling mode parameters */ int64_t poll_ns; /* current polling time in nanoseconds */ int64_t poll_max_ns; /* maximum polling time in nanoseconds */ int64_t poll_grow; /* polling time growth factor */ int64_t poll_shrink; /* polling time shrink factor */ /* AIO engine parameters */ int64_t aio_max_batch; /* maximum number of requests in a batch */ /* * List of handlers participating in userspace polling. Protected by * ctx->list_lock. Iterated and modified mostly by the event loop thread * from aio_poll() with ctx->list_lock incremented. aio_set_fd_handler() * only touches the list to delete nodes if ctx->list_lock's count is zero. */ AioHandlerList poll_aio_handlers; /* Are we in polling mode or monitoring file descriptors? */ bool poll_started; /* epoll(7) state used when built with CONFIG_EPOLL */ int epollfd; const FDMonOps *fdmon_ops; };上述结构体较为复杂，下面主要分析几个较为关键的成员变量的含义与作用：GSource source： glib 事件源，每一个 glib 自定义事件源的第一个成员必须是 GSource 结构的成员QemuRecMutex lock： 用于保护 AioContext 的递归互斥锁，防止多线程访问AioHandlerList aio_handlers： 一个链表头，其链表中的数据类型为 AioHandler，所有加入到 AioContext 事件源的文件 fd 的事件处理函数都挂载到该链表上uint32_t notify_me：用于避免在 aio_notify 中进行不必要的 event_notifier_set 调用bool notified：notify_me 和 notified 都与 aio_notify 相关，主要用于在块设备层的 I/O 同步时处理 QEMU 下半部 (Bottom Halvs，BH)BHList bh_list： QEMU 下半部链表，用来连接挂到该事件源的下半部，QEMU 的 BH 默认挂载到 qemu_aio_context 下EventNotifier notifier： 事件通知对象，在块设备进行同步且需要调用 BH 的时候需要用到该成员QEMUTimerListGroup tlg： 管理挂到该事件源的定时器AioContext 拓展了 glib 中事件源的功能，不但支持 fd 的事件处理，还模拟内核中的下半部机制，实现了 QEMU 中的下半部以及定时器的管理。AioContext 可以通过调用 aio_context_new 函数进行创建并初始化：/* util/async.c: 542 */ AioContext *aio_context_new(Error **errp) { int ret; AioContext *ctx; ctx = (AioContext *) g_source_new(&aio_source_funcs, sizeof(AioContext)); QSLIST_INIT(&ctx->bh_list); QSIMPLEQ_INIT(&ctx->bh_slice_list); aio_context_setup(ctx); ret = event_notifier_init(&ctx->notifier, false); if (ret < 0) { error_setg_errno(errp, -ret, "Failed to initialize event notifier"); goto fail; } g_source_set_can_recurse(&ctx->source, true); qemu_lockcnt_init(&ctx->list_lock); ctx->co_schedule_bh = aio_bh_new(ctx, co_schedule_bh_cb, ctx); QSLIST_INIT(&ctx->scheduled_coroutines); aio_set_event_notifier(ctx, &ctx->notifier, false, aio_context_notifier_cb, aio_context_notifier_poll, aio_context_notifier_poll_ready); #ifdef CONFIG_LINUX_AIO ctx->linux_aio = NULL; #endif #ifdef CONFIG_LINUX_IO_URING ctx->linux_io_uring = NULL; #endif ctx->thread_pool = NULL; qemu_rec_mutex_init(&ctx->lock); timerlistgroup_init(&ctx->tlg, aio_timerlist_notify, ctx); ctx->poll_ns = 0; ctx->poll_max_ns = 0; ctx->poll_grow = 0; ctx->poll_shrink = 0; ctx->aio_max_batch = 0; ctx->thread_pool_min = 0; ctx->thread_pool_max = THREAD_POOL_MAX_THREADS_DEFAULT; register_aiocontext(ctx); return ctx; fail: g_source_destroy(&ctx->source); return NULL; }aio_context_new 函数返回一个指向新创建的 AioContext 的指针，并接受一个 Error 指针的指针，用于报告初始化过程中出现的错误。该函数首先调用 g_source_new 等函数完成 AioContext 的创建、设置以及 QEMU 下半部列表的初始化，接着初始化事件通知器及其回调函数、初始化锁并设置协程调度，然后完成线程池、计时器列表、轮询参数等其他内容的初始化工作，最后注册新创建的 AioContext。在 AioContext 的创建函数中，aio_set_event_notifer 函数调用了 aio_set_fd_handler 函数，后者用于添加或者删除 AioContext 事件源中的 fd，如果是添加则会设置 fd 对应的读写函数。添加事件源中 fd 监听处理的具体过程如下：/* util/aio-posix.c: 100 */ void aio_set_fd_handler(AioContext *ctx, int fd, bool is_external, IOHandler *io_read, IOHandler *io_write, AioPollFn *io_poll, IOHandler *io_poll_ready, void *opaque) { AioHandler *node; AioHandler *new_node = NULL; bool is_new = false; bool deleted = false; int poll_disable_change; if (io_poll && !io_poll_ready) { io_poll = NULL; /* polling only makes sense if there is a handler */ } qemu_lockcnt_lock(&ctx->list_lock); node = find_aio_handler(ctx, fd); /* Are we deleting the fd handler? */ if (!io_read && !io_write && !io_poll) { if (node == NULL) { qemu_lockcnt_unlock(&ctx->list_lock); return; } /* Clean events in order to unregister fd from the ctx epoll. */ node->pfd.events = 0; poll_disable_change = -!node->io_poll; } else { poll_disable_change = !io_poll - (node && !node->io_poll); if (node == NULL) { is_new = true; } /* Alloc and insert if it's not already there */ new_node = g_new0(AioHandler, 1); /* Update handler with latest information */ new_node->io_read = io_read; new_node->io_write = io_write; new_node->io_poll = io_poll; new_node->io_poll_ready = io_poll_ready; new_node->opaque = opaque; new_node->is_external = is_external; if (is_new) { new_node->pfd.fd = fd; } else { new_node->pfd = node->pfd; } g_source_add_poll(&ctx->source, &new_node->pfd); new_node->pfd.events = (io_read ? G_IO_IN | G_IO_HUP | G_IO_ERR : 0); new_node->pfd.events |= (io_write ? G_IO_OUT | G_IO_ERR : 0); QLIST_INSERT_HEAD_RCU(&ctx->aio_handlers, new_node, node); } /* No need to order poll_disable_cnt writes against other updates; * the counter is only used to avoid wasting time and latency on * iterated polling when the system call will be ultimately necessary. * Changing handlers is a rare event, and a little wasted polling until * the aio_notify below is not an issue. */ qatomic_set(&ctx->poll_disable_cnt, qatomic_read(&ctx->poll_disable_cnt) + poll_disable_change); ctx->fdmon_ops->update(ctx, node, new_node); if (node) { deleted = aio_remove_fd_handler(ctx, node); } qemu_lockcnt_unlock(&ctx->list_lock); aio_notify(ctx); if (deleted) { g_free(node); } }该函数接受以下 6 个参数：AioContext *ctx： 待添加 fd 的异步 I/O 上下文int fd： 待处理的文件描述符bool is_external： 用于块设备层，指示是否为外部事件，对于事件监听的 fd 都设置为 falseIOHandler *io_read： 读取回调函数IOHandler *io_write： 写入回调函数AioPollFn *io_poll： 轮询回调函数IOHandler *io_poll_ready： 轮询就绪回调函数void *opaque： 传递给回调函数的透明指针aio_set_fd_handler 函数首先调用 find_aio_handler 查找当前事件源中是否已有 fd，对于新加入的情况，这里会创建一个名为 node 的 AioHandler，使用 fd 初始化 node->pfd.fd，并将其插入到 ctx->aio_handlers 链表上，调用 glib 接口函数 g_source_add_poll 将该 fd 插入到了事件源监听 fd 列表中。最后设置 node 的事件读写函数为 io_read 和 io_write，根据 io_read 和 io_write 的有无设置 node->pfd.events，即要监听的事件。在 aio_set_fd_handler 执行完成之后，新的 fd 事件就加入到了事件源的 aio_handlers 链表上了，具体如下图所示：状态机回调函数QEMU 丰富了 glib 的事件源，状态机回调函数的实现逻辑变得更加复杂，但基本框架仍然遵循 glib 规范，aio_context_new 函数中的 aio_source_funcs 是状态机回调函数的声明：/* util/async.c: 389 */ static GSourceFuncs aio_source_funcs = { aio_ctx_prepare, aio_ctx_check, aio_ctx_dispatch, aio_ctx_finalize };其中 aio_ctx_prepare 是状态机 prepare 阶段的回调函数，目的是准备 AioContext 进行轮询，计算超时，并检查是否有即时事件需要处理。如果有即时事件，它会返回 true，这意味着主事件循环应该立即处理这些事件而非等待。/* util/async.c: 267 */ static gboolean aio_ctx_prepare(GSource *source, gint *timeout) { AioContext *ctx = (AioContext *) source; qatomic_set(&ctx->notify_me, qatomic_read(&ctx->notify_me) | 1); /* * Write ctx->notify_me before computing the timeout * (reading bottom half flags, etc.). Pairs with * smp_mb in aio_notify(). */ smp_mb(); /* We assume there is no timeout already supplied */ *timeout = qemu_timeout_ns_to_ms(aio_compute_timeout(ctx)); if (aio_prepare(ctx)) { *timeout = 0; } return *timeout == 0; }aio_ctx_check 是状态机 check 阶段的回调函数，主要负责检查 AioContext 是否有待处理的事件。如果有则返回 true。该函数首先使用原子操作清除 ctx->notify_me 的最低位，这是用于跟踪是否需要通知 AioContext 的标志。接着调用 aio_notify_accept 来处理任何挂起的通知，并通过 QSLIST_FOREACH_RCU 宏遍历 ctx->bh_list 中的所有下半部，如果找到一个已调度但未删除的下半部，函数返回 true。然后，使用 QSIMPLEQ_FOREACH 和 QSLIST_FOREACH_RCU 宏遍历 ctx->bh_slice_list 中的所有下半部，如果在任何切片中找到一个已调度但未删除的下半部，函数返回 true。最后，使用 aio_pending 函数检查是否有其他挂起的事件。此外，函数还会检查 timerlistgroup_deadline_ns 的返回值是否为 0，这意味着有一个立即的定时器事件需要处理。/* util/async.c: 291 */ static gboolean aio_ctx_check(GSource *source) { AioContext *ctx = (AioContext *) source; QEMUBH *bh; BHListSlice *s; /* Finish computing the timeout before clearing the flag. */ qatomic_store_release(&ctx->notify_me, qatomic_read(&ctx->notify_me) & ~1); aio_notify_accept(ctx); QSLIST_FOREACH_RCU(bh, &ctx->bh_list, next) { if ((bh->flags & (BH_SCHEDULED | BH_DELETED)) == BH_SCHEDULED) { return true; } } QSIMPLEQ_FOREACH(s, &ctx->bh_slice_list, next) { QSLIST_FOREACH_RCU(bh, &s->bh_list, next) { if ((bh->flags & (BH_SCHEDULED | BH_DELETED)) == BH_SCHEDULED) { return true; } } } return aio_pending(ctx) || (timerlistgroup_deadline_ns(&ctx->tlg) == 0); }aio_ctx_dispatch 是状态机 dispatch 阶段的回调函数，整体逻辑较为简单，在确保参数 callback 不为空的情况下直接调用 aio_dispatch 函数进行处理。/* util/async.c: 318 */ static gboolean aio_ctx_dispatch(GSource *source, GSourceFunc callback, gpointer user_data) { AioContext *ctx = (AioContext *) source; assert(callback == NULL); aio_dispatch(ctx); return true; }aio_dispatch 函数是所有事件处理函数中最为重要的一个，主要负责事件的分派处理，具体负责：下半部处理处理文件 fd 列表中有事件的 fd调用定时器到期的函数aio_dispatch 函数代码对上述三个行为进行了封装：/* util/aio-posix.c: 418 */ void aio_dispatch(AioContext *ctx) { qemu_lockcnt_inc(&ctx->list_lock); aio_bh_poll(ctx); aio_dispatch_handlers(ctx, INVALID_HANDLE_VALUE); qemu_lockcnt_dec(&ctx->list_lock); timerlistgroup_run_timers(&ctx->tlg); }这里我们重点关注 aio_dispatch_handlers 函数的执行逻辑：/* util/aio-posix.c: 406 */ static bool aio_dispatch_handlers(AioContext *ctx, HANDLE event) { AioHandler *node; bool progress = false; AioHandler *tmp; /* * We have to walk very carefully in case aio_set_fd_handler is * called while we're walking. */ QLIST_FOREACH_SAFE_RCU(node, &ctx->aio_handlers, node, tmp) { int revents = node->pfd.revents; if (!node->deleted && (revents || event_notifier_get_handle(node->e) == event) && node->io_notify) { node->pfd.revents = 0; node->io_notify(node->e); /* aio_notify() does not count as progress */ if (node->e != &ctx->notifier) { progress = true; } } if (!node->deleted && (node->io_read || node->io_write)) { node->pfd.revents = 0; if ((revents & G_IO_IN) && node->io_read) { node->io_read(node->opaque); progress = true; } if ((revents & G_IO_OUT) && node->io_write) { node->io_write(node->opaque); progress = true; } /* if the next select() will return an event, we have progressed */ if (event == event_notifier_get_handle(&ctx->notifier)) { WSANETWORKEVENTS ev; WSAEnumNetworkEvents(node->pfd.fd, event, &ev); if (ev.lNetworkEvents) { progress = true; } } } if (node->deleted) { if (qemu_lockcnt_dec_if_lock(&ctx->list_lock)) { QLIST_REMOVE(node, node); g_free(node); qemu_lockcnt_inc_and_unlock(&ctx->list_lock); } } } return progress; }aio_dispatch_handlers 函数的行为主要分为 4 个步骤：使用 QLIST_FOREACH_SAFE_RCU 宏安全地遍历 ctx->aio_handlers 列表，检查监听 fd 上的事件是否发生。fd 发生的事件存在 node->pfd.revents 中，注册时指定需要接受的事件存放在 node->pfd.events 中，revents 变量保存了 fd 接收到的事件。处理通知，如果事件没有被删除，其事件标志被设置，且有一个通知回调函数，那么该回调函数将被调用。处理读写事件，对应 G_IO_IN 可读事件来说，会调用注册的 fd 的 io_read 回调，对 G_IN_OUT 可写事件来说，会调用注册的 fd 的 io_write 函数。如果当前的 fd 已经被标记删除了，则会删除这个节点并释放锁。aio_ctx_finalize 函数是事件源不再被引用时的回调函数，其主要目的是确保与 AioContext 关联的所有资源都被正确地清理和释放，以防止发生内存泄漏等问题。aio_ctx_finalize 函数的整体逻辑相对而言比较简单，这里不再赘述。poll 机制应用程序对文件描述符的读写，一般有两种方式：阻塞式 IO： 如果 fd 的某个状态（比如可读，可写）没有准备就绪，进程将永远阻塞在这个 IO 上非阻塞式 IO： 进程 IO 立即执行，如果 fd 没有准备就绪，直接返回错误，表示如果继续执行 IO 将阻塞。通常来说，上述两种方式能够满足大多数应用程序对文件描述符的读写需求，但是在更复杂的场景下，比如一个不允许阻塞的进程需要读写一个可能永远阻塞进程的 fd，这时就需要使用内核提供的 poll 机制。poll 机制不直接读写 IO，而是先探测 fd 的 IO 状态是否就绪。这个探测行为可以设置超时时间：如果在超时时间到达前 IO 状态准备就绪，poll 接口立即返回，指示应用程序可以进行读写操作；如果 poll 超过了设置的超时时间，但应用需要的 IO 状态仍未完全就绪，poll 接口也会返回，同时把已经就绪的部分 IO 状态返回。这种机制使得应用程序能够根据内核返回的 poll 信息确定下一步的操作，避免应用程序被永远阻塞在某一 IO 上。QEMU 基于 glib 事件循环的框架，定制了 glib 事件循环机制中几乎所有的接口。需要注意的是，QEMU 并没有调用 glib 的接口 g_main_loop_run 执行事件循环，而是直接调用了 glib 提供的 g_poll 函数实现 poll 的功能。g_poll 函数具有可移植性，在有 poll 接口的平台上由 poll 模拟，在没有 poll 接口的平台上由 select 模拟。当用户不想执行整个 glib 事件循环，又想实现阻塞一段时间的高级 IO，就可以通过直接调用 g_poll 实现。QEMU 主事件循环的具体逻辑由函数 qemu_main_loop 体现：/* softmmu/runstate.c: 720 */ int qemu_main_loop(void) { int status = EXIT_SUCCESS; #ifdef CONFIG_PROFILER int64_t ti; #endif while (!main_loop_should_exit(&status)) { #ifdef CONFIG_PROFILER ti = profile_getclock(); #endif main_loop_wait(false); #ifdef CONFIG_PROFILER dev_time += profile_getclock() - ti; #endif } return status; }观察上述代码我们可以发现，QEMU 使用了一条 while 语句循环调用 main_loop_wait 函数，再通过一串函数调用链最终调用 g_poll 接口。g_poll 接口接受的参数是一个文件描述符的数组，因此它能够同时 poll 多个 fd，poll 的时候是休眠的，一旦其中一个 fd 准备好，poll 就会被唤醒，同时 GPollFD.revent 会被内核设置，以表明 fd 的状态。QEMU 对 g_poll 接口的封装实现除了以纳秒为单位计算超时之外，其他方面均与 glib 的 g_poll 接口保持一致：/* util/qemu-timer.c: 335 */ int qemu_poll_ns(GPollFD *fds, guint nfds, int64_t timeout) { #ifdef CONFIG_PPOLL if (timeout < 0) { return ppoll((struct pollfd *)fds, nfds, NULL, NULL); } else { struct timespec ts; int64_t tvsec = timeout / 1000000000LL; /* Avoid possibly overflowing and specifying a negative number of * seconds, which would turn a very long timeout into a busy-wait. */ if (tvsec > (int64_t)INT32_MAX) { tvsec = INT32_MAX; } ts.tv_sec = tvsec; ts.tv_nsec = timeout % 1000000000LL; return ppoll((struct pollfd *)fds, nfds, &ts, NULL); } #else return g_poll(fds, nfds, qemu_timeout_ns_to_ms(timeout)); #endif }如果 QEMU 配置了 PPOLL，qemu_poll_ns 函数会使用 PPOLL 实现 poll 探测，否则直接调用 g_poll 函数实现 poll 探测。主事件循环一般流程QEMU 事件循环执行流程遵循 glib 标准，与普通应用的执行流程类似，相关函数的调用关系如下图所示：QEMU 的 main 函数定义在 softmmu/ vl.c 文件中，主函数在调用 qemu_init() 完成所有初始化工作之后会直接跳转函数 qemu_main 开始执行主事件循环。初始化QEMU 主事件循环初始化由 qemu_init_main_loop 函数负责：/* util/main-loop.c: 156 */ int qemu_init_main_loop(Error **errp) { int ret; GSource *src; init_clocks(qemu_timer_notify_cb); ret = qemu_signal_init(errp); if (ret) { return ret; } qemu_aio_context = aio_context_new(errp); if (!qemu_aio_context) { return -EMFILE; } qemu_set_current_aio_context(qemu_aio_context); qemu_notify_bh = qemu_bh_new(notify_event_cb, NULL); gpollfds = g_array_new(FALSE, FALSE, sizeof(GPollFD)); src = aio_get_g_source(qemu_aio_context); g_source_set_name(src, "aio-context"); g_source_attach(src, NULL); g_source_unref(src); src = iohandler_get_g_source(); g_source_set_name(src, "io-handler"); g_source_attach(src, NULL); g_source_unref(src); return 0; }观察上述代码可以发现，QEMU 事件初始化通过 glib 接口封装出本文开头所介绍的两种自定义事件源 qemu_aio_context 和 iohandler_ctx，然后将它们加入到 glib 默认的事件循环 default GMainContext 中。此外，一个上下文只在一个线程中运行，而 qemu_aio_context 和 iohandler_ctx 属于同一个上下文，因此可以运行在同一线程中。关键函数在上述函数调用关系图中，较为关键的是 os_host_main_loop_wait 函数，具体执行对 fd 的监听行为。需要注意的是，main_loop_wait 在调用 os_host_main_loop_wait 函数前，会调用 qemu_soonest_timeout 函数先计算一个最小的 timeout 值，该值是从定时器列表中获取的，表示监听事件的时候最多让主事件循环阻塞的时间，timeout 的存在使得 QEMU 能够及时处理系统中的定时器到期事件。os_host_main_loop_wait 函数定义位于 util/main-loop.c 文件中，下面针对代码进行具体分析：/* util/main-loop.c: 296 */ static int os_host_main_loop_wait(int64_t timeout) { GMainContext *context = g_main_context_default(); int ret; g_main_context_acquire(context); glib_pollfds_fill(&timeout); qemu_mutex_unlock_iothread(); replay_mutex_unlock(); ret = qemu_poll_ns((GPollFD *)gpollfds->data, gpollfds->len, timeout); replay_mutex_lock(); qemu_mutex_lock_iothread(); glib_pollfds_poll(); g_main_context_release(context); return ret; }在上述代码中，我们需要重点关注以下三个函数：glib_pollfds_fill： 检索事件源中待监控的 fd 集合qemu_poll_ns： QEMU 对 g_poll 接口的封装实现，以纳秒为单位计算超时时间glib_pollfds_poll： 有事件发生，准备开始执行回调其中 qemu_poll_ns 函数上文已经分析过，这里重点关注另外两个函数。/* util/main-loop.c: 255 */ static void glib_pollfds_fill(int64_t *cur_timeout) { GMainContext *context = g_main_context_default(); int timeout = 0; int64_t timeout_ns; int n; g_main_context_prepare(context, &max_priority); glib_pollfds_idx = gpollfds->len; n = glib_n_poll_fds; do { GPollFD *pfds; glib_n_poll_fds = n; g_array_set_size(gpollfds, glib_pollfds_idx + glib_n_poll_fds); pfds = &g_array_index(gpollfds, GPollFD, glib_pollfds_idx); n = g_main_context_query(context, max_priority, &timeout, pfds, glib_n_poll_fds); } while (n != glib_n_poll_fds); if (timeout < 0) { timeout_ns = -1; } else { timeout_ns = (int64_t)timeout * (int64_t)SCALE_MS; } *cur_timeout = qemu_soonest_timeout(timeout_ns, *cur_timeout); }glib_pollfds_fill 函数的主要工作是获取所有需要进行监听的 fd，并且计算一个最小的超时时间。该函数首先调用 g_main_context_prepare 开始为主循环的监听做准备。接着在一个循环中调用 g_main_context_query 获取需要监听的 fd，所有 fd 保存在全局变量 gpollfds 数组中，需要监听的 fd 的数量保存在 glib_n_poll_fds 中，g_main_context_query 还会返回 fd 时间最小的 timeout，该值用来与传入的参数 cur_timeout 进行比较，选取较小值表示主循环的最大阻塞时间。/* util/main-loop.c: 284 */ static void glib_pollfds_poll(void) { GMainContext *context = g_main_context_default(); GPollFD *pfds = &g_array_index(gpollfds, GPollFD, glib_pollfds_idx); if (g_main_context_check(context, max_priority, pfds, glib_n_poll_fds)) { g_main_context_dispatch(context); } }glib_pollfds_poll 函数负责对事件进行分发处理，它的代码结构比较简单，首先调用 glib 框架的 g_main_context_check 检测事件，然后再调用 g_main_context_dispatch 对事件进行分发处理。事件处理signalfd 是 Linux 的一个系统调用，可以将特定的信号与一个 fd 绑定，当有信号到达的时 fd 就会产生对应的可读事件。下面以 signalfd 为例梳理总结 QEMU 事件处理的过程。初始化阶段主要逻辑如下：调用 qemu_signal_init 将一个 fd 与一组信号关联起来，qemu_signal_init 调用 qemu_set_fd_handler 函数将该 signalfd 对应的可读回调函数设置为 sigfd_handlerqemu_set_fd_handler 在首次调用时会调用 iohandler_init 创建一个全局的 iohandler_ctx 事件源，这个事件源的作用是监听 QEMU 中的各类事件qemu_signal_init 在 iohandlers_ctx 的 aio_handlers 上挂载一个 AioHandler 节点，其 fd 为 signalfd，其 io_read 函数为 sigfd_handler调用 aio_context_new 创建一个全局的 qemu_aio_context 事件源，这个事件源主要用于处理 BH 和块设备层的同步使用调用 aio_get_g_source 和 iohandler_get_g_source 分别获取 qemu_aio_context 和 iohandler_ctx 的 GSource 成员变量，以该变量为参数调用 g_source_attach 两个 AioContext 加入主循环将信号对应的 fd 加入事件源并将事件源加入主循环之后，QEMU 就会进入上文所述的 qemu_main_loop 函数循环进行事件监听。当使用 kill 命令向 QEMU 进程发送 SIGALARM 信号时，signalfd 就会有可读信号，从而导致主循环返回调用 g_main_context_dispatch 进行事件分发，这会调用 aio_ctx_dispatch，最终会调用 qemu_signal_init 注册的可读处理函数 sigfd_handler 进行事件处理。总结本文在 glib 事件循环机制的基础上，介绍了 QEMU 事件循环机制的基本组成，包括事件源、状态机回调函数以及 poll 机制，详细分析了 QEMU 主事件循环的运行原理，并以 signalfd 为例梳理了 QEMU 事件处理流程。在下一篇文章中，我们将继续深入分析 QEMU 下半部（Bottom Halvs，BH）机制。参考资料深入理解 QEMU 事件循环QEMU 事件循环机制QEMU Internals: Event loops《QEMU/KVM 源码解析与应用》李强，机械工业出版社
前言QEMU 采用了基于事件驱动的架构，其事件循环机制基于 glib 实现，因此 QEMU 中有大量的概念来源于 glib，在深入分析 QEMU 事件循环机制之前先了解 glib 事件循环的基本原理有助于增进我们对 QEMU 事件循环机制的理解，本文将基于 glib v2.25.7 源码介绍 glib 事件循环机制。概述glib 是一个跨平台的、使用 C 语言编写的若干底层库的集合，它实现了完整的事件循环分发机制，在这个机制中有一个主事件循环负责管理所有可用事件源。这些事件可以来自多种不同类型的源，如文件描述符（文件、管道或套接字）和超时，我们还可以通过调用 g_source_attach() 函数添加新类型的事件源。glib 使用 GMainLoop 结构体来表示一个主事件循环，一个 GMainLoop 由 g_main_loop_new() 函数创建，添加初始事件源后，调用 g_main_loop_run() 函数运行，该函数将持续检查每个事件源的新事件，并将其派发出去，直到处理完其中一个事件源的事件后，调用 g_main_loop_quit() 函数退出主循环，g_main_loop_run() 函数才会返回。GMainContext 用于表示主事件循环的上下文，每一个 GMainLoop 都有一个对应的上下文 GMainContext，它封装了事件循环的状态和行为。事件源使用 GSource 表示，每个 GSource 可以关联多个文件描述符，为了能够在不同线程中处理多组独立的事件源，每个 GSource 会关联到一个 GMainContext，而一个 GMainContext 可以关联多个 GSource，具体关系如下图所示。需要注意的是，GMainContext 只能在单线程中运行，但是事件源可以由其他线程添加或删除。因此，所有对 GMainContext 以及内置的 GSource 进行操作的函数都是线程安全的。glib 中每个事件源都有一个优先级，默认优先级 G_PRIORITY_DEFAULT 为 0，大于 0 表示优先级较低。来自高优先级事件源的事件总是先于来自低优先级事件源的事件得到处理。此外，还可以添加空闲函数（idle functions），并为其分配优先级，当没有来自更高优先级事件源的事件需要处理时，空闲函数就会运行。状态机glib 的核心是 poll 机制，通过 poll 检查用户注册的事件源，并执行对应的回调，用户不需要关注其具体实现，只需要按照要求注册对应的事件源和回调。glib 事件循环机制管理所有注册的事件源，不同事件源可以在一个线程中处理，也可以在不同线程中处理，这取决于事件源所在的上下文，一个上下文只能运行在一个线程中，如果想要事件源在不同线程中并发被处理，可以将其放在不同的上下文中。glib 中一次主循环分为 4 个阶段：prepare、query、check 和 dispatch，其状态转移如下图所示：上述状态机中的四个阶段分别对应以下四个函数：prepare：gboolean g_main_context_prepare(GMainContext *context, gint *priority)该函数接受两个参数：context： 主循环的上下文，若为 NULL，则使用默认上下文priority： 用于返回准备好的事件源的最高优先级函数执行过程中会循环遍历所有事件源，调用相应事件源的 prepare 函数检查事件源是否准备好被分派。如果准备好了，设置相应事件源的 G_SOURCE_READY 标志。此外，该函数还会计算下一个超时事件的时间。如果存在准备好的事件源，超时将被设置为 0，函数返回 TRUE，否则待超时后直接返回 FALSE。query：gint g_main_context_query(GMainContext *context, gint max_priority, gint *timeout_, GPollFD *fds, gint n_fds)该函数接受五个参数：context： 主循环的上下文max_priority： 要查询的最大优先级timeout： 返回下一个超时的时间fds： 用于存储查询结果的文件描述符数组n_fds： 文件描述符数组的大小函数执行过程中会遍历上下文中的轮询记录，将每个记录转换为 GPollFD 结构，并存储在 fds 数组中，其中数组的大小由 n_fds 指定，如果轮询记录的数量超过 n_fds，则多余的记录将被忽略。如果 timeout 参数不为 NULL，则函数会将上下文的超时值复制到 timeout 指向的位置。如果超时不为 0，则设置 context->time_is_current 为 FALSE。最后，函数将返回填充到 fds 数组中的轮询记录数量。该函数将主循环上下文中的轮询记录转换为可以传递给底层轮询系统调用的格式，使得主循环可以等待多个事件源上的事件。check：gint g_main_context_check(GMainContext *context, gint max_priority, GPollFD *fds, gint n_fds)该函数接受四个参数：context： 主循环的上下文max_priority： 要检查的最大优先级fds： 包含轮询结果的文件描述符数组n_fds： 文件描述符数组的大小函数执行过程中会遍历上下文中的轮询记录，并使用 fds 数组中的结果更新每个记录的 revents 字段。然后遍历上下文中的事件源，调用每个事件源的 check 函数来确定是否准备好。如果事件源准备好，则将其添加到待调度列表中，并更新准备好的事件源源的数量和最大优先级。最后，如果有一个或多个事件源准备好，则函数返回 TRUE，否则返回 FALSE。该函数保证了主循环能够根据事件源的优先级和准备状态来进行调度。dispatch：void g_main_context_dispatch (GMainContext *context)该函数的主要负责调度主循环上下文中准备好的事件源，一般在调用 g_main_context_check 之后调用，确保事件源在准备好时得到适当的处理。在调度过程中，每个事件源的 dispatch 函数将被调用，以执行与事件源关联的特定操作。自定义事件源GMainLoop 除了内置的事件源类型外，还允许创建和使用自定义的事件源类型。自定义事件源必须将 glib 事件源作为"基类"，具体到实现上就是把 GSource 作为自定义事件源结构体的第一个成员，在创建事件源时调用 g_source_new() 函数并以参数的形式传入自定义事件源结构体的大小和函数表 GSourceFuncs。自定义事件源一般通过两种方式与 GMainContext 进行交互。GSourceFuncs 函数表中的准备函数可以设置超时，以确定主循环在再次检查事件源之前的最长休眠时间。此外，自定义事件源还可以调用 g_source_add_poll() 函数将文件描述符添加到 GMainContext 的检查集合中。代码示例以上介绍了 glib 事件循环的一般流程以及自定义事件源的方法，我们需要做的就是将新的 GSource 加入其中，glib 会负责处理 GSource 上注册的各种事件源。glib 中有两个函数，分别实现将 GSource 加入到 GMainContext 和将 fd 加入到 GSource 的功能：g_source_attach()： 将 source->poll_fds 中的文件描述符加入到 context->poll_records 中；source 添加到 context 的 source 链表中g_source_add_poll()： 将 fd 加入到 source->poll_fds 中，然后再加入到 context->poll_records 中，设置 context->poll_changed 为 TRUE下面将通过两个代码示例来具体说明，在使用 glib 库之前，首先使用以下命令安装开发包：sudo apt install libglib2.0-dev然后通过 pkg-config 工具查看 gcc 编译需要链接的动态库以及头文件：$ pkg-config --libs glib-2.0 -lglib-2.0 $ pkg-config --cflags glib-2.0 -I/usr/include/glib-2.0 -I/usr/lib/x86_64-linux-gnu/glib-2.0/include打印标准输入中的内容长度本例通过注册自定义事件源回调函数实现了打印标准输入中读到的内容长度，具体代码如下：#include <glib.h> gboolean io_watch(GIOChannel *channel, GIOCondition condition, gpointer data) { gsize len = 0; gchar *buffer = NULL; g_io_channel_read_line(channel, &buffer, &len, NULL, NULL); if (len > 0) { g_print("%d\n", len); } g_free(buffer); return TRUE; } int main(int argc, char *argv[]) { GMainLoop *loop = g_main_loop_new(NULL, FALSE); GIOChannel *channel = g_io_channel_unix_new(1); if (channel) { g_io_add_watch(channel, G_IO_IN, io_watch, NULL); g_io_channel_unref(channel); } g_main_loop_run(loop); g_main_context_unref(g_main_loop_get_context(loop)); g_main_loop_unref(loop); return 0; }上述代码中首先定义了回调函数 io_watch 用于实现“打印标准输入中读到内容的长度”的功能。在程序的主函数中，首先通过 g_main_loop_new 函数获取一个上下文的事件循环实例，接着调用 g_io_channel_unix_new 函数将标准输入描述符转化成 GIOChannel 以便操作，然后将针对 channel 事件源的回调函数 io_watch 注册到默认上下文，告诉 glib 需要关注 channel 的输入 G_IO_IN，当输入准备好之后，调用自己注册的回调函数，并传入参数 NULL。在 main 函数的最后，执行执行默认上下文的事件循环。使用如下命令编译上述代码：cc `pkg-config --cflags glib-2.0` demo1.c -o demo1 `pkg-config --libs glib-2.0`在终端运行 demo1 程序所得结果如下：$ ./demo1 hello world 12 glib event loop 16 ^C打印指定输入源中的内容长度本例在上一例的基础上更进一步，添加了自定义事件源和自定义状态机回调函数，实现了打印指定输入源中读取的内容长度，具体代码如下：#include <glib.h> typedef struct _MySource { GSource _source; GIOChannel *channel; GPollFD fd; } MySource; static gboolean watch(GIOChannel *channel) { gsize len = 0; gchar *buffer = NULL; g_io_channel_read_line(channel, &buffer, &len, NULL, NULL); if (len > 0) { g_print("%d\n", len); } g_free(buffer); return TRUE; } static gboolean prepare(GSource *source, gint *timeout) { *timeout = -1; return FALSE; } static gboolean check(GSource *source) { MySource *mysource = (MySource *)source; if (mysource->fd.revents != mysource->fd.events) { return FALSE; } return TRUE; } static gboolean dispatch(GSource *source, GSourceFunc callback, gpointer user_data) { MySource *mysource = (MySource *)source; if (callback) { callback(mysource->channel); } return TRUE; } static void finalize(GSource *source) { MySource *mysource = (MySource *)source; if (mysource->channel) { g_io_channel_unref(mysource->channel); } } int main(int argc, char *argv[]) { GError *error = NULL; GMainLoop *loop = g_main_loop_new(NULL, FALSE); GSourceFuncs funcs = {prepare, check, dispatch, finalize}; GSource *source = g_source_new(&funcs, sizeof(MySource)); MySource *mysource = (MySource *)source; if (!(mysource->channel = g_io_channel_new_file("test", "r", &error))) { if (error != NULL) { g_print("Unable to get test file channel: %s\n", error->message); } return -1; } mysource->fd.fd = g_io_channel_unix_get_fd(mysource->channel); mysource->fd.events = G_IO_IN; g_source_add_poll(source, &mysource->fd); g_source_set_callback(source, (GSourceFunc)watch, NULL, NULL); g_source_set_priority(source, G_PRIORITY_DEFAULT_IDLE); g_source_attach(source, NULL); g_source_unref(source); g_main_loop_run(loop); g_main_context_unref(g_main_loop_get_context(loop)); g_main_loop_unref(loop); return 0; }上述代码首先定义了自定义事件源 MySource 以及事件源回调函数 watch，实现了“读出 iochannel 中内容并打印其长度”的功能。接着，代码定义了四个状态机回调函数：prepare： 状态机 prepare 阶段的回调函数，timeout 等于 -1 告诉 poll 如果 IO 没有准备好，一直等待，即阻塞 IO。函数返回 FALSE 表示需要 poll 来检查事件源是否准备好，返回 TRUE 则表示跳过 pollcheck： 状态机 check 阶段的回调函数，检查自己感兴趣的 fd 状态是否准备好，用户通过设置 events 标志设置感兴趣的 fd 状态（包括文件可读、可写、异常等），revents 是 poll 的返回值，由内核设置，表明 fd 哪些状态是准备好的。当感兴趣的状态和 poll 返回的状态不相同，则表示 fd 没有准备好，函数返回 FALSE，glib 不发起调度，反之函数返回 TRUE，glib 发起调度diapatch： 状态机 dispatch 阶段的回调函数，只要函数 prepare 和 check 中有一个返回 TRUE，glib 就会直接调用此接口，执行用户注册的回调函数finalize： 当事件源不再被引用时的回调函数下面分析本例代码中主函数的执行逻辑：首先进行有关变量的初始化，包括从默认上下文获取事件循环实例、声明自定义的状态机回调函数以及定义自定义事件源。接着，创建一个文件类型的 GIOChannel，获取 GIOChannel 的 fd，放到 GPollFD 的 fd 域中，并设置感兴趣的文件状态，本例中为文件可读状态。这里 GIOChannel 是 glib 对文件描述符的封装，以实现平台可移植性，其 fd 类型可以是文件、管道或套接字。最终传给 poll 的文件描述符结构体如下：struct GPollFD { gint fd; // 文件描述符 gushort events; // 感兴趣的文件状态 gushort revents; // 返回值，由内核设置 };然后，程序将文件描述符添加到事件源中，设置事件源的回调函数和优先级，当位于同一个上下文中的多个事件源同时处于准备好了的状态时，优先级高的事件源会被 glib 优先调度。最后，glib 开始执行默认上下文的事件循环。使用如下命令编译上述代码：cc `pkg-config --cflags glib-2.0` demo2.c -o demo2 `pkg-config --libs glib-2.0`在终端直接运行 demo2 程序所得结果如下：$ ./demo2 Unable to get test file channel: No such file or directory打开另一终端窗口，新建 test 文件，在前一窗口中再次运行 demo2 程序，然后在当前窗口向 test 文件中写入内容：$ touch test $ echo "hello world" > test $ echo "RISC-V Linux" >> test可在前一窗口中看到如下运行结果：$ ./demo2 12 13 ^C总结本文为分析 QEMU 事件循环机制补充了前置知识，基于 glib v2.25.7 源码介绍了 glib 事件循环机制的基本原理，梳理了主要数据结构之间的关系，详细描述了 glib 事件循环的状态转移图，并通过两个具体的代码示例进一步说明了 glib 事件循环机制的运行流程。在下一篇文章中，我们将详细分析 QEMU 事件循环机制。参考资料《QEMU/KVM 源码解析与应用》李强，机械工业出版社GLib – 2.0: The Main Event LoopGLib – 2.0: Compiling GLib Applications
前言QEMU 使用命令行来配置模拟器的启动参数，本文将以 8.0.0 版本的 QEMU RISC-V (qemu-system-riscv64) 为例，简要梳理 QEMU 常用参数的含义与用法，重点分析 QEMU 的参数解析机制。常用参数QEMU 各选项的具体用法可以通过指令 qemu-system-riscv64 [options] help 查看。下面以在 QEMU 中启动 RISC-V Linux 内核的命令为例，简要介绍 QEMU 常用参数的含义：qemu-system-riscv64 -M virt -m 256M -nographic \ -kernel linux-kernel/arch/riscv/boot/Image \ -drive file=rootfs.img,format=raw,id=hd0 \ -device virtio-blk-device,drive=hd0 \ -append "root=/dev/vda rw console=ttyS0"-M： 指定模拟器运行时的设备类型，在上面的例子中指定了 qemu-system-riscv64 的模拟设备为 virt，即 RISC-V 架构下的“通用虚拟平台”-m： 指定虚拟机内部的内存大小-kernel： 指定内核镜像-append： 指定内核命令行代码-drive： 定义磁盘，file 表示磁盘镜像的文件路径，format 表示磁盘格式，id 表示磁盘号-device： 添加设备到虚拟机中参数解析流程概述QEMU 的全部可用参数定义于项目目录下的 qemu-options.hx 文件中，而参数解析的主体逻辑位于 softmmu/vl.c 文件的 qemu_init 函数中，主要分为两个阶段，第一阶段负责检查传入参数的合法性，并不具体解析，第二阶段执行具体的解析操作，并通过 switch 语句跳转不同为分支完成对应选项的设置。QEMU 在不同的抽象层次上定义了多种数据结构来对不同的参数进行描述，这些数据结构都在参数解析之前完成初始化。数据结构及初始化QEMU 在 softmmu/vl.c 文件中定义了 QEMUOption 结构体来描述不同的命令行参数，其代码如下：typedef struct QEMUOption { const char *name; int flags; int index; uint32_t arch_mask; } QEMUOption;其中 name 表示参数名称，flags 表示参数属性，为 1 表示拥有子参数，为 0 则表示无子参数，index 表示命令索引 (QEMU_OPTION_cmd)，arch_mask 表示参数支持的架构。在 softmmu/vl.c 文件中还定义了一个全局 QEMUOption 数组 qemu_options 来描述 QEMU 的全部可用参数，具体如下：static const QEMUOption qemu_options[] = { { "h", 0, QEMU_OPTION_h, QEMU_ARCH_ALL }, #define DEF(option, opt_arg, opt_enum, opt_help, arch_mask) \ { option, opt_arg, opt_enum, arch_mask }, #define DEFHEADING(text) #define ARCHHEADING(text, arch_mask) #include "qemu-options.def" { /* end of list */ } };可以看到，qemu_options 数组中首先定义了一个参数 h，其使用方法为 qemu-system-riscv64 -h，作用是打印帮助信息。其余所有的可用参数都都通过 DEF 宏定义在 <qemu_build_dir>/qemu-options.def 文件中。需要注意的是，qemu-options.def 文件是由 scripts/hxtool 脚本在编译时根据 qemu-options.hx 文件生成的，因此不在 QEMU 源代码目录中。对于含有多个子选项的参数，QEMU 在文件 include/qemu/option_int.h 中定义了两个结构体 QemuOpt 对子参数进行描述：struct QemuOpt { char *name; char *str; const QemuOptDesc *desc; union { bool boolean; uint64_t uint; } value; QemuOpts *opts; QTAILQ_ENTRY(QemuOpt) next; };其中 QemuOpt 用于存储子选项，每个 QemuOpt 结构体都有一个 QemuOptDesc 类型的成员变量来描述子选项的信息。QEMU 在文件 include/qemu/option.h 中定义了 QemuOptsList 结构体用于进一步抽象描述一个参数的所有子选项，有关代码如下：enum QemuOptType { QEMU_OPT_STRING = 0, /* no parsing (use string as-is) */ QEMU_OPT_BOOL, /* on/off */ QEMU_OPT_NUMBER, /* simple number */ QEMU_OPT_SIZE, /* size, accepts (K)ilo, (M)ega, (G)iga, (T)era postfix */ }; typedef struct QemuOptDesc { const char *name; enum QemuOptType type; const char *help; const char *def_value_str; } QemuOptDesc; struct QemuOptsList { const char *name; const char *implied_opt_name; bool merge_lists; /* Merge multiple uses of option into a single list? */ QTAILQ_HEAD(, QemuOpts) head; QemuOptDesc desc[]; };上述代码中，首先定义了枚举类型 QemuOptType 用于描述不同的参数类型，包括字符串、布尔值、数字和空间大小四种。接着定义了结构体 QemuOptDesc 用于存放对参数的描述信息，包括名称、类型、帮助信息和参数默认值。最后定义了结构体 QemuOptsList，每一个 QemuOptsList 结构体就对应了一个参数。但是，这里需要特别注意的是，QemuOptsList 并不直接与 QemuOpt 联系，中间需要通过结构体 QemuOpts 进行转接：struct QemuOpts { char *id; QemuOptsList *list; Location loc; QTAILQ_HEAD(, QemuOpt) head; QTAILQ_ENTRY(QemuOpts) next; };这样设计的目的在于避免发生参数混淆，QEMU 可以通过命令行指定创建两个相同的设备，这个时候这两个设备的选项都挂在 QemuOptsList 上，但是分别属于两个独立的 QemuOpts（id 不同），每个 QemuOpts 都维护自己的 QemuOpt 链表，成员变量 head 是 QemuOpts 下的 QemuOpt 链表头，成员变量 next 用来连接同一 QemuOptsList 下的其他 QemuOpts，具体如下图所示：QEMU 在 util/qemu-config.c 中定义了一个全局的 QemuOptsList 数组 vm_config_groups 来储存所有可用的参数：static QemuOptsList *vm_config_groups[48]; static QemuOptsList *drive_config_groups[5];这两行代码说明了 QEMU 最多支持 48 个参数，5 个驱动器参数。这两个全局数组由位于 softmmu/vl.c 文件的 qemu_init 函数负责初始化： qemu_add_opts(&qemu_drive_opts); qemu_add_drive_opts(&qemu_legacy_drive_opts); qemu_add_drive_opts(&qemu_common_drive_opts); qemu_add_drive_opts(&qemu_drive_opts); qemu_add_drive_opts(&bdrv_runtime_opts); qemu_add_opts(&qemu_chardev_opts); qemu_add_opts(&qemu_device_opts); qemu_add_opts(&qemu_netdev_opts); qemu_add_opts(&qemu_nic_opts); qemu_add_opts(&qemu_net_opts); qemu_add_opts(&qemu_rtc_opts); qemu_add_opts(&qemu_global_opts); qemu_add_opts(&qemu_mon_opts); qemu_add_opts(&qemu_trace_opts); qemu_plugin_add_opts(); qemu_add_opts(&qemu_option_rom_opts); qemu_add_opts(&qemu_accel_opts); qemu_add_opts(&qemu_mem_opts); qemu_add_opts(&qemu_smp_opts); qemu_add_opts(&qemu_boot_opts); qemu_add_opts(&qemu_add_fd_opts); qemu_add_opts(&qemu_object_opts); qemu_add_opts(&qemu_tpmdev_opts); qemu_add_opts(&qemu_overcommit_opts); qemu_add_opts(&qemu_msg_opts); qemu_add_opts(&qemu_name_opts); qemu_add_opts(&qemu_numa_opts); qemu_add_opts(&qemu_icount_opts); qemu_add_opts(&qemu_semihosting_config_opts); qemu_add_opts(&qemu_fw_cfg_opts); qemu_add_opts(&qemu_action_opts);其中，qemu_add_opts 函数的实现位于 util/qemu-config.c 文件中，该函数主要负责将参数中传入的 OemuOptsList 添加到全局数组 vm_config_groups 中：void qemu_add_opts(QemuOptsList *list) { int entries, i; entries = ARRAY_SIZE(vm_config_groups); entries--; /* keep list NULL terminated */ for (i = 0; i < entries; i++) { if (vm_config_groups[i] == NULL) { vm_config_groups[i] = list; return; } } fprintf(stderr, "ran out of space in vm_config_groups"); abort(); }第一阶段QEMU 参数解析的第一阶段，遍历参数数组，通过 lookup_opt 函数来得到一个 QEMUOption，判断这个 QEMUOption 是否在之前初始化的的数组中。这一步骤的主要作用验证参数的合法性，下面结合源码具体分析： /* first pass of option parsing */ optind = 1; while (optind < argc) { if (argv[optind][0] != '-') { /* disk image */ optind++; } else { const QEMUOption *popt; popt = lookup_opt(argc, argv, &optarg, &optind); switch (popt->index) { case QEMU_OPTION_nouserconfig: userconfig = false; break; } } } machine_opts_dict = qdict_new(); if (userconfig) { qemu_read_default_config_file(&error_fatal); }首先按照下标顺序依次读取终端传入的参数数组，跳过子选项，调用 lookup_opt 函数查询读取的参数名称，然后初始化 machine_opts_dict 数组，并根据实际情况加载用户配置。这里的 machine_opts_dict 是一个字典结构，主要用于存储终端传入的参数数组中的虚拟机选项和参数，包括 CPU 数量、内存大小、设备配置等。machine_opts_dict 的存在使得参数解析机制能够以一种结构化的方式管理和访问虚拟机参数，而不是使用分散的单独变量或者凌乱的数据结构。下面给出 lookup_opt 函数的定义：static const QEMUOption *lookup_opt(int argc, char **argv, const char **poptarg, int *poptind) { const QEMUOption *popt; int optind = *poptind; char *r = argv[optind]; const char *optarg; loc_set_cmdline(argv, optind, 1); optind++; /* Treat --foo the same as -foo. */ if (r[1] == '-') r++; popt = qemu_options; for(;;) { if (!popt->name) { error_report("invalid option"); exit(1); } if (!strcmp(popt->name, r + 1)) break; popt++; } if (popt->flags & HAS_ARG) { if (optind >= argc) { error_report("requires an argument"); exit(1); } optarg = argv[optind++]; loc_set_cmdline(argv, optind - 2, 2); } else { optarg = NULL; } *poptarg = optarg; *poptind = optind; return popt; }lookup_opt 函数首先调用 loc_set_cmdline 函数在终端传入的命令行参数中根据参数索引 optind 进行定位，然后通过参数名称的比较在全局数组 qemu_options 中寻找对应的 QEMUOption。同时，lookup_opt 函数会将参数后面的子选项保存到 optarg 中。至此，我们不难发现，第一阶段的工作并没有涉及实际的参数解析，而是完成了从命令行读取用户的配置参数并对所有参数进行有效性验证，为第二阶段的正式解析做准备。QEMU 参数解析机制的两步设计，实现了验证逻辑和执行逻辑的有效分离，减少了出错的风险，使代码逻辑更加清晰。第二阶段QEMU 参数解析的第二阶段，是真正解析具体参数并执行相应设置的阶段，主要逻辑如下： /* second pass of option parsing */ optind = 1; for(;;) { if (optind >= argc) break; if (argv[optind][0] != '-') { loc_set_cmdline(argv, optind, 1); drive_add(IF_DEFAULT, 0, argv[optind++], HD_OPTS); } else { const QEMUOption *popt; popt = lookup_opt(argc, argv, &optarg, &optind); if (!(popt->arch_mask & arch_type)) { error_report("Option not supported for this target"); exit(1); } switch(popt->index) { case QEMU_OPTION_cpu: ... break; ... default: ... } } }第二阶段的逻辑就是按照下标顺序依次遍历终端传入的参数数组，调用 lookup_opt 函数找到对应的 QEMUOption，然后检查对应选项在当前架构下是否支持，最后使用 switch 语句根据 QEMUOption 的成员变量 index 的不同来执行不同的分支完成具体的设置。下面将以不同的参数为例，详细说明参数解析第二阶段 switch 语句中的分支执行流程。首先关注最为简单的 -version 参数，其含义就是打印 QEMU 版本信息，然后退出程序，有关代码如下： case QEMU_OPTION_version: version(); exit(0); break;可以看到，由于其含义最为简单，因此解析实现也非常简洁，直接调用 version 函数打印版本信息，随后退出程序。接着分析 -kernel 参数，通过对 -kernel 参数解析流程的考察，我们可以一窥与机器设置有关的参数的解析机制。首先给出 -kernel 参数在解析第二阶段分支执行的主要代码： case QEMU_OPTION_kernel: qdict_put_str(machine_opts_dict, "kernel", optarg); break;我们可以发现，与机器设置有关的参数在整个解析过程中并不会真正执行具体设置，而是将对应的参数选项添加到 machine_opts_dict 中，最终参数设置的落实工作由机器创建及初始化阶段的 qemu_apply_legacy_machine_options 函数和 qemu_apply_machine_options 函数完成，这一部分将在“QEMU 机器创建及初始化机制”的分析文章中具体阐述，本文不再赘述。下图给出了与与机器设置有关的参数从解析到落实的一般流程：最后，观察 -device 参数，同样给出分支执行代码： case QEMU_OPTION_device: if (optarg[0] == '{') { QObject *obj = qobject_from_json(optarg, &error_fatal); DeviceOption *opt = g_new0(DeviceOption, 1); opt->opts = qobject_to(QDict, obj); loc_save(&opt->loc); assert(opt->opts != NULL); QTAILQ_INSERT_TAIL(&device_opts, opt, next); } else { if (!qemu_opts_parse_noisily(qemu_find_opts("device"), optarg, true)) { exit(1); } } break;分支执行的主要逻辑如下：首先检查 optarg（-device 选项的参数）是否以 { 开始。如果是，说明 optarg 是一个 JSON 对象，那么就需要调用 qobject_from_json 函数将 JSON 对象转换为一个 QObject 对象。然后，创建一个新的 DeviceOption 对象，并将 QObject 对象转换为一个 QDict 并存储在 DeviceOption 中，最后将 DeviceOption 添加到 device_opts 队列中。如果 optarg 不是一个 JSON 对象，那么就调用 qemu_opts_parse_noisily 函数将 optarg 解析为一个 QemuOpts 对象，并将这个对象添加到 "device" 选项的列表中。如果解析失败，则退出程序。下面我们重点关注 else 分支的部分，首先给出 qemu_find_opts 函数的定义，它位于 util/qemu-config.c 文件中：QemuOptsList *qemu_find_opts(const char *group) { QemuOptsList *ret; Error *local_err = NULL; ret = find_list(vm_config_groups, group, &local_err); if (local_err) { error_report_err(local_err); } return ret; }qemu_find_opts 函数从全局数组 vm_config_groups 中找到刚才插入的 -device 选相对应的 QemuOptsList 并返回，而 qemu_opts_parse_noisily 函数只是简单调用了 opts_parse 函数，后者会解析出一个 QemuOpts，每一个大类的参数都会在相应的 QemuOptsList 中构造 QEMUOpts。继续分析 opts_parse 函数：static QemuOpts *opts_parse(QemuOptsList *list, const char *params, bool permit_abbrev, bool warn_on_flag, bool *help_wanted, Error **errp) { const char *firstname; char *id = opts_parse_id(params); QemuOpts *opts; assert(!permit_abbrev || list->implied_opt_name); firstname = permit_abbrev ? list->implied_opt_name : NULL; opts = qemu_opts_create(list, id, !list->merge_lists, errp); g_free(id); if (opts == NULL) { return NULL; } if (!opts_do_parse(opts, params, firstname, warn_on_flag, help_wanted, errp)) { qemu_opts_del(opts); return NULL; } return opts; }opts_parse 函数代码中最重要的两行是对 qemu_opts_create 函数和 opts_do_parse 函数的调用，前者用来创建 QemuOpts 并将它插入到对应的 QemuOptsList 上，后者则负责解析出 QemuOpt。函数 opts_do_parse 的作用是解析参数的值，如本文开头例子中的命令行参数 -device virtio-blk-device,drive=hd0。QEMU 的参数可能存在多种情况，比如上面的例子中 virtio-blk-device 表示开启一个标志，也有可能类似于 drive=hd0 的参数赋值语句。opts_do_parse 函数需要处理各种情况，并对每一个值生成一个 QemuOpt，关键代码如下：static bool opts_do_parse(QemuOpts *opts, const char *params, const char *firstname, bool warn_on_flag, bool *help_wanted, Error **errp) { char *option, *value; const char *p; QemuOpt *opt; for (p = params; *p;) { p = get_opt_name_value(p, firstname, warn_on_flag, help_wanted, &option, &value); if (help_wanted && *help_wanted) { g_free(option); g_free(value); return false; } firstname = NULL; if (!strcmp(option, "id")) { g_free(option); g_free(value); continue; } opt = opt_create(opts, option, value); g_free(option); if (!opt_validate(opt, errp)) { qemu_opt_del(opt); return false; } } return true; }opts_do_parse 函数是 QEMU 参数解析过程的核心部分，它接受如下六个参数：opts 是一个 QemuOpts 对象，用于存储解析后的选项params 是一个字符串，包含需要解析的参数firstname 是第一个选项的名字warn_on_flag 是一个布尔值，如果为 true，那么当遇到一个标志选项时，函数会打印一个警告消息help_wanted 是一个指向布尔值的指针，如果函数解析到一个 help 选项，则设置 *help_wanted 为 true；errp 是一个错误指针，如果函数遇到错误，则设置 *errperrp 是一个错误指针，如果函数遇到错误，那么它会设置 *errpopts_do_parse 函数使用一个 for 循环一次遍历解析 params 中的每个选项：首先调用 get_opt_name_value 函数来获取下一个选项的名字和值，然后创建一个新的 QemuOpt 对象并添加到 opts 中，如果解析到一个 help 选项，则会设置 *help_wanted 为 true，并立即返回 false。注意，如果解析到的选项的名称是 id，则会跳过这个选项，因为参数 id 是用于设置 QemuOpts 对象的 ID 的，不能当作普通参数解析为 QemuOpt。最后，调用 opt_validate 函数来验证新创建的 QemuOpt 的有效性并返回解析结果。仍然以命令行参数 -device virtio-blk-device,drive=hd0 为例，经过上述过程将解析出 2 个 QemuOpt 并形成如下图所示的链表：总结本文以 QEMU 中引导 RISC-V 架构 Linux 内核启动的指令为例，总结归纳了 QEMU 常用参数的用法与含义，分析阐述了描述不同参数的各种数据结构的定义、作用以及相互关系，并按照 QEMU 的执行顺序详细梳理了数据结构初始化、参数解析第一阶段、参数解析第二阶段的代码逻辑。参考资料《QEMU 启动方式分析（1）：QEMU 及 RISC-V 启动流程简介》System Emulation: Invocation《QEMU/KVM 源码解析与应用》李强，机械工业出版社
QEMU 系统模式下指令解码模块简析：以 RISC-V 为例
前言QEMU 是一个通用的、开源的模拟器，通过纯软件方式实现硬件的虚拟化，模拟外部硬件，为用户提供抽象、虚拟的硬件环境。QEMU 亦可藉由硬件虚拟化变身为虚拟机。QEMU 支持以下两种方式进行模拟：用户模式（User Mode Emulation）：在一种架构的 CPU 上运行为另一种架构的 CPU 编译的程序。在该模式下，QEMU 作为进程级虚拟机，只模拟系统调用前的用户态代码，系统调用进入内核后，由宿主机操作系统原生执行，QEMU 提供不同架构系统调用映射的转换。系统模拟（System Emulation）：在该模式下，QEMU 作为系统级虚拟机提供运行客户机所需的完整环境，包括 CPU，内存和外围设备等。概述QEMU 模拟的基本逻辑QEMU 提供两种 CPU 模拟实现方式，一种基于架构无关的中间码实现，另一种基于硬件虚拟化技术实现。第一种方式，QEMU 使用 Tiny Code Generator（下文简称 TCG）将客户机指令动态翻译为宿主机指令执行。这种方式的主要思想是使用纯软件的方法将客户机 CPU 指令先解码为架构无关的中间码（即 Intermediate Representation），然后再把中间码翻译为宿主机 CPU 指令执行。其中，由客户机 CPU 指令解码为中间码的过程被称为前端，由中间码翻译为宿主机 CPU 指令的过程被称为后端。以 RISC-V 为例，指令的前端解码逻辑位于 target/riscv 中，后端翻译逻辑位于 tcg/ 中，外设及其他硬件模拟代码位于 hw/riscv 中。第二种方式，基于硬件虚拟化技术实现，直接使用宿主机 CPU 执行客户机指令，可以达到接近真实机器的运行性能。本文只关注系统模式下 TCG 方式的分析，不讨论基础硬件虚拟化技术的运行逻辑。QEMU 翻译执行过程TCG 定义了一系列中间码，将已经翻译的代码以代码块的形式储存在 Translation Block 中，通过跳转指令将宿主机 CPU 的指令集和客户机 CPU 的指令集链接。下图以 RISC-V 为例，给出了 QEMU v8.0.0 系统模拟的翻译执行流程。我们可以发现 TCG 前端解码和后端翻译都按照指令块的粒度进行，将一个客户机指令块翻译成中间码，然后把中间码翻译成宿主机 CPU 指令，整个过程动态执行。为了提高翻译效率，QEMU 将翻译成的宿主机 CPU 指令块做了缓存，即上文提到的 Translation Block，CPU 执行的时候，先在缓存中查找对应的 TB，如果查找成功就直接执行，否则进入翻译流程。从更加抽象的视角来看，TCG 模式下所谓客户机 CPU 的运行，实际上就是根据指令不断改变客户机 CPU 的状态，即改变描述客户机 CPU 的状态的数据结构中的有关变量。因为实际的代码执行过程是在宿主机 CPU 上完成的，因此客户机 CPU 的指令必须被翻译为宿主机 CPU 指令才能被执行，才能改变客户机 CPU 的数据状态。QEMU 为了解耦把客户机 CPU 指令先解码为中间码，中间码其实就是一组描述如何改变客户机 CPU 数据状态且架构无关的语句，所以目标 CPU 状态参数会被传入中间码描述语句。中间码实际上是改变客户机 CPU 状态的抽象的描述，对于部分难以抽象成一般描述的指令就用 helper 函数进行补充。例如 RV32FD 指令集中的 fadd.d rd, rs1, rs2 指令，表示双精度浮点加，将 rs1 和 rs2 寄存器中的双精度浮点数相加并将舍入后的结果送到 rd 寄存器。对于该指令的行为，TCG 中间码并没有合适的描述，因此 QEMU 使用 helper_fmadd_d 函数根据情况直接调用模拟 FPU 解决问题。最后，将中间码翻译为宿主机 CPU 代码时，TCG 后端使用 tcg_gen_xxx 函数描述具体某条客户机 CPU 指令对客户机 CPU 数据状态的改变。翻译过程中 gen_intermediate_code 函数负责前端解码，把客户机的指令翻译成中间码。而 tcg_gen_code 负责后端翻译，将中间码翻译成宿主机 CPU 上的指令，其中 tcg_out_xxx 函数执行具体的翻译工作。TCG 细节前端解码QEMU 定义了 instruction pattern 来描述客户机 CPU 指令，一个 instruction pattern 是指一组相同或相近的指令，RISC-V 架构的指令描述位于 target/riscv 目录下的 insn16.decode、insn32.decode 文件中。QEMU 编译的时候会解析 .decode 文件，使用脚本 scripts/decodetree.py 生成对应指令描述函数的声明并存放于 <qemu_build_dir>/libqemu-riscv64-softmmu.fa.p 目录下的 decode-insn32.c.inc 和 decode-insn16.c.inc 文件中。在这两个文件中还定义两个较为关键的解码函数 decode_insn32 和 decode_insn16，QEMU 将客户机指令翻译成中间码的时候需要调用这两个解码函数。需要注意的是，脚本 scripts/decodetree.py 生成的只是 trans_xxx 函数的声明，其定义需要开发者实现，RISC-V 对应的实现位于 target/riscv/insn_trans/ 目录中。Decode Tree每种 instruction pattern 都有固定位和固定掩码，它们的组合构成了模式匹配的条件：(insn & fixedmask) == fixedbits对于每种 instruction pattern，scripts/decodetree.py 脚本定义了具体描述形式，下面进行简要分析：Fields： CPU 在解码的时候需要把指令中的特性 field 中的数据取出作为传入参数（寄存器编号，立即数，操作码等），field 描述一个指令编码中特定的字段，根据描述可以生成取对应字段的函数。InputGenerated code%disp 0:s16sextract(i, 0, 16)%imm9 16:6 10:3extract(i, 16, 6) << 3 \extract(i, 10, 3)%disp12 0:s1 1:1 2:10sextract(i, 0, 1) << 11 \extract(i, 1, 1) << 10 \extract(i, 2, 10)%shimm8 5:s8 13:1 !function=expand_shimm8expand_shimm8(sextract(i, 5, 8) << 1 \extract(i, 13, 1))%sz_imm 10:2 sz:3 !function=expand_sz_immexpand_sz_imm(extract(i, 10, 2) << 3 \extract(a->sz, 0, 3))上表给出了一些例子，如第一行的 %disp 0:s16 表示指令编码第 0 位起的 16 位构成了一个带符号数，因此生成代码 sextract(i, 0, 16)，意即从指令 i 的编码第 0 位开始取 16 位解释为带符号数返回。还有第三行的 %disp12 0:s1 1:1 2:10 表示该立即数由三个部分拼接而成，因此生成的代码中就包含了相应的移位、拼接运算。由 field 定义所生成的函数就负责完成这种与从指令编码中取数有关的计算。Argument Sets： 定义数据结构。比如，target/riscv/insn32.decode 中定义的 &b imm rs2 rs1 在编译后的 decode-insn32.c.inc 中生成的数据结构如下，这个结构将作为 trans_xxx 函数的传入参数。typedef struct { int imm; int rs2; int rs1; } arg_b;Formats： 定义指令的格式，例如下面的例子是对一个 32-bit 指令编码的描述，其中 . 表示一个 bit 位。@opr ...... ra:5 rb:5 ... 0 ....... rc:5 @opi ...... ra:5 lit:8 1 ....... rc:5Patterns： 用来定义具体指令。这里借助 RV32I 基础指令集中的 lui 指令进行详细分析：lui .................... ..... 0110111 @u另外列出相关的 format、argument、field 的定义，以便分析：# Argument sets: &u imm rd # Formats 32: @u .................... ..... ....... &u imm=%imm_u %rd # Fields: %rd 7:5 # immediates: %imm_u 12:s20 !function=ex_shift_12可以看到 lui 指令的操作码是 0110111，指令的格式定义是 @u，使用的参数定义是 &u，而 &u 就是 trans_lui 函数的传入参数结构体里的变量定义，其中定义的变量名字是 imm、rd，这个 imm 实际的格式是 %imm_u，它是一个由指令编码 31-12 位定义的立即数，将指令编码 31-12 位的数值左移 12 位即可得到最终结果，rd 实际的格式是 %rd，是一个在指令编码 7-5 位定义的 rd 寄存器的标号。可以看到 target/riscv/insn_trans/trans_rvi.c.inc 中对应的 trans_lui 函数的实现如下：static bool trans_lui(DisasContext *ctx, arg_lui *a) { gen_set_gpri(ctx, a->rd, a->imm); return true; }trans_xxx 函数trans_xxx 函数负责将具体的客户机指令转换为中间码指令，若转换成功则返回 true，否则返回 false。下面以 RISC-V 架构的 add 指令为例进行分析。如下是 target/riscv/insn_trans/trans_rvi.c.inc 文件中对 add 指令的模拟。static bool trans_add(DisasContext *ctx, arg_add *a) { return gen_arith(ctx, a, EXT_NONE, tcg_gen_add_tl, tcg_gen_add2_tl); }函数 gen_arith 被定义在文件 target/riscv/translate.c 中：static bool gen_arith(DisasContext *ctx, arg_r *a, DisasExtend ext, void (*func)(TCGv, TCGv, TCGv), void (*f128)(TCGv, TCGv, TCGv, TCGv, TCGv, TCGv)) { TCGv dest = dest_gpr(ctx, a->rd); TCGv src1 = get_gpr(ctx, a->rs1, ext); TCGv src2 = get_gpr(ctx, a->rs2, ext); if (get_ol(ctx) < MXL_RV128) { func(dest, src1, src2); gen_set_gpr(ctx, a->rd, dest); } else { if (f128 == NULL) { return false; } TCGv src1h = get_gprh(ctx, a->rs1); TCGv src2h = get_gprh(ctx, a->rs2); TCGv desth = dest_gprh(ctx, a->rd); f128(dest, desth, src1, src1h, src2, src2h); gen_set_gpr128(ctx, a->rd, dest, desth); } return true; }注意到函数中 func 指向的函数是由 trans_add 传入的 tcg_gen_add_tl 函数，而此函数又在 inluce/tcg/tcg-op.h 中以宏定义的形式被定义为 tcg_gen_add_i64 或 tcg_gen_add_i32 函数，下面给出 tcg_gen_add_i64 函数的定义：void tcg_gen_addi_i64(TCGv_i64 ret, TCGv_i64 arg1, int64_t arg2) { if (arg2 == 0) { tcg_gen_mov_i64(ret, arg1); } else if (TCG_TARGET_REG_BITS == 64) { tcg_gen_add_i64(ret, arg1, tcg_constant_i64(arg2)); } else { tcg_gen_add2_i32(TCGV_LOW(ret), TCGV_HIGH(ret), TCGV_LOW(arg1), TCGV_HIGH(arg1), tcg_constant_i32(arg2), tcg_constant_i32(arg2 >> 32)); } }RISC-V 的 add 指令内容是从 CPU 的 rs1 和 rs2 寄存器中取操作数，相加后送入 rd 寄存器中。宏观上看，gen_arith 函数首先调用 dest_gpr 和 get_gpr 这两个寄存器操作封装函数获取 rs1 和 rs2 寄存器的值，并准备 rd 寄存器。然后通过 func(dest, src1, src2) 最终调用 tcg_gen_addi_i64 函数完成两数相加，最后使用 gen_set_gpr 将结果传送至 rd 寄存器，完成 add 指令解码。接着，我们针对 gen_set_gpr 进行深入分析，以 RV32 指令为例，追踪该函数的调用链：分析上述调用链的参数可以发现最后生成了一条 mov_i32 t0, t1 指令，意思是将 t1 寄存器中的数移动到 t0 寄存器中。该指令先被挂到了一个链表里，此后的后端翻译会把这些指令翻译成宿主机指令。到这里，前端解码的逻辑就基本上打通了。还有最后一个问题需要解决：cpu_gpr[reg_num] 这个全局变量是如何索引到客户机 CPU 寄存器的？解决该问题的基本思路是，只要 TCG 前端和后端约定描述客户机 CPU 状态数据结构相同，确保 cpu_gpr[reg_num] 指向的就是相关寄存器在这个数据结构中的位置即可，这一点在 cpu_gpr[] 数组的初始化过程中具体体现：void riscv_translate_init(void) { int i; // ... for (i = 1; i < 32; i++) { cpu_gpr[i] = tcg_global_mem_new(cpu_env, offsetof(CPURISCVState, gpr[i]), riscv_int_regnames[i]); // ... } // ... }cpu_gpr[] 数组在初始化时调用 tcg_global_mem_new 函数在 TCG 上下文 tcg_ctx 中分配空间并返回其相对地址，而后段翻译时访问 cpu_gpr[] 数组就是在访问 TCG 上下文中描述寄存器的变量，这样 cpu_gpr[reg_name] 就在前端和后端之间建立了连接。后端翻译后端的代码主要负责将中间码翻译成宿主机指令，本质上就是根据中间码的描述使用宿主机指令来改变内存中表示的客户机 CPU 的数据结构以及客户机内存的状态。考虑以下两条 RISC-V 汇编指令：addi sp,sp,-32 sd s0,24(sp)经过前端解码，可以得到以下中间码：add_i64 x2/sp,x2/sp,$0xffffffffffffffe0 add_i64 tmp4,x2/sp,$0x18 qemu_st_i64 x8/s0,tmp4,leq,0注意到 sd 指令被翻译成了两条中间码，第一条 add_i64 是用来计算 sd 指令的目标地址，计算结果保存在 tmp4 这个虚拟寄存器里，第二条中间码把 s0 的值储存到虚拟寄存器 tmp4 描述的内存上。在中间码中，x2/sp 和 x8/s0 仍然是客户机 CPU 上寄存器的名字，但是逻辑上已经全部映射为 QEMU 虚拟寄存器。TCG 前端将 RISC-V 汇编指令解码为中间码和虚拟寄存器的表示，后端翻译则基于中间码和虚拟寄存器进行。再次审视上述两条指令，addi 的中间码表示要把客户机的 sp 寄存器加上 -32，sd 的中间码表示要将客户机的 s0 寄存器中的值送到 sp 寄存器加 24 后得到的地址处。对于这些中间码，在 ARM 架构的宿主机上可能被翻译为以下指令：ldr x20, [x19, #0x10] sub x20, x20, #0x20 str x20, [x19, #0x10] add x21, x20, #0x18 ldr x22, [x19, #0x40] str x22, [x21, xzr]这段指令主要进行了以下操作：把客户机 CPU 的 sp 寄存器装载到宿主机 CPU 的 x20 寄存器，使用 sub 指令完成客户机 CPU sp 寄存器值的计算并进行更新；使用 add 指令计算客户机 CPU 的 sd 指令的目标地址并保存到宿主机 CPU 的 x21 寄存器，接着把客户机 CPU 的 s0 寄存器装载到宿主机 CPU 的 x22 寄存器，最后使用 str 指令更新目标地址处的值。通过以上案例可以发现，TCG 后端主要完成三件事情：分配宿主机 CPU 寄存器、生成宿主机 CPU 指令以及宿主机 CPU 和客户机 CPU 之间的状态同步。其中，状态同步实际上通过两次映射完成：第一次是 TCG 前端解码时将客户机 CPU 寄存器映射为 QEMU 虚拟寄存器，第二次是 TCG 后端分配宿主机 CPU 寄存器时将 QEMU 虚拟寄存器映射为宿主机 CPU 的物理寄存器。下面仍然以 add 指令为例，给出后端代码调用过程的详细分析：tcg_gen_code 是整个后端翻译的入口，负责寄存器和内存区域之间的同步逻辑并根据不同指令类型调用相关函数将中间码翻译为宿主机 CPU 指令。默认情况下，tcg_gen_code 会调用 tcg_reg_alloc_op 函数，该函数会生成用宿主机 CPU 指令描述的同步逻辑，存放在 TB 中，最后调用不同架构的开发者提供的 tcg_out_op 函数完成具体指令的翻译工作。针对 add 指令，最终会调用 tcg_out32() 函数，该函数负责将一个 32 位无符号整数 v 写入到指针 s->code_ptr 对应的内存位置，并根据目标平台的指令单元大小更新该指针的值。总结本文主要分析了 QEMU 系统模式下指令解码模块。QEMU 将客户机 CPU 指令解码为中间码，中间码是对指令如何改变客户机 CPU 数据状态的抽象描述，TCG 后端将中间码翻译为宿主机 CPU 指令，也就是将中间码所描述的对客户机 CPU 数据状态的更改用宿主机 CPU 指令的形式进行描述，执行完成后就达到了模拟客户机 CPU 运行的效果。至此，从前端到后端，从解码到翻译的逻辑链条就完整了。参考资料Decodetree SpecificationTCG Intermediate Representation