QEMU 9.1.50 版本为例。
数据结构及初始化
QEMU 在 softmmu/vl.c
文件中定义了 QEMUOption
结构体来描述不同的命令行参数,其代码如下:
1typedef struct QEMUOption {2 const char *name;3 int flags;4 int index;5 uint32_t arch_mask;6} QEMUOption;
其中 name
表示参数名称,flags
表示参数属性,为 1 表示拥有子参数,为 0 则表示无子参数,index
表示命令索引 (QEMU_OPTION_cmd),arch_mask
表示参数支持的架构。在 softmmu/vl.c
文件中还定义了一个全局 QEMUOption
数组 qemu_options
来描述 QEMU 的全部可用参数,具体如下:
1enum {2#define DEF(option, opt_arg, opt_enum, opt_help, arch_mask) \3 opt_enum,4#define DEFHEADING(text)5#define ARCHHEADING(text, arch_mask)6
7#include "qemu-options.def"8};9
10static const QEMUOption qemu_options[] = {11 { "h", 0, QEMU_OPTION_h, QEMU_ARCH_ALL },12
13#define DEF(option, opt_arg, opt_enum, opt_help, arch_mask) \14 { option, opt_arg, opt_enum, arch_mask },15#define DEFHEADING(text)5 collapsed lines
16#define ARCHHEADING(text, arch_mask)17
18#include "qemu-options.def"19 { /* end of list */ }20};
可以看到,qemu_options
数组中首先定义了一个参数 h
,其使用方法为 qemu-system-riscv64 -h
,作用是打印帮助信息。其余所有的可用参数都都通过 DEF
宏定义在 <qemu_build_dir>/qemu-options.def
文件中。需要注意的是,qemu-options.def
文件是由 scripts/hxtool
脚本在编译时根据 qemu-options.hx
文件生成的,因此不在 QEMU 源代码目录中。
这里需要说明一下,在 QEMU 中常用的一种编程方式:将可重复利用的配置信息通过宏定义的方式放在一个文件中,在正式使用时,通过重新定义宏来实现同一个配置信息文件生成不同结构体或数组的功能。例如需要定义的
qemu_options
静态数组和各个选项的enum
类型,其中对DEF
宏进行了重新定义,并包含了同一个文件 “qemu-options.def”,但由于DEF
两次定义的内容不同,最终生成的数据结构不同。实现一次配置,多次重复利用。
QEMUOption
只定义了每一个大选项的名称、是否有子选项、支持的体系结构,但并没有定义子选项。子选项则是由文件 include/qemu/option_int.h
中定义的两个结构体 QemuOpt
、QemuOpts
进行描述。
1struct QemuOpt {2 char *name;3 char *str;4
5 const QemuOptDesc *desc;6 union {7 bool boolean;8 uint64_t uint;9 } value;10
11 QemuOpts *opts;12 QTAILQ_ENTRY(QemuOpt) next;13};14
15struct QemuOpts {6 collapsed lines
16 char *id;17 QemuOptsList *list;18 Location loc;19 QTAILQ_HEAD(, QemuOpt) head;20 QTAILQ_ENTRY(QemuOpts) next;21};
QemuOpt
保存每一个子选项的具体信息,每一个子选项会通过 TailQueue
连接成一个双向尾队列 QemuOpts
。因此,也可以将 QemuOpt
视作 QemuOpts
中的一个队列节点。其中所定义的 QTAILQ_ENTRY
是和 TailQueue
相关的数据结构,关于 TailQueue
的详细信息可查看 QEMU中的队列queue。
QemuOpts
是大选项中各个子选项的动态集合。QemuOpts
中保存的QTAILQ_HEAD(, QemuOpt) head;
是 QemuOpt
队列的头节点,能够访问所有的 QemuOpt
。
以 -device
大选项为例,QEMU 中 -device
表示设备,设备有非常多种,每一种都是独立的,且可以重复添加。一个 QemuOpts
只能保存一种设备的子选项集合。那么多个相同种类的设备,但是参数不同,如何保存?这就需要再引入一个数据结构 QemuOptsList
,QemuOptsList
也是一个 TailQueue
,保存 QemuOpts
的集合。
1struct QemuOptsList {2 const char *name;3 const char *implied_opt_name;4 bool merge_lists; /* Merge multiple uses of option into a single list? */5 QTAILQ_HEAD(, QemuOpts) head;6 QemuOptDesc desc[];7};
每一个 QemuOptsList
代表了大选项,QemuOptsList
中的每一个 QemuOpts
代表一类设备,由大选项中的子选项集合组成。
下图给出完整体的数据结构的实例,将 QemuOptsList
中的数据结构和 QemuOpts
分开绘制,并在 QemuOpts
给出了 TailQueue
的细节,结合 QEMU中的队列queue 理解更佳。
QEMU 在 util/qemu-config.c
中定义了一个全局的 QemuOptsList
数组 vm_config_groups
来储存所有可用的参数(即上图中提到的数组 vm_config_groups
):
1static QemuOptsList *vm_config_groups[48];2static QemuOptsList *drive_config_groups[5];
这两行代码说明了 QEMU 最多支持 48 个参数,5 个驱动器参数。这两个全局数组由位于 softmmu/vl.c
文件的 qemu_init
函数负责初始化:
1 qemu_add_opts(&qemu_drive_opts);2 qemu_add_drive_opts(&qemu_legacy_drive_opts);3 qemu_add_drive_opts(&qemu_common_drive_opts);4 qemu_add_drive_opts(&qemu_drive_opts);5 qemu_add_drive_opts(&bdrv_runtime_opts);6 qemu_add_opts(&qemu_chardev_opts);7 qemu_add_opts(&qemu_device_opts);8 qemu_add_opts(&qemu_netdev_opts);9 qemu_add_opts(&qemu_nic_opts);10 qemu_add_opts(&qemu_net_opts);11 qemu_add_opts(&qemu_rtc_opts);12 qemu_add_opts(&qemu_global_opts);13 qemu_add_opts(&qemu_mon_opts);14 qemu_add_opts(&qemu_trace_opts);15 qemu_plugin_add_opts();16 collapsed lines
16 qemu_add_opts(&qemu_option_rom_opts);17 qemu_add_opts(&qemu_accel_opts);18 qemu_add_opts(&qemu_mem_opts);19 qemu_add_opts(&qemu_smp_opts);20 qemu_add_opts(&qemu_boot_opts);21 qemu_add_opts(&qemu_add_fd_opts);22 qemu_add_opts(&qemu_object_opts);23 qemu_add_opts(&qemu_tpmdev_opts);24 qemu_add_opts(&qemu_overcommit_opts);25 qemu_add_opts(&qemu_msg_opts);26 qemu_add_opts(&qemu_name_opts);27 qemu_add_opts(&qemu_numa_opts);28 qemu_add_opts(&qemu_icount_opts);29 qemu_add_opts(&qemu_semihosting_config_opts);30 qemu_add_opts(&qemu_fw_cfg_opts);31 qemu_add_opts(&qemu_action_opts);
其中,qemu_add_opts
和 qemu_add_drive_opts
函数的实现位于 util/qemu-config.c
文件中,主要负责将参数中传入的 OemuOptsList
添加到全局数组中:
1void qemu_add_drive_opts(QemuOptsList *list)2{3 int entries, i;4
5 entries = ARRAY_SIZE(drive_config_groups);6 entries--; /* keep list NULL terminated */7 for (i = 0; i < entries; i++) {8 if (drive_config_groups[i] == NULL) {9 drive_config_groups[i] = list;10 return;11 }12 }13 fprintf(stderr, "ran out of space in drive_config_groups");14 abort();15}16 collapsed lines
16
17void qemu_add_opts(QemuOptsList *list)18{19 int entries, i;20
21 entries = ARRAY_SIZE(vm_config_groups);22 entries--; /* keep list NULL terminated */23 for (i = 0; i < entries; i++) {24 if (vm_config_groups[i] == NULL) {25 vm_config_groups[i] = list;26 return;27 }28 }29 fprintf(stderr, "ran out of space in vm_config_groups");30 abort();31}
命令行参数的解析
在 qemu 中,参数解析在 vl.c
中的 qemu_init
函数中,参数的解析分为两部分:
- 第一部分检查选项是否是 QEMU 中预定义的
QEMUOption
,并初始化machine_opts_dict
数组,根据是否传入了-no-user-config
参数来加载用户配置。 - 真正解析具体参数并执行相应设置
第一阶段
首先按照下标顺序依次读取终端传入的参数数组,跳过子选项,只解析主选项。通过 lookup_opt
函数查询主选项是否是预定义的 QEMUOption
,如果没找到,退出程序,如果找到,则返回找到的 QEMUOption
指针。虽然 lookup_opt
函数也会保存主选项对应的子选项参数数组的指针到 optarg
,但是第一阶段并不会使用。
如果在解析主选项过程中,检查到有主选项-no-user-config
,后续就跳过加载用户配置,否则还会加载用户配置。然后会初始化 machine_opts_dict
数组,这里的 machine_opts_dict
是一个字典结构,主要用于存储终端传入的参数数组中的虚拟机选项和参数,包括 CPU 数量、内存大小、设备配置等。machine_opts_dict
的存在使得参数解析机制能够以一种结构化的方式管理和访问虚拟机参数,而不是使用分散的单独变量或者凌乱的数据结构。
1 /* first pass of option parsing */2 optind = 1;3 while (optind < argc) {4 if (argv[optind][0] != '-') {5 /* disk image */6 optind++;7 } else {8 const QEMUOption *popt;9
10 popt = lookup_opt(argc, argv, &optarg, &optind);11 switch (popt->index) {12 case QEMU_OPTION_nouserconfig:13 userconfig = false;14 break;15 }7 collapsed lines
16 }17 }18
19 machine_opts_dict = qdict_new();20 if (userconfig) {21 qemu_read_default_config_file(&error_fatal);22 }
lookup_opt
函数定义如下,由于qemu_options
定义时,最后一个元素为{ }
,在遍历时发现 popt->name
为空还没有匹配到主选项,就可以认为该主选项非法。lookup_opt
函数若匹配到当前的主选项,且主选项有子选项,则将子选项数组的指针保存到 poptarg
并返回给上层函数。lookup_opt
同时还会将已经遍历的 optind
的值也返回给上层函数,表示已经遍历过参数数组的这些参数。
1static const QEMUOption *lookup_opt(int argc, char **argv,2 const char **poptarg, int *poptind)3{4 const QEMUOption *popt;5 int optind = *poptind;6 char *r = argv[optind];7 const char *optarg;8
9 loc_set_cmdline(argv, optind, 1);10 optind++;11 /* Treat --foo the same as -foo. */12 if (r[1] == '-')13 r++;14 popt = qemu_options;15 for(;;) {24 collapsed lines
16 if (!popt->name) {17 error_report("invalid option");18 exit(1);19 }20 if (!strcmp(popt->name, r + 1))21 break;22 popt++;23 }24 if (popt->flags & HAS_ARG) {25 if (optind >= argc) {26 error_report("requires an argument");27 exit(1);28 }29 optarg = argv[optind++];30 loc_set_cmdline(argv, optind - 2, 2);31 } else {32 optarg = NULL;33 }34
35 *poptarg = optarg;36 *poptind = optind;37
38 return popt;39}
第二阶段
真正开始解析参数。
1 /* second pass of option parsing */2 optind = 1;3 for(;;) {4 if (optind >= argc)5 break;6 if (argv[optind][0] != '-') {7 loc_set_cmdline(argv, optind, 1);8 drive_add(IF_DEFAULT, 0, argv[optind++], HD_OPTS);9 } else {10 const QEMUOption *popt;11
12 popt = lookup_opt(argc, argv, &optarg, &optind);13 if (!(popt->arch_mask & arch_type)) {14 error_report("Option not supported for this target");15 exit(1);11 collapsed lines
16 }17 switch(popt->index) {18 case QEMU_OPTION_cpu:19 ...20 break;21 ...22 default:23 ...24 }25 }26 }
重新按照下标顺序依次遍历终端传入的参数数组,调用 lookup_opt
函数找到对应的 QEMUOption
,然后检查对应选项在当前架构下是否支持,最后使用 switch
语句根据 QEMUOption
的成员变量 index
的不同来执行不同的分支完成具体的设置。需要注意,主选项和子选项是成对出现的,在 lookup_opt
函数中也是成对解析,如果发现子选项进入了主循环,则默认为 driver
。
参考资料
- QEMU 参数解析机制简析
- 《QEMU/KVM 源码解析与应用》李强,机械工业出版社