虚拟机创建流程-qemu篇(上)

达芬奇密码2018-07-12 09:13

在kvm+qemu架构下,qemu负责模拟虚拟机所有的硬件设备,并与kvm交互。qemu是云计算中虚拟化的最终执行者,通过openstack,libvirt等封装的各种设备配置都需要qemu模拟并运行。本文会通过解析qemu在虚拟机创建过程中的流程来向大家介绍一下qemu的大致工作流程及其工作原理。

从libvirt到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架构下,qemu负责各种设备的模拟,kvm则负责保障虚拟机中的代码可以正常执行。具体来说,kvm暴露一个设备文件接口/dev/kvm给用户态的qemu进程。而qemu进程通过系统调用ioctl操作kvm接口,完成一些需要真实硬件参与的虚拟机操作。

vcpu运行

现在使用的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数据域。在数据域中包括如下各种信息:

  • 客户机状态域 在虚拟机内运行时,即非root模式下CPU的状态。vm-exit发生时,cpu当前的状态会存储到客户机状态域。vm-entry发生时,从客户机状态域恢复cpu状态。
  • 宿主机状态域 在VMM运行时,即root模式下CPU的状态。vm-exit发生时,cpu从这里恢复cpu运行状态。
  • vm-entry控制域 控制vm-entry的过程
  • vm-execution控制域 控制非根模式下的行为
  • vm-exit控制域 控制vm-exit的过程
  • vm-exit信息域 提供vm-exit的原因和其他信息,只读域。

内存访问

在虚拟化场景下,虚拟机内部如果需要访问一段内存,需要经过两步映射才能找到真正的物理地址: Guest虚拟机地址(GVA)->Guest物理地址(GPA)->宿主机虚拟地址(HVA)->宿主机物理地址(HPA)

影子页表

在hypervisor中维护一张内存影子页表,根据GVA-GPA-HVA-HPA的映射关系直接计算GVA-HPA的映射关系,并将对应的映射关系写入影子页表。这样可以解决虚拟机内存访问的问题,但是依赖软件实现的影子页表也带来了很多问题。像各种页表之间的同步问题,页表本身的内存开销等。

EPT

EPT页表利用硬件实现了从GPA到HPA的映射,每个虚拟机只需要维护一个EPT页表即可。减少了开销,提高了性能。

代码流程分析

qemu command命令运行之后,首先进入qemu进程的入口--vl.c文件的main函数中。main函数大致执行流程如下:

  • 初始化各种设备的初始化入口函数
  • 解析qemu command传入的各种参数
  • 初始化加速器(与kmod交互)
  • 初始化后端设备
  • 初始化芯片组(vcpu,内存,南桥,北桥,bios)
  • 进入main_loop

下面对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;

这些代码我们需要按照如下的顺序来看:

  • qemu定义各种设备类型
  • 初始化一个ModuleTypeList类型的链表数组
  • 通过find_type函数可以获取指定设备类型的列表。
  • register_module_init中把参数指定的设备加入到其所属设备类型的链表中
  • 把上面的函数封装到一个函数中,并且这个函数添加了gcc的attribute:__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支持的设备都使用这种规范。我们可以看到在初始化的链表数组中还有其它类型的设备,后面会涉及到。

  1. 初始化qemu记录命令行参数的数据结构,等待下面解析qemu command。这里为所有可能的qemu command参数准备了存储结构。
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);
  1. 初始化qemu管理的虚拟机运行状态
#qemu中通过一个二维数组记录虚拟机的状态变化,这个二维数组中记录了所有可能的状态变化,第一维表示初始状态,第二维表示目标状态。
runstate_init();
  1. 忽略早期pipe信号
os_setup_early_signal_handling
  1. 初始化芯片组入口信息,使用的仍然是第一步中已经分析过的module_init方式。但是这里指定的初始化类型是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);
  1. 获取当前arch下的默认芯片组型号。qemu本身支持多种arch,在初始化时根据执行的二进制文件只会初始化某一个arch。而每一个arch中都会有一个具体的型号作为默认的芯片组型号,一般都是当前支持的最新版本。
machine_class = find_default_machine();
  1. 初始化block driver入口。使用的仍然是module_init方式。这里的block driver即我们在使用file disk的时候指定的各种driver类型,如qcow2,raw等。
bdrv_init_with_whitelist();
  1. 以上qemu已经执行了6个关键步骤,但都是一些基本的初始化操作,在物理节点上每一个虚拟机启动都会执行完全一样的操作。而区分不同虚拟机的qemu command参数到这里为止还没有解析。接下来会先遍历一遍qemu command中的参数,根据参数确定是否使用预先配置在/etc/qemu/target-{arch}.conf文件中的配置参数。
if (defconfig) {
        int ret;
        ret = qemu_read_default_config_files(userconfig);
        if (ret < 0) {
            exit(1);
        }
    }
  1. 接下来真正解析qemu command中配置的各种参数 通过一个for循环,解析结果放入vm_config_group

  2. 初始化main loop

初始化main_loop中使用的时钟。在当前的架构下,qemu中需要维护三种时间:

  • QEMU_CLOCK_REALTIME RTC
  • QEMU_CLOCK_VIRTUAL 虚拟机运行时间
  • QEMU_CLOCK_HOST 宿主机时间

注册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篇(下)

本文来自网易实践者社区,经作者岳文远授权发布。