QEMU学习笔记(2020-12-03)
QEMU学习笔记
1 QEMU构建系统架构
1.1 Makefiles
QEMU 构建系统需要使用 GNU make。
QEMU 当前支持 VPATH 和非 VPATH 构建,因此有三种通用方式调用 configure 并执行构建。
VPATH,完全在 QEMU 源码树之外构建产品
$ cd ../
$ mkdir build
$ cd build
$ ../qemu/configure
$ make
VPATH,在 QEMU 源码树的一个子目录中构建产品
$ mkdir build
$ cd build
$ ../configure
$ make
非 VPATH,在任何地方构建产品
$ ./configure
$ make
QEMU 的维护者通常建议开发者使用 VPATH 构建。QEMU 的补丁期待确保 VPATH 构建依然有效。
1.2 模块结构
QEMU 构建系统有大量的重要输出:
- 工具 - qemu-img,qemu-nbd,qga (guest agent),等等
- 系统模拟器 - qemu-system-$ARCH
- 用户空间模拟器 - qemu-$ARCH
- 单元测试
源码是高度模块化的,分割多个文件,以便尽可能少地重复编译所有这些组件。可以认为是两个不同的文件组,包括独立于 QEMU 仿真目标的文件和依赖于QEMU 仿真目标的文件组。
独立于仿真目标的文件集合中是各种通用辅助代码,比如错误处理基础设施,标准数据结构,平台移植性封装函数,等等。这些代码可以只被编译一次,而把它们的 .o 文件链接到所有的输出二进制文件。
依赖于仿真目标的文件集合中是 CPU 模拟,设备模拟和许多胶水代码。这有时还不得不编译多次,为每个目标编译一次。
所有二进制文件都用到的实用代码被编译为一个称为 libqemuutil.a 静态包,它会被链接进所有的二进制文件。为了提供只有一部分二进制文件需要的钩子,libqemuutil.a 中的代码可能依赖于其它不完全由 QEMU 二进制实现的函数。为了处理这种情况,还有另一个称为 libqemustub.a 的库,它为所有这些函数提供了 dummy stubs。如果没有真正的实现,则它们将被延迟链接进二进制中。以这种方式,libqemustub.a 静态库可以被看作一个弱符号概念的可移植实现。所有的二进制文件应该同时链接 libqemuutil.a 和 libqemustub.a。比如
qemu-img$(EXESUF): qemu-img.o ..snippet.. libqemuutil.a libqemustub.a
1.3 目标变量命名
QEMU 用约定变量来列出不同的目标文件组的。它们的命名约定为 $PREFIX-obj-y。比如,libqemuutil.a 文件将与变量 util-obj-y 列出的所有目标文件链接。因此,比如,util/Makefile.obj 将包含一系列看起来像这样的定义:
util-obj-y += bitmap.o bitops.o hbitmap.o
util-obj-y += fifo8.o
util-obj-y += acl.o
util-obj-y += error.o qemu-error.o
当有一个目标文件需要基于主机系统的一些特性有条件地构建时,配置脚本将条件定义一个变量。比如,在 Windows 上它将定义 $(CONFIG_POSIX)
值为 'n',而 $(CONFIG_WIN32)
值为 'y'。现在可以在列出目标文件时使用配置变量了。比如,
util-obj-$(CONFIG_WIN32) += oslib-win32.o qemu-thread-win32.o
util-obj-$(CONFIG_POSIX) += oslib-posix.o qemu-thread-posix.o
在 Windows 上被扩展为
util-obj-y += oslib-win32.o qemu-thread-win32.o
util-obj-n += oslib-posix.o qemu-thread-posix.o
由于 libqemutil.a 被链接进 $(util-obj-y)
,在 Windows 平台构建中,$(util-obj-n)
中列出的 POSIX 特有文件被忽略。
2 模块的注册与加载
2.1 模块的注册
QEMU 4.2.0 版本的入口在 vl.c
的main
函数中,其中在vl.c
的 2885 行是module_call_init(MODULE_INIT_QOM);
。
module_call_init
在include/qemu/module.h
中声明,在util/module.c
中定义,其作用是遍历init_type_list[MODULE_INIT_QOM]
调用所有已注册 QOM(QEMU Object Module) 模块的init
函数,完成 QOM 模块的初始化。
QOM 模块的注册在每个模块文件末尾的type_init
中进行,type_init
通过include/qemu/module.h#module_init
宏定义的__attribute__((constructor))
将注册函数转换成构造函数,在运行main
入口函数之前完成注册。
以hw/virtio/virtio-blk-pci.c
为例,module_call_init会调用init,而init实际指向virtio_blk_pci_register, virtio_blk_pci_register最终会将设备对应的TypeImpl注册到哈希表中(以TypeInfo的name属性为索引)
2.2 参数解析
两次循环中解析argv,将解析结果保存到QemuOpts链表中
2.3 模块加载
1. 在main函数中会遍历指定的-device参数,然后调用device_init_func来做设备的初始化。
2. 会依次调用device_init_func-> qdev_device_add -> object_new -> object_initialize_with_type。主要的初始化工作都是在object_initialize_with_type中完成的。
3. 首先会调用type_initialize完成类的初始化。在类的初始化中会设置类的realize回调函数为virtio_balloon_pci_realize。该函数在做类对象的实例化的时候会调用。
4. 之后会调用object_init_with_type做类对象的初始化。会递归从父对象开始执行instance_init。TYPE_DEVICE的instance_init函数为device_initfn。该函数调用执行了object_property_add_bool增加了三个属性。其中realized属性是当设置了对象真正创建的时候调用的。其set回调函数设置为device_set_realized。任何一个设备创建的时候都会调用该函数。
image5. 设备类和对象实例初始化完成后会回到qdev_device_add函数,接下来就会进行具体的设备实现过程。
6. 调用object_property_set_bool将realized属性设置为true,从而会调用刚才设置的回调函数device_set_realized。
7. 会首先调用DeviceClass的realize函数即virtio_pci_dc_realize。该函数是在virtio_pci_class_init中设置的。
image8. 父类的realize函数会一次调用子类的realize函数。接着会调用PCIDeviceClass的realize函数即pci_qdev_realize。
9. 然后依次调用VirtioPCIClass、VirtioBalloonPCIClass的realize函数。从而实现了VirtioBalloonPCI设备的创建。最后还需要创建VirtioBalloon设备,将其挂载到PCI总线上。
10. 即依次调用VirtioDeviceClass、VirtioBalloonDeviceClass的realize函数。VirtioBalloon设备的具体实现就是在函数virtio_balloon_device_realize中。
整个流程如下图所示,类之间的调用流则如类继承关系图中红线所示。
image在virtio_balloon_device_realize中,调用virtio_add_queue为设备添加了三个virtqueue。其中两个的回调处理函数为virtio_ballon_handle_output,另一个是virtio_balloon_receive_stats。从而构建了从设备的输入输出通道,以及当收到输入输出信息时调用的回调处理函数。
image至此设备创建完成,但是还差最后一步将设备挂载到virtio总线上去。该步骤是在函数virtio_device_realize中实现的。该函数调用vdc->realize创建了具体设备后,会调用virtio_bus_device_plugged,该函数的作用就是将virtio设备插入到virtio总线。