在kvm+qemu架构下,qemu负责模拟虚拟机所有的硬件设备,并与kvm交互。qemu是云计算中虚拟化的最终执行者,通过openstack,libvirt等封装的各种设备配置都需要qemu模拟并运行。本文会通过解析qemu在虚拟机创建过程中的流程来向大家介绍一下qemu的大致工作流程及其工作原理。
在上一篇中我们分析了libvirt中创建虚拟机的流程,在最后阶段libvirt组装了qemu command,并通过fork调用拉起qemu进程,在宿主机上可以看到这样一个进程:
/usr/bin/qemu-system-x86_64
-name guest=instance-000439eb,debug-threads=on
-S
-machine pc-i440fx-2.5,accel=kvm,usb=off
-cpu IvyBridge,+ds,+acpi,+ss,+ht,+tm,+pbe,+dtes64,+monitor,+ds_cpl,+vmx,+smx,+est,+tm2,+xtpr,+pdcm,+pcid,+dca,+osxsave,+pdpe1gb
-m size=1048576k,slots=64,maxmem=268435456k
-realtime mlock=off
-smp 1,maxcpus=64,sockets=64,cores=1,threads=1
-numa node,nodeid=0,cpus=0-63,mem=1024
-uuid 2178112f-1e08-4a0b-b495-3bcc8faf3d59
-smbios type=1,manufacturer=OpenStack Foundation,product=OpenStack Nova,version=2013.2-netease.910,serial=44454c4c-3900-1057-8032-b6c04f373232,uuid=2178112f-1e08-4a0b-b495-3bcc8faf3d59
-drive file=rbd:vms/2178112f-1e08-4a0b-b495-3bcc8faf3d59_disk:auth_supported=none:mon_host=10.180.0.47\:6789\;10.180.0.48\:6789\;10.180.0.49\:6789,format=raw,if=none,id=drive-virtio-disk0,cache=none
-device virtio-blk-pci,scsi=off,bus=pci.0,addr=0x5,drive=drive-virtio-disk0,id=virtio-disk0,bootindex=1
-drive file=rbd:vms/2178112f-1e08-4a0b-b495-3bcc8faf3d59_disk.config:auth_supported=none:mon_host=10.180.0.47\:6789\;10.180.0.48\:6789\;10.180.0.49\:6789,format=raw,if=none,id=drive-virtio-disk25,readonly=on,cache=none
-device virtio-blk-pci,scsi=off,bus=pci.0,addr=0x6,drive=drive-virtio-disk25,id=virtio-disk25
-netdev tap,fd=179,id=hostnet0,vhost=on,vhostfd=183
-device virtio-net-pci,netdev=hostnet0,id=net0,mac=fa:16:3e:38:e9:53,bus=pci.0,addr=0x3
-chardev file,id=charserial0,path=/data/nova/instances/2178112f-1e08-4a0b-b495-3bcc8faf3d59/console.log
-device isa-serial,chardev=charserial0,id=serial0
-chardev pty,id=charserial1 -device isa-serial,chardev=charserial1,id=serial1
-chardev socket,id=charchannel0,path=/var/lib/libvirt/qemu/org.qemu.guest_agent.0.instance-000439eb.sock,server,nowait
-device virtserialport,bus=virtio-serial0.0,nr=1,chardev=charchannel0,id=channel0,name=org.qemu.guest_agent.0
-device usb-tablet,id=input0
-vnc 10.180.0.47:64,password -k en-us
-device cirrus-vga,id=video0,bus=pci.0,addr=0x2
-device virtio-balloon-pci,id=balloon0,bus=pci.0,addr=0x7
-msg timestamp=on
-no-user-config
-nodefaults
-chardev socket,id=charmonitor,path=/var/lib/libvirt/qemu/domain-408-instance-000439eb/monitor.sock,server,nowait
-mon chardev=charmonitor,id=monitor,mode=control
-rtc base=utc,driftfix=slew
-global kvm-pit.lost_tick_policy=discard
-no-shutdown
-boot strict=on
-device piix3-usb-uhci,id=usb,bus=pci.0,addr=0x1.0x2
-device virtio-serial-pci,id=virtio-serial0,bus=pci.0,addr=0x4
可以看到,qemu进程的可执行文件是qemu-system-x86_64,该文件是x86_64架构的模拟器。
在qemu kvm架构下,qemu负责各种设备的模拟,kvm则负责保障虚拟机中的代码可以正常执行。具体来说,kvm暴露一个设备文件接口/dev/kvm
给用户态的qemu进程。而qemu进程通过系统调用ioctl操作kvm接口,完成一些需要真实硬件参与的虚拟机操作。
现在使用的x86架构的虚拟化技术利用了intel的VT-x技术。vt-x的基本思想是区分cpu的工作模式,root和非root模式。每一种模式又分为0-3四个特权级。在虚拟机中cpu运行在非root模式下,当执行敏感指令时,cpu会自动从非root模式切换到root模式,称为vm-exit。对应的,VMM也会发起从root模式到非root模式的切换,称为vm-entry。VT-x还引入了VMCS的概念,用户保存cpu在各种模式下的运行状态,方便cpu在多种模式下的切换。VMCS在系统中存储在一块最大不超过4kB大小的内存中,内容包括VMCS版本号,VMCS中止标识以及VMCS数据域。在数据域中包括如下各种信息:
在虚拟化场景下,虚拟机内部如果需要访问一段内存,需要经过两步映射才能找到真正的物理地址: Guest虚拟机地址(GVA)->Guest物理地址(GPA)->宿主机虚拟地址(HVA)->宿主机物理地址(HPA)
在hypervisor中维护一张内存影子页表,根据GVA-GPA-HVA-HPA的映射关系直接计算GVA-HPA的映射关系,并将对应的映射关系写入影子页表。这样可以解决虚拟机内存访问的问题,但是依赖软件实现的影子页表也带来了很多问题。像各种页表之间的同步问题,页表本身的内存开销等。
EPT页表利用硬件实现了从GPA到HPA的映射,每个虚拟机只需要维护一个EPT页表即可。减少了开销,提高了性能。
qemu command命令运行之后,首先进入qemu进程的入口--vl.c文件的main函数中。main函数大致执行流程如下:
下面对main函数做具体的分析:
1.初始化QOM设备
module_call_init(MODULE_INIT_QOM);
打开这个函数的代码可以看到如下的内容,看起来非常简单:
void module_call_init(module_init_type type)
{
ModuleTypeList *l;
ModuleEntry *e;
module_load(type);
l = find_type(type);
QTAILQ_FOREACH(e, l, node) {
e->init();
}
}
这个函数实现的功能就是执行ModuleTypeList类型的链表中每一个节点的init函数。可是我们现在是在一个二进制文件的入口main函数中,并没有初始化这样一个链表么~那么这个链表中的内容是怎么来的呢。通过搜索代码我们可以看到很多设备文件中最后都会调用一个type_init函数:
type_init(virtio_register_types)
#define type_init(function) module_init(function, MODULE_INIT_QOM)
/* This should not be used directly. Use block_init etc. instead. */
#define module_init(function, type) \
static void __attribute__((constructor)) do_qemu_init_ ## function(void) \
{ \
register_module_init(function, type); \
}
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);
}
static ModuleTypeList *find_type(module_init_type type)
{
ModuleTypeList *l;
init_lists();
l = &init_type_list[type];
return l;
}
static ModuleTypeList init_type_list[MODULE_INIT_MAX];
typedef enum {
MODULE_INIT_BLOCK,
MODULE_INIT_MACHINE,
MODULE_INIT_QAPI,
MODULE_INIT_QOM,
MODULE_INIT_MAX
} module_init_type;
这些代码我们需要按照如下的顺序来看:
__attribute__((constructor))
,其含义是在整个程序的main函数执行之前该函数就会被执行。至此我们就可以看到,在main函数开始执行之前,init_type_list
链表就已经初始化完成。因此上面的module_call_init(MODULE_INIT_QOM);
就可以遍历所有的QOM设备并执行他们的init函数,以virtio-blk函数为例,init的执行内容如下。就是注册一下当前的设备类型,设备总线,以及其它一些相关的初始化函数。
static const TypeInfo virtio_device_info = {
.name = TYPE_VIRTIO_BLK,
.parent = TYPE_VIRTIO_DEVICE,
.instance_size = sizeof(VirtIOBlock),
.instance_init = virtio_blk_instance_init,
.class_init = virtio_blk_class_init,ß
};
static void virtio_register_types(void)
{
type_register_static(&virtio_device_info);
}
type_init(virtio_register_types)
QOM到底指的是什么设备呢?QOM即qemu object model,qemu设备模型,是一种qemu设备模拟的规范。目前基本上所有qemu支持的设备都使用这种规范。我们可以看到在初始化的链表数组中还有其它类型的设备,后面会涉及到。
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_opts(&qemu_chardev_opts);
qemu_add_opts(&qemu_device_opts);
qemu_add_opts(&qemu_netdev_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_add_opts(&qemu_option_rom_opts);
qemu_add_opts(&qemu_machine_opts);
qemu_add_opts(&qemu_mem_opts);
qemu_add_opts(&qemu_smp_opts);
qemu_add_opts(&qemu_boot_opts);
qemu_add_opts(&qemu_sandbox_opts);
qemu_add_opts(&qemu_add_fd_opts);
qemu_add_opts(&qemu_object_opts);
qemu_add_opts(&qemu_tpmdev_opts);
qemu_add_opts(&qemu_realtime_opts);
qemu_add_opts(&qemu_msg_opts);
qemu_add_opts(&qemu_name_opts);
qemu_add_opts(&qemu_numa_opts);
#qemu中通过一个二维数组记录虚拟机的状态变化,这个二维数组中记录了所有可能的状态变化,第一维表示初始状态,第二维表示目标状态。
runstate_init();
os_setup_early_signal_handling
MODULE_INIT_MACHINE
module_call_init(MODULE_INIT_MACHINE);
我们现在使用的默认主板类型是pc-i440fx-2.5
,通过代码搜索我们可以直接找到pc_piix.c
文件,这个文件就是用于模拟Intel piix系列芯片组的。在这个文件的最后通过module_init在main函数执行之前注册链表中的初始化函数。在main函数执行到初始化machine的时候,会注册qemu支持的所有Intel piix芯片组的初始化入口。这里使用的代码版本比较低,还没有支持我们使用的i440fx-2.5
版本的芯片组。我们主要是分析逻辑,具体的版本差异就先不考虑了。
static void pc_machine_init(void)
{
qemu_register_pc_machine(&pc_i440fx_machine_v2_1);
qemu_register_pc_machine(&pc_i440fx_machine_v2_0);
qemu_register_pc_machine(&pc_i440fx_machine_v1_7);
qemu_register_pc_machine(&pc_i440fx_machine_v1_6);
qemu_register_pc_machine(&pc_i440fx_machine_v1_5);
qemu_register_pc_machine(&pc_i440fx_machine_v1_4);
qemu_register_pc_machine(&pc_machine_v1_3);
qemu_register_pc_machine(&pc_machine_v1_2);
qemu_register_pc_machine(&pc_machine_v1_1);
qemu_register_pc_machine(&pc_machine_v1_0);
qemu_register_pc_machine(&pc_machine_v0_15);
qemu_register_pc_machine(&pc_machine_v0_14);
qemu_register_pc_machine(&pc_machine_v0_13);
qemu_register_pc_machine(&pc_machine_v0_12);
qemu_register_pc_machine(&pc_machine_v0_11);
qemu_register_pc_machine(&pc_machine_v0_10);
qemu_register_pc_machine(&isapc_machine);
#ifdef CONFIG_XEN
qemu_register_pc_machine(&xenfv_machine);
#endif
}
machine_init(pc_machine_init);
machine_class = find_default_machine();
bdrv_init_with_whitelist();
/etc/qemu/target-{arch}.conf
文件中的配置参数。if (defconfig) {
int ret;
ret = qemu_read_default_config_files(userconfig);
if (ret < 0) {
exit(1);
}
}
接下来真正解析qemu command中配置的各种参数 通过一个for循环,解析结果放入vm_config_group
初始化main loop
初始化main_loop中使用的时钟。在当前的架构下,qemu中需要维护三种时间:
注册qemu进程信号量处理函数,qemu收到的进程信号会触发注册的sigfd_handler
回调函数
为main_loop监听的fd申请管理内存
gpollfds = g_array_new(FALSE, FALSE, sizeof(GPollFD));
cpu_exec_init_all 遗留 memory_map_init qemu进程设备模拟占用的内存申请及初始化 io_mem_init io rom内存空间申请及初始化。
相关阅读:虚拟机创建流程-qemu篇(下)
本文来自网易实践者社区,经作者岳文远授权发布。