上一节我们讲的 qemu 启动时候的命令。

  1. qemu-system-x86_64 -enable-kvm -name ubuntutest -m 2048 -hda ubuntutest.qcow2 -vnc :19 -net nic,model=virtio -nettap,ifname=tap0,script=no,downscript=no
  2. 复制代码

接下来,我们在这里下载qemu 的代码。qemu 的 main 函数在 vl.c 下面。这是一个非常非常长的函数,我们来慢慢地解析它。

1. 初始化所有的 Module

第一步,初始化所有的 Module,调用下面的函数。

  1. module_call_init(MODULE_INIT_QOM);
  2. 复制代码

上一节我们讲过,qemu 作为中间人其实挺累的,对上面的虚拟机需要模拟各种各样的外部设备。当虚拟机真的要使用物理资源的时候,对下面的物理机上的资源要进行请求,所以它的工作模式有点儿类似操作系统对接驱动。驱动要符合一定的格式,才能算操作系统的一个模块。同理,qemu 为了模拟各种各样的设备,也需要管理各种各样的模块,这些模块也需要符合一定的格式。
定义一个 qemu 模块会调用 type_init。例如,kvm 的模块要在 accel/kvm/kvm-all.c 文件里面实现。在这个文件里面,有一行下面的代码:

  1. type_init(kvm_type_init);
  2. #define type_init(function) module_init(function, MODULE_INIT_QOM)
  3. #define module_init(function, type) \
  4. static void __attribute__((constructor)) do_qemu_init_ ## function(void) \
  5. { \
  6. register_module_init(function, type); \
  7. }
  8. void register_module_init(void (*fn)(void), module_init_type type)
  9. {
  10. ModuleEntry *e;
  11. ModuleTypeList *l;
  12. e = g_malloc0(sizeof(*e));
  13. e->init = fn;
  14. e->type = type;
  15. l = find_type(type);
  16. QTAILQ_INSERT_TAIL(l, e, node);
  17. }
  18. 复制代码

从代码里面的定义我们可以看出来,type_init 后面的参数是一个函数,调用 type_init 就相当于调用 module_init,在这里函数就是 kvm_type_init,类型就是 MODULE_INIT_QOM。是不是感觉和驱动有点儿像?
module_init 最终要调用 register_module_init。属于 MODULE_INIT_QOM 这种类型的,有一个 Module 列表 ModuleTypeList,列表里面是一项一项的 ModuleEntry。KVM 就是其中一项,并且会初始化每一项的 init 函数为参数表示的函数 fn,也即 KVM 这个 module 的 init 函数就是 kvm_type_init。
当然,MODULE_INIT_QOM 这种类型会有很多很多的 module,从后面的代码我们可以看到,所有调用 type_init 的地方都注册了一个 MODULE_INIT_QOM 类型的 Module。
了解了 Module 的注册机制,我们继续回到 main 函数中 module_call_init 的调用。

  1. void module_call_init(module_init_type type)
  2. {
  3. ModuleTypeList *l;
  4. ModuleEntry *e;
  5. l = find_type(type);
  6. QTAILQ_FOREACH(e, l, node) {
  7. e->init();
  8. }
  9. }
  10. 复制代码

在 module_call_init 中,我们会找到 MODULE_INIT_QOM 这种类型对应的 ModuleTypeList,找出列表中所有的 ModuleEntry,然后调用每个 ModuleEntry 的 init 函数。这里需要注意的是,在 module_call_init 调用的这一步,所有 Module 的 init 函数都已经被调用过了。
后面我们会看到很多的 Module,当你看到它们的时候,你需要意识到,它的 init 函数在这里也被调用过了。这里我们还是以对于 kvm 这个 module 为例子,看看它的 init 函数都做了哪些事情。你会发现,其实它调用的是 kvm_type_init。

  1. static void kvm_type_init(void)
  2. {
  3. type_register_static(&kvm_accel_type);
  4. }
  5. TypeImpl *type_register_static(const TypeInfo *info)
  6. {
  7. return type_register(info);
  8. }
  9. TypeImpl *type_register(const TypeInfo *info)
  10. {
  11. assert(info->parent);
  12. return type_register_internal(info);
  13. }
  14. static TypeImpl *type_register_internal(const TypeInfo *info)
  15. {
  16. TypeImpl *ti;
  17. ti = type_new(info);
  18. type_table_add(ti);
  19. return ti;
  20. }
  21. static TypeImpl *type_new(const TypeInfo *info)
  22. {
  23. TypeImpl *ti = g_malloc0(sizeof(*ti));
  24. int i;
  25. if (type_table_lookup(info->name) != NULL) {
  26. }
  27. ti->name = g_strdup(info->name);
  28. ti->parent = g_strdup(info->parent);
  29. ti->class_size = info->class_size;
  30. ti->instance_size = info->instance_size;
  31. ti->class_init = info->class_init;
  32. ti->class_base_init = info->class_base_init;
  33. ti->class_data = info->class_data;
  34. ti->instance_init = info->instance_init;
  35. ti->instance_post_init = info->instance_post_init;
  36. ti->instance_finalize = info->instance_finalize;
  37. ti->abstract = info->abstract;
  38. for (i = 0; info->interfaces && info->interfaces[i].type; i++) {
  39. ti->interfaces[i].typename = g_strdup(info->interfaces[i].type);
  40. }
  41. ti->num_interfaces = i;
  42. return ti;
  43. }
  44. static void type_table_add(TypeImpl *ti)
  45. {
  46. assert(!enumerating_types);
  47. g_hash_table_insert(type_table_get(), (void *)ti->name, ti);
  48. }
  49. static GHashTable *type_table_get(void)
  50. {
  51. static GHashTable *type_table;
  52. if (type_table == NULL) {
  53. type_table = g_hash_table_new(g_str_hash, g_str_equal);
  54. }
  55. return type_table;
  56. }
  57. static const TypeInfo kvm_accel_type = {
  58. .name = TYPE_KVM_ACCEL,
  59. .parent = TYPE_ACCEL,
  60. .class_init = kvm_accel_class_init,
  61. .instance_size = sizeof(KVMState),
  62. };
  63. 复制代码

每一个 Module 既然要模拟某种设备,那应该定义一种类型 TypeImpl 来表示这些设备,这其实是一种面向对象编程的思路,只不过这里用的是纯 C 语言的实现,所以需要变相实现一下类和对象。
kvm_type_init 会注册 kvm_accel_type,定义上面的代码,我们可以认为这样动态定义了一个类。这个类的名字是 TYPE_KVM_ACCEL,这个类有父类 TYPE_ACCEL,这个类的初始化应该调用函数 kvm_accel_class_init(看,这里已经直接叫类 class 了)。如果用这个类声明一个对象,对象的大小应该是 instance_size。是不是有点儿 Java 语言反射的意思,根据一些名称的定义,一个类就定义好了。
这里的调用链为:kvm_type_init->type_register_static->type_register->type_register_internal。
在 type_register_internal 中,我们会根据 kvm_accel_type 这个 TypeInfo,创建一个 TypeImpl 来表示这个新注册的类,也就是说,TypeImpl 才是我们想要声明的那个 class。在 qemu 里面,有一个全局的哈希表 type_table,用来存放所有定义的类。在 type_new 里面,我们先从全局表里面根据名字找这个类。如果找到,说明这个类曾经被注册过,就报错;如果没有找到,说明这是一个新的类,则将 TypeInfo 里面信息填到 TypeImpl 里面。type_table_add 会将这个类注册到全局的表里面。到这里,我们注意,class_init 还没有被调用,也即这个类现在还处于纸面的状态。
这点更加像 Java 的反射机制了。在 Java 里面,对于一个类,首先我们写代码的时候要写一个 class xxx 的定义,编译好就放在.class 文件中,这也是出于纸面的状态。然后,Java 会有一个 Class 对象,用于读取和表示这个纸面上的 class xxx,可以生成真正的对象。
相同的过程在后面的代码中我们也可以看到,class_init 会生成 XXXClass,就相当于 Java 里面的 Class 对象,TypeImpl 还会有一个 instance_init 函数,相当于构造函数,用于根据 XXXClass 生成 Object,这就相当于 Java 反射里面最终创建的对象。和构造函数对应的还有 instance_finalize,相当于析构函数。
这一套反射机制放在 qom 文件夹下面,全称 QEMU Object Model,也即用 C 实现了一套面向对象的反射机制。
说完了初始化 Module,我们还回到 main 函数接着分析。

2. 解析 qemu 的命令行

第二步我们就要开始解析 qemu 的命令行了。qemu 的命令行解析,就是下面这样一长串。还记得咱们自己写过一个解析命令行参数的程序吗?这里的 opts 是差不多的意思。

  1. qemu_add_opts(&qemu_drive_opts);
  2. qemu_add_opts(&qemu_chardev_opts);
  3. qemu_add_opts(&qemu_device_opts);
  4. qemu_add_opts(&qemu_netdev_opts);
  5. qemu_add_opts(&qemu_nic_opts);
  6. qemu_add_opts(&qemu_net_opts);
  7. qemu_add_opts(&qemu_rtc_opts);
  8. qemu_add_opts(&qemu_machine_opts);
  9. qemu_add_opts(&qemu_accel_opts);
  10. qemu_add_opts(&qemu_mem_opts);
  11. qemu_add_opts(&qemu_smp_opts);
  12. qemu_add_opts(&qemu_boot_opts);
  13. qemu_add_opts(&qemu_name_opts);
  14. qemu_add_opts(&qemu_numa_opts);
  15. 复制代码

为什么有这么多的 opts 呢?这是因为,我们上一节给的参数都是简单的参数,实际运行中创建的 kvm 参数会复杂 N 倍。这里我们贴一个开源云平台软件 OpenStack 创建出来的 KVM 的参数,如下所示。不要被吓坏,你不需要全部看懂,只需要看懂一部分就行了。具体我来给你解析。

  1. qemu-system-x86_64
  2. -enable-kvm
  3. -name instance-00000024
  4. -machine pc-i440fx-trusty,accel=kvm,usb=off
  5. -cpu SandyBridge,+erms,+smep,+fsgsbase,+pdpe1gb,+rdrand,+f16c,+osxsave,+dca,+pcid,+pdcm,+xtpr,+tm2,+est,+smx,+vmx,+ds_cpl,+monitor,+dtes64,+pbe,+tm,+ht,+ss,+acpi,+ds,+vme
  6. -m 2048
  7. -smp 1,sockets=1,cores=1,threads=1
  8. ......
  9. -rtc base=utc,driftfix=slew
  10. -drive file=/var/lib/nova/instances/1f8e6f7e-5a70-4780-89c1-464dc0e7f308/disk,if=none,id=drive-virtio-disk0,format=qcow2,cache=none
  11. -device virtio-blk-pci,scsi=off,bus=pci.0,addr=0x4,drive=drive-virtio-disk0,id=virtio-disk0,bootindex=1
  12. -netdev tap,fd=32,id=hostnet0,vhost=on,vhostfd=37
  13. -device virtio-net-pci,netdev=hostnet0,id=net0,mac=fa:16:3e:d1:2d:99,bus=pci.0,addr=0x3
  14. -chardev file,id=charserial0,path=/var/lib/nova/instances/1f8e6f7e-5a70-4780-89c1-464dc0e7f308/console.log
  15. -vnc 0.0.0.0:12
  16. -device cirrus-vga,id=video0,bus=pci.0,addr=0x2
  17. 复制代码
  • -enable-kvm:表示启用硬件辅助虚拟化。
  • -name instance-00000024:表示虚拟机的名称。
  • -machine pc-i440fx-trusty,accel=kvm,usb=off:machine 是什么呢?其实就是计算机体系结构。不知道什么是体系结构的话,可以订阅极客时间的另一个专栏《深入浅出计算机组成原理》。
    qemu 会模拟多种体系结构,常用的有普通 PC 机,也即 x86 的 32 位或者 64 位的体系结构、Mac 电脑 PowerPC 的体系结构、Sun 的体系结构、MIPS 的体系结构,精简指令集。如果使用 KVM hardware-assisted virtualization,也即 BIOS 中 VD-T 是打开的,则参数中 accel=kvm。如果不使用 hardware-assisted virtualization,用的是纯模拟,则有参数 accel = tcg,-no-kvm。
  • -cpu SandyBridge,+erms,+smep,+fsgsbase,+pdpe1gb,+rdrand,+f16c,+osxsave,+dca,+pcid,+pdcm,+xtpr,+tm2,+est,+smx,+vmx,+ds_cpl,+monitor,+dtes64,+pbe,+tm,+ht,+ss,+acpi,+ds,+vme:表示设置 CPU,SandyBridge 是 Intel 处理器,后面的加号都是添加的 CPU 的参数,这些参数会显示在 /proc/cpuinfo 里面。
  • -m 2048:表示内存。
  • -smp 1,sockets=1,cores=1,threads=1:SMP 我们解析过,叫对称多处理器,和 NUMA 对应。qemu 仿真了一个具有 1 个 vcpu,一个 socket,一个 core,一个 threads 的处理器。
    socket、core、threads 是什么概念呢?socket 就是主板上插 cpu 的槽的数目,也即常说的“路”,core 就是我们平时说的“核”,即双核、4 核等。thread 就是每个 core 的硬件线程数,即超线程。举个具体的例子,某个服务器是:2 路 4 核超线程(一般默认为 2 个线程),通过 cat /proc/cpuinfo,我们看到的是 242=16 个 processor,很多人也习惯成为 16 核了。
  • -rtc base=utc,driftfix=slew:表示系统时间由参数 -rtc 指定。
  • -device cirrus-vga,id=video0,bus=pci.0,addr=0x2:表示显示器用参数 -vga 设置,默认为 cirrus,它模拟了 CL-GD5446PCI VGA card。
  • 有关网卡,使用 -net 参数和 -device。
  • 从 HOST 角度:-netdev tap,fd=32,id=hostnet0,vhost=on,vhostfd=37。
  • 从 GUEST 角度:-device virtio-net-pci,netdev=hostnet0,id=net0,mac=fa:16:3e:d1:2d:99,bus=pci.0,addr=0x3。
  • 有关硬盘,使用 -hda -hdb,或者使用 -drive 和 -device。
  • 从 HOST 角度:-drive file=/var/lib/nova/instances/1f8e6f7e-5a70-4780-89c1-464dc0e7f308/disk,if=none,id=drive-virtio-disk0,format=qcow2,cache=none
  • 从 GUEST 角度:-device virtio-blk-pci,scsi=off,bus=pci.0,addr=0x4,drive=drive-virtio-disk0,id=virtio-disk0,bootindex=1
  • -vnc 0.0.0.0:12:设置 VNC。

在 main 函数中,接下来的 for 循环和大量的 switch case 语句,就是对于这些参数的解析,我们不一一解析,后面真的用到这些参数的时候,我们再仔细看。

3. 初始化 machine

回到 main 函数,接下来是初始化 machine。

machine_class = select_machine();
current_machine = MACHINE(object_new(object_class_get_name(
                          OBJECT_CLASS(machine_class))));
复制代码

这里面的 machine_class 是什么呢?这还得从 machine 参数说起。

-machine pc-i440fx-trusty,accel=kvm,usb=off
复制代码

这里的 pc-i440fx 是 x86 机器默认的体系结构。在 hw/i386/pc_piix.c 中,它定义了对应的 machine_class。

DEFINE_I440FX_MACHINE(v4_0, "pc-i440fx-4.0", NULL,
                      pc_i440fx_4_0_machine_options);
#define DEFINE_I440FX_MACHINE(suffix, name, compatfn, optionfn) \
    static void pc_init_##suffix(MachineState *machine) \
    { \
......
        pc_init1(machine, TYPE_I440FX_PCI_HOST_BRIDGE, \
                 TYPE_I440FX_PCI_DEVICE); \
    } \
    DEFINE_PC_MACHINE(suffix, name, pc_init_##suffix, optionfn)
#define DEFINE_PC_MACHINE(suffix, namestr, initfn, optsfn) \
    static void pc_machine_##suffix##_class_init(ObjectClass *oc, void *data
) \
    { \
        MachineClass *mc = MACHINE_CLASS(oc); \
        optsfn(mc); \
        mc->init = initfn; \
    } \
    static const TypeInfo pc_machine_type_##suffix = { \
        .name       = namestr TYPE_MACHINE_SUFFIX, \
        .parent     = TYPE_PC_MACHINE, \
        .class_init = pc_machine_##suffix##_class_init, \
    }; \
    static void pc_machine_init_##suffix(void) \
    { \
        type_register(&pc_machine_type_##suffix); \
    } \
    type_init(pc_machine_init_##suffix)
复制代码

为了定义 machine_class,这里有一系列的宏定义。入口是 DEFINE_I440FX_MACHINE。这个宏有几个参数,v4_0 是后缀,”pc-i440fx-4.0”是名字,pc_i440fx_4_0_machine_options 是一个函数,用于定义 machine_class 相关的选项。这个函数定义如下:

static void pc_i440fx_4_0_machine_options(MachineClass *m)
{
    pc_i440fx_machine_options(m);
    m->alias = "pc";
    m->is_default = 1;
}
static void pc_i440fx_machine_options(MachineClass *m)
{
    PCMachineClass *pcmc = PC_MACHINE_CLASS(m);
    pcmc->default_nic_model = "e1000";
    m->family = "pc_piix";
    m->desc = "Standard PC (i440FX + PIIX, 1996)";
    m->default_machine_opts = "firmware=bios-256k.bin";
    m->default_display = "std";
    machine_class_allow_dynamic_sysbus_dev(m, TYPE_RAMFB_DEVICE);
}
复制代码

我们先不看 pci440fx_4_0_machine_options,先来看 DEFINE_I440FX_MACHINE。
这里面定义了一个 pc_init
##suffix,也就是 pcinit_v4_0。这里面转而调用 pc_init1。注意这里这个函数只是定义了一下,没有被调用。
接下来,DEFINE_I440FX_MACHINE 里面又定义了 DEFINE_PC_MACHINE。它有四个参数,除了 DEFINE_I440FX_MACHINE 传进来的三个参数以外,多了一个 initfn,也即初始化函数,指向刚才定义的 pc_init
##suffix。
在 DEFINEPC_MACHINE 中,我们定义了一个函数 pc_machine##suffix##classinit。从函数的名字 class_init 可以看出,这是 machine_class 从纸面上的 class 初始化为 Class 对象的方法。在这个函数里面,我们可以看到,它创建了一个 MachineClass 对象,这个就是 Class 对象。MachineClass 对象的 init 函数指向上面定义的 pc_init##suffix,说明这个函数是 machine 这种类型初始化的一个函数,后面会被调用。
接着,我们看 DEFINE_PC_MACHINE。它定义了一个 pc_machine_type
##suffix 的 TypeInfo。这是用于生成纸面上的 class 的原材料,果真后面调用了 type_init。
看到了 type_init,我们应该能够想到,既然它定义了一个纸面上的 class,那上面的那句 module_call_init,会和我们上面解析的 type_init 是一样的,在全局的表里面注册了一个全局的名字是”pc-i440fx-4.0”的纸面上的 class,也即 TypeImpl。
现在全局表中有这个纸面上的 class 了。我们回到 select_machine。

static MachineClass *select_machine(void)
{
    MachineClass *machine_class = find_default_machine();
    const char *optarg;
    QemuOpts *opts;
......
    opts = qemu_get_machine_opts();
    qemu_opts_loc_restore(opts);
    optarg = qemu_opt_get(opts, "type");
    if (optarg) {
        machine_class = machine_parse(optarg);
    }
......
    return machine_class;
}
MachineClass *find_default_machine(void)
{
    GSList *el, *machines = object_class_get_list(TYPE_MACHINE, false);
    MachineClass *mc = NULL;
    for (el = machines; el; el = el->next) {
        MachineClass *temp = el->data;
        if (temp->is_default) {
            mc = temp;
            break;
        }
    }
    g_slist_free(machines);
    return mc;
}
static MachineClass *machine_parse(const char *name)
{
    MachineClass *mc = NULL;
    GSList *el, *machines = object_class_get_list(TYPE_MACHINE, false);
    if (name) {
        mc = find_machine(name);
    }
    if (mc) {
        g_slist_free(machines);
        return mc;
    }
......
}
复制代码

在 select_machine 中,有两种方式可以生成 MachineClass。一种方式是 find_default_machine,找一个默认的;另一种方式是 machine_parse,通过解析参数生成 MachineClass。无论哪种方式,都会调用 object_class_get_list 获得一个 MachineClass 的列表,然后在里面找。object_class_get_list 定义如下:

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;
}
void object_class_foreach(void (*fn)(ObjectClass *klass, void *opaque), const char *implements_type, bool include_abstract,
                          void *opaque)
{
    OCFData data = { fn, implements_type, include_abstract, opaque };
    enumerating_types = true;
    g_hash_table_foreach(type_table_get(), object_class_foreach_tramp, &data);
    enumerating_types = false;
}
复制代码

在全局表 type_table_get() 中,对于每一项 TypeImpl,我们都执行 object_class_foreach_tramp。

static void object_class_foreach_tramp(gpointer key, gpointer value,
                                       gpointer opaque)
{
    OCFData *data = opaque;
    TypeImpl *type = value;
    ObjectClass *k;
    type_initialize(type);
    k = type->class;
......
    data->fn(k, data->opaque);
}
static void type_initialize(TypeImpl *ti)
{
    TypeImpl *parent;
......
    ti->class_size = type_class_get_size(ti);
    ti->instance_size = type_object_get_size(ti);
    if (ti->instance_size == 0) {
        ti->abstract = true;
    }
......
    ti->class = g_malloc0(ti->class_size);
......
    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);
    }
}
复制代码

在 objectclass_foreach_tramp 中,会调用将 type_initialize,这里面会调用 class_init 将纸面上的 class 也即 TypeImpl 变为 ObjectClass,ObjectClass 是所有 Class 类的祖先,MachineClass 是它的子类。
因为在 machine 的命令行里面,我们指定了名字为”pc-i440fx-4.0”,就肯定能够找到我们注册过了的 TypeImpl,并调用它的 class_init 函数。
因而 pc_machine
##suffix##class_init 会被调用,在这里面,pc_i440fx_machine_options 才真正被调用初始化 MachineClass,并且将 MachineClass 的 init 函数设置为 pc_init##suffix。也即,当 select_machine 执行完毕后,就有一个 MachineClass 了。
接着,我们回到 object_new。这就很好理解了,MachineClass 是一个 Class 类,接下来应该通过它生成一个 Instance,也即对象,这就是 object_new 的作用。

Object *object_new(const char *typename)
{
    TypeImpl *ti = type_get_by_name(typename);
    return object_new_with_type(ti);
}
static Object *object_new_with_type(Type type)
{
    Object *obj;
    type_initialize(type);
    obj = g_malloc(type->instance_size);
    object_initialize_with_type(obj, type->instance_size, type);
    obj->free = g_free;
    return obj;
}
复制代码

object_new 中,TypeImpl 的 instance_init 会被调用,创建一个对象。current_machine 就是这个对象,它的类型是 MachineState。
至此,绕了这么大一圈,有关体系结构的对象才创建完毕,接下来很多的设备的初始化,包括 CPU 和内存的初始化,都是围绕着体系结构的对象来的,后面我们会常常看到 current_machine。

总结时刻

这一节,我们学到,虚拟机对于设备的模拟是一件非常复杂的事情,需要用复杂的参数模拟各种各样的设备。为了能够适配这些设备,qemu 定义了自己的模块管理机制,只有了解了这种机制,后面看每一种设备的虚拟化的时候,才有一个整体的思路。
这里的 MachineClass 是我们遇到的第一个,我们需要掌握它里面各种定义之间的关系。
image.png
每个模块都会有一个定义 TypeInfo,会通过 type_init 变为全局的 TypeImpl。TypeInfo 以及生成的 TypeImpl 有以下成员:

  • name 表示当前类型的名称
  • parent 表示父类的名称
  • class_init 用于将 TypeImpl 初始化为 MachineClass
  • instance_init 用于将 MachineClass 初始化为 MachineState

所以,以后遇到任何一个类型的时候,将父类和子类之间的关系,以及对应的初始化函数都要看好,这样就一目了然了。


4. 初始化块设备

我们接着回到 main 函数,接下来初始化的是块设备,调用的是 configure_blockdev。这里我们需要重点关注上面参数中的硬盘,不过我们放在存储虚拟化那一节再解析。

configure_blockdev(&bdo_queue, machine_class, snapshot);
复制代码

5. 初始化计算虚拟化的加速模式

接下来初始化的是计算虚拟化的加速模式,也即要不要使用 KVM。根据参数中的配置是启用 KVM。这里调用的是 configure_accelerator。

configure_accelerator(current_machine, argv[0]);
void configure_accelerator(MachineState *ms, const char *progname)
{
    const char *accel;
    char **accel_list, **tmp;
    int ret;
    bool accel_initialised = false;
    bool init_failed = false;
    AccelClass *acc = NULL;
    accel = qemu_opt_get(qemu_get_machine_opts(), "accel");
    accel = "kvm";
    accel_list = g_strsplit(accel, ":", 0);
    for (tmp = accel_list; !accel_initialised && tmp && *tmp; tmp++) {
        acc = accel_find(*tmp);
        ret = accel_init_machine(acc, ms);
    }
}
static AccelClass *accel_find(const char *opt_name)
{
    char *class_name = g_strdup_printf(ACCEL_CLASS_NAME("%s"), opt_name);
    AccelClass *ac = ACCEL_CLASS(object_class_by_name(class_name));
    g_free(class_name);
    return ac;
}
static int accel_init_machine(AccelClass *acc, MachineState *ms)
{
    ObjectClass *oc = OBJECT_CLASS(acc);
    const char *cname = object_class_get_name(oc);
    AccelState *accel = ACCEL(object_new(cname));
    int ret;
    ms->accelerator = accel;
    *(acc->allowed) = true;
    ret = acc->init_machine(ms);
    return ret;
}
复制代码

在 configure_accelerator 中,我们看命令行参数里面的 accel,发现是 kvm,则调用 accel_find 根据名字,得到相应的纸面上的 class,并初始化为 Class 类。
MachineClass 是计算机体系结构的 Class 类,同理,AccelClass 就是加速器的 Class 类,然后调用 accel_init_machine,通过 object_new,将 AccelClass 这个 Class 类实例化为 AccelState,类似对于体系结构的实例是 MachineState。
在 accel_find 中,我们会根据名字 kvm,找到纸面上的 class,也即 kvm_accel_type,然后调用 type_initialize,里面调用 kvm_accel_type 的 class_init 方法,也即 kvm_accel_class_init。

static void kvm_accel_class_init(ObjectClass *oc, void *data)
{
    AccelClass *ac = ACCEL_CLASS(oc);
    ac->name = "KVM";
    ac->init_machine = kvm_init;
    ac->allowed = &kvm_allowed;
}
复制代码

在 kvm_accel_class_init 中,我们创建 AccelClass,将 init_machine 设置为 kvm_init。在 accel_init_machine 中其实就调用了这个 init_machine 函数,也即调用 kvm_init 方法。

static int kvm_init(MachineState *ms)
{
    MachineClass *mc = MACHINE_GET_CLASS(ms);
    int soft_vcpus_limit, hard_vcpus_limit;
    KVMState *s;
    const KVMCapabilityInfo *missing_cap;
    int ret;
    int type = 0;
    const char *kvm_type;
    s = KVM_STATE(ms->accelerator);
    s->fd = qemu_open("/dev/kvm", O_RDWR);
    ret = kvm_ioctl(s, KVM_GET_API_VERSION, 0);
......
    do {
        ret = kvm_ioctl(s, KVM_CREATE_VM, type);
    } while (ret == -EINTR);
......
    s->vmfd = ret;
    /* check the vcpu limits */
    soft_vcpus_limit = kvm_recommended_vcpus(s);
    hard_vcpus_limit = kvm_max_vcpus(s);
......
    ret = kvm_arch_init(ms, s);
    if (ret < 0) {
        goto err;
    }
    if (machine_kernel_irqchip_allowed(ms)) {
        kvm_irqchip_create(ms, s);
    }
......
    return 0;
}
复制代码

这里面的操作就从用户态到内核态的 KVM 了。就像前面原理讲过的一样,用户态使用内核态 KVM 的能力,需要打开一个文件 /dev/kvm,这是一个字符设备文件,打开一个字符设备文件的过程我们讲过,这里不再赘述。

static struct miscdevice kvm_dev = {
    KVM_MINOR,
    "kvm",
    &kvm_chardev_ops,
};
static struct file_operations kvm_chardev_ops = {
    .unlocked_ioctl = kvm_dev_ioctl,
    .compat_ioctl   = kvm_dev_ioctl,
    .llseek     = noop_llseek,
};
复制代码

KVM 这个字符设备文件定义了一个字符设备文件的操作函数 kvm_chardev_ops,这里面只定义了 ioctl 的操作。
接下来,用户态就通过 ioctl 系统调用,调用到 kvm_dev_ioctl 这个函数。这个过程我们在字符设备那一节也讲了。

static long kvm_dev_ioctl(struct file *filp,
              unsigned int ioctl, unsigned long arg)
{
    long r = -EINVAL;
    switch (ioctl) {
    case KVM_GET_API_VERSION:
        r = KVM_API_VERSION;
        break;
    case KVM_CREATE_VM:
        r = kvm_dev_ioctl_create_vm(arg);
        break;  
    case KVM_CHECK_EXTENSION:
        r = kvm_vm_ioctl_check_extension_generic(NULL, arg);
        break;  
    case KVM_GET_VCPU_MMAP_SIZE:
        r = PAGE_SIZE;     /* struct kvm_run */
        break;  
......
    }   
out:
    return r;
}
复制代码

我们可以看到,在用户态 qemu 中,调用 KVM_GET_API_VERSION 查看版本号,内核就有相应的分支,返回版本号,如果能够匹配上,则调用 KVM_CREATE_VM 创建虚拟机。
创建虚拟机,需要调用 kvm_dev_ioctl_create_vm。

static int kvm_dev_ioctl_create_vm(unsigned long type)
{
    int r;
    struct kvm *kvm;
    struct file *file;
    kvm = kvm_create_vm(type);
......
    r = get_unused_fd_flags(O_CLOEXEC);
......
    file = anon_inode_getfile("kvm-vm", &kvm_vm_fops, kvm, O_RDWR);
......
    fd_install(r, file);
    return r;
}
复制代码

在 kvm_dev_ioctl_create_vm 中,首先调用 kvm_create_vm 创建一个 struct kvm 结构。这个结构在内核里面代表一个虚拟机。
从下面结构的定义里,我们可以看到,这里面有 vcpu,有 mm_struct 结构。这个结构本来用来管理进程的内存的。虚拟机也是一个进程,所以虚拟机的用户进程空间也是用它来表示。虚拟机里面的操作系统以及应用的进程空间不归它管。
在 kvm_dev_ioctl_create_vm 中,第二件事情就是创建一个文件描述符,和 struct file 关联起来,这个 struct file 的 file_operations 会被设置为 kvm_vm_fops。

struct kvm {
    struct mm_struct *mm; /* userspace tied to this vm */
    struct kvm_memslots __rcu *memslots[KVM_ADDRESS_SPACE_NUM];
    struct kvm_vcpu *vcpus[KVM_MAX_VCPUS];
    atomic_t online_vcpus;
    int created_vcpus;
    int last_boosted_vcpu;
    struct list_head vm_list;
    struct mutex lock;
    struct kvm_io_bus __rcu *buses[KVM_NR_BUSES];
......
    struct kvm_vm_stat stat;
    struct kvm_arch arch;
    refcount_t users_count;
......
    long tlbs_dirty;
    struct list_head devices;
    pid_t userspace_pid;
};
static struct file_operations kvm_vm_fops = {
    .release        = kvm_vm_release,
    .unlocked_ioctl = kvm_vm_ioctl,
    .llseek        = noop_llseek,
};
复制代码

kvm_dev_ioctl_create_vm 结束之后,对于一台虚拟机而言,只是在内核中有一个数据结构,对于相应的资源还没有分配,所以我们还需要接着看。

6. 初始化网络设备

接下来,调用 net_init_clients 进行网络设备的初始化。我们可以解析 net 参数,也会在 net_init_clients 中解析 netdev 参数。这属于网络虚拟化的部分,我们先暂时放一下。

int net_init_clients(Error **errp)
{
    QTAILQ_INIT(&net_clients);
    if (qemu_opts_foreach(qemu_find_opts("netdev"),
                          net_init_netdev, NULL, errp)) {
        return -1;
    }
    if (qemu_opts_foreach(qemu_find_opts("nic"), net_param_nic, NULL, errp)) {
        return -1;
   }
    if (qemu_opts_foreach(qemu_find_opts("net"), net_init_client, NULL, errp)) {
        return -1;
    }
    return 0;
}  
复制代码

7.CPU 虚拟化

接下来,我们要调用 machine_run_board_init。这里面调用了 MachineClass 的 init 函数。盼啊盼才到了它,这才调用了 pc_init1。

void machine_run_board_init(MachineState *machine)
{
    MachineClass *machine_class = MACHINE_GET_CLASS(machine);
    numa_complete_configuration(machine);
    if (nb_numa_nodes) {
        machine_numa_finish_cpu_init(machine);
    }
......
    machine_class->init(machine);
}
复制代码

在 pc_init1 里面,我们重点关注两件重要的事情,一个的 CPU 的虚拟化,主要调用 pc_cpus_init;另外就是内存的虚拟化,主要调用 pc_memory_init。这一节我们重点关注 CPU 的虚拟化,下一节,我们来看内存的虚拟化。

void pc_cpus_init(PCMachineState *pcms)
{
......
    for (i = 0; i < smp_cpus; i++) {
        pc_new_cpu(possible_cpus->cpus[i].type, possible_cpus->cpus[i].arch_id, &error_fatal);
    }
}
static void pc_new_cpu(const char *typename, int64_t apic_id, Error **errp)
{
    Object *cpu = NULL;
    cpu = object_new(typename);
    object_property_set_uint(cpu, apic_id, "apic-id", &local_err);
    object_property_set_bool(cpu, true, "realized", &local_err);// 调用 object_property_add_bool 的时候,设置了用 device_set_realized 来设置
......
}
复制代码

在 pc_cpus_init 中,对于每一个 CPU,都调用 pc_new_cpu,在这里,我们又看到了 object_new,这又是一个从 TypeImpl 到 Class 类再到对象的一个过程。
这个时候,我们就要看 CPU 的类是怎么组织的了。
在上面的参数里面,CPU 的配置是这样的:

-cpu SandyBridge,+erms,+smep,+fsgsbase,+pdpe1gb,+rdrand,+f16c,+osxsave,+dca,+pcid,+pdcm,+xtpr,+tm2,+est,+smx,+vmx,+ds_cpl,+monitor,+dtes64,+pbe,+tm,+ht,+ss,+acpi,+ds,+vme
复制代码

在这里我们知道,SandyBridge 是 CPU 的一种类型。在 hw/i386/pc.c 中,我们能看到这种 CPU 的定义。

{ "SandyBridge" "-" TYPE_X86_CPU, "min-xlevel", "0x8000000a" }
复制代码

接下来,我们就来看”SandyBridge”,也即 TYPE_X86_CPU 这种 CPU 的类,是一个什么样的结构。

static const TypeInfo device_type_info = {
    .name = TYPE_DEVICE,
    .parent = TYPE_OBJECT,
    .instance_size = sizeof(DeviceState),
    .instance_init = device_initfn,
    .instance_post_init = device_post_init,
    .instance_finalize = device_finalize,
    .class_base_init = device_class_base_init,
    .class_init = device_class_init,
    .abstract = true,
    .class_size = sizeof(DeviceClass),
};
static const TypeInfo cpu_type_info = {
    .name = TYPE_CPU,
    .parent = TYPE_DEVICE,
    .instance_size = sizeof(CPUState),
    .instance_init = cpu_common_initfn,
    .instance_finalize = cpu_common_finalize,
    .abstract = true,
    .class_size = sizeof(CPUClass),
    .class_init = cpu_class_init,
};
static const TypeInfo x86_cpu_type_info = {
    .name = TYPE_X86_CPU,
    .parent = TYPE_CPU,
    .instance_size = sizeof(X86CPU),
    .instance_init = x86_cpu_initfn,
    .abstract = true,
    .class_size = sizeof(X86CPUClass),
    .class_init = x86_cpu_common_class_init,
};
复制代码

CPU 这种类的定义是有多层继承关系的。TYPE_X86_CPU 的父类是 TYPE_CPU,TYPE_CPU 的父类是 TYPE_DEVICE,TYPE_DEVICE 的父类是 TYPE_OBJECT。到头了。
这里面每一层都有 class_init,用于从 TypeImpl 生产 xxxClass,也有 instance_init 将 xxxClass 初始化为实例。
在 TYPE_X86_CPU 这一层的 class_init 中,也即 x86_cpu_common_class_init 中,设置了 DeviceClass 的 realize 函数为 x86_cpu_realizefn。这个函数很重要,马上就能用到。

static void x86_cpu_common_class_init(ObjectClass *oc, void *data)
{
    X86CPUClass *xcc = X86_CPU_CLASS(oc);
    CPUClass *cc = CPU_CLASS(oc);
    DeviceClass *dc = DEVICE_CLASS(oc);
    device_class_set_parent_realize(dc, x86_cpu_realizefn,
                                    &xcc->parent_realize);
......
}
复制代码

在 TYPE_DEVICE 这一层的 instance_init 函数 device_initfn,会为这个设备添加一个属性”realized”,要设置这个属性,需要用函数 device_set_realized。

static void device_initfn(Object *obj)
{
    DeviceState *dev = DEVICE(obj);
    ObjectClass *class;
    Property *prop;
    dev->realized = false;
    object_property_add_bool(obj, "realized",
                             device_get_realized, device_set_realized, NULL);
......
}
复制代码

我们回到 pc_new_cpu 函数,这里面就是通过 object_property_set_bool 设置这个属性为 true,所以 device_set_realized 函数会被调用。
在 device_set_realized 中,DeviceClass 的 realize 函数 x86_cpu_realizefn 会被调用。这里面 qemu_init_vcpu 会调用 qemu_kvm_start_vcpu。

static void qemu_kvm_start_vcpu(CPUState *cpu)
{
    char thread_name[VCPU_THREAD_NAME_SIZE];
    cpu->thread = g_malloc0(sizeof(QemuThread));
    cpu->halt_cond = g_malloc0(sizeof(QemuCond));
    qemu_cond_init(cpu->halt_cond);
    qemu_thread_create(cpu->thread, thread_name, qemu_kvm_cpu_thread_fn, cpu, QEMU_THREAD_JOINABLE);
}
复制代码

在这里面,为这个 vcpu 创建一个线程,也即虚拟机里面的一个 vcpu 对应物理机上的一个线程,然后这个线程被调度到某个物理 CPU 上。
我们来看这个 vcpu 的线程执行函数。

static void *qemu_kvm_cpu_thread_fn(void *arg)
{
    CPUState *cpu = arg;
    int r;
    rcu_register_thread();
    qemu_mutex_lock_iothread();
    qemu_thread_get_self(cpu->thread);
    cpu->thread_id = qemu_get_thread_id();
    cpu->can_do_io = 1;
    current_cpu = cpu;
    r = kvm_init_vcpu(cpu);
    kvm_init_cpu_signals(cpu);
    /* signal CPU creation */
    cpu->created = true;
    qemu_cond_signal(&qemu_cpu_cond);
    do {
        if (cpu_can_run(cpu)) {
            r = kvm_cpu_exec(cpu);
        }
        qemu_wait_io_event(cpu);
    } while (!cpu->unplug || cpu_can_run(cpu));
    qemu_kvm_destroy_vcpu(cpu);
    cpu->created = false;
    qemu_cond_signal(&qemu_cpu_cond);
    qemu_mutex_unlock_iothread();
    rcu_unregister_thread();
    return NULL;
}
复制代码

在 qemu_kvm_cpu_thread_fn 中,先是 kvm_init_vcpu 初始化这个 vcpu。

int kvm_init_vcpu(CPUState *cpu)
{
    KVMState *s = kvm_state;
    long mmap_size;
    int ret;
......
    ret = kvm_get_vcpu(s, kvm_arch_vcpu_id(cpu));
......
    cpu->kvm_fd = ret;
    cpu->kvm_state = s;
    cpu->vcpu_dirty = true;
    mmap_size = kvm_ioctl(s, KVM_GET_VCPU_MMAP_SIZE, 0);
......
    cpu->kvm_run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, cpu->kvm_fd, 0);
......
    ret = kvm_arch_init_vcpu(cpu);
err:
    return ret;
}
复制代码

在 kvm_get_vcpu 中,我们会调用 kvm_vm_ioctl(s, KVM_CREATE_VCPU, (void *)vcpu_id),在内核里面创建一个 vcpu。在上面创建 KVM_CREATE_VM 的时候,我们已经创建了一个 struct file,它的 file_operations 被设置为 kvm_vm_fops,这个内核文件也是可以响应 ioctl 的。
如果我们切换到内核 KVM,在 kvm_vm_ioctl 函数中,有对于 KVM_CREATE_VCPU 的处理,调用的是 kvm_vm_ioctl_create_vcpu。

static long kvm_vm_ioctl(struct file *filp,
               unsigned int ioctl, unsigned long arg)
{
    struct kvm *kvm = filp->private_data;
    void __user *argp = (void __user *)arg;
    int r;
    switch (ioctl) {
    case KVM_CREATE_VCPU:
        r = kvm_vm_ioctl_create_vcpu(kvm, arg);
        break;
    case KVM_SET_USER_MEMORY_REGION: {
        struct kvm_userspace_memory_region kvm_userspace_mem;
        if (copy_from_user(&kvm_userspace_mem, argp,
                        sizeof(kvm_userspace_mem)))
            goto out;
        r = kvm_vm_ioctl_set_memory_region(kvm, &kvm_userspace_mem);
        break;
    }
......
    case KVM_CREATE_DEVICE: {
        struct kvm_create_device cd;
        if (copy_from_user(&cd, argp, sizeof(cd)))
            goto out;
        r = kvm_ioctl_create_device(kvm, &cd);
        if (copy_to_user(argp, &cd, sizeof(cd)))
            goto out;
        break;
    }
    case KVM_CHECK_EXTENSION:
        r = kvm_vm_ioctl_check_extension_generic(kvm, arg);
        break;
    default:
        r = kvm_arch_vm_ioctl(filp, ioctl, arg);
    }
out:
    return r;
}
复制代码

在 kvm_vm_ioctl_create_vcpu 中,kvm_arch_vcpu_create 调用 kvm_x86_ops 的 vcpu_create 函数来创建 CPU。

static int kvm_vm_ioctl_create_vcpu(struct kvm *kvm, u32 id)
{
    int r;
    struct kvm_vcpu *vcpu;
    kvm->created_vcpus++;
......
    vcpu = kvm_arch_vcpu_create(kvm, id);
    preempt_notifier_init(&vcpu->preempt_notifier, &kvm_preempt_ops);
    r = kvm_arch_vcpu_setup(vcpu);
......
    /* Now it's all set up, let userspace reach it */
    kvm_get_kvm(kvm);
    r = create_vcpu_fd(vcpu);
    kvm->vcpus[atomic_read(&kvm->online_vcpus)] = vcpu;
......
}
struct kvm_vcpu *kvm_arch_vcpu_create(struct kvm *kvm,
                        unsigned int id)        
{
    struct kvm_vcpu *vcpu;
    vcpu = kvm_x86_ops->vcpu_create(kvm, id);
    return vcpu;
}
static int create_vcpu_fd(struct kvm_vcpu *vcpu)
{
    return anon_inode_getfd("kvm-vcpu", &kvm_vcpu_fops, vcpu, O_RDWR | O_CLOEXEC);
}
复制代码

然后,create_vcpu_fd 又创建了一个 struct file,它的 file_operations 指向 kvm_vcpu_fops。从这里可以看出,KVM 的内核模块是一个文件,可以通过 ioctl 进行操作。基于这个内核模块创建的 VM 也是一个文件,也可以通过 ioctl 进行操作。在这个 VM 上创建的 vcpu 同样是一个文件,同样可以通过 ioctl 进行操作。
我们回过头来看,kvm_x86_ops 的 vcpu_create 函数。kvm_x86_ops 对于不同的硬件加速虚拟化指向不同的结构,如果是 vmx,则指向 vmx_x86_ops;如果是 svm,则指向 svm_x86_ops。我们这里看 vmx_x86_ops。这个结构很长,里面有非常多的操作,我们用一个看一个。

static struct kvm_x86_ops vmx_x86_ops __ro_after_init = {
......
    .vcpu_create = vmx_create_vcpu,
......
}
static struct kvm_vcpu *vmx_create_vcpu(struct kvm *kvm, unsigned int id)
{
    int err;
    struct vcpu_vmx *vmx = kmem_cache_zalloc(kvm_vcpu_cache, GFP_KERNEL);
    int cpu;
    vmx->vpid = allocate_vpid();
    err = kvm_vcpu_init(&vmx->vcpu, kvm, id);
    vmx->guest_msrs = kmalloc(PAGE_SIZE, GFP_KERNEL);
    vmx->loaded_vmcs = &vmx->vmcs01;
    vmx->loaded_vmcs->vmcs = alloc_vmcs();
    vmx->loaded_vmcs->shadow_vmcs = NULL;
    loaded_vmcs_init(vmx->loaded_vmcs);
    cpu = get_cpu();
    vmx_vcpu_load(&vmx->vcpu, cpu);
    vmx->vcpu.cpu = cpu;
    err = vmx_vcpu_setup(vmx);
    vmx_vcpu_put(&vmx->vcpu);
    put_cpu();
    if (enable_ept) {
        if (!kvm->arch.ept_identity_map_addr)
            kvm->arch.ept_identity_map_addr =
                VMX_EPT_IDENTITY_PAGETABLE_ADDR;
        err = init_rmode_identity_map(kvm);
    }
    return &vmx->vcpu;
}
复制代码

vmx_create_vcpu 创建用于表示 vcpu 的结构 struct vcpu_vmx,并填写里面的内容。例如 guest_msrs,咱们在讲系统调用的时候提过 msr 寄存器,虚拟机也需要有这样的寄存器。
enable_ept 是和内存虚拟化相关的,EPT 全称 Extended Page Table,顾名思义,是优化内存虚拟化的,这个功能我们放到内存的那一节讲。
最最重要的就是 loaded_vmcs 了。VMCS 是什么呢?它的全称是 Virtual Machine Control Structure。它是来干什么呢?
前面咱们将进程调度的时候讲过,为了支持进程在 CPU 上的切换,CPU 硬件要求有一个 TSS 结构,用于保存进程运行时的所有寄存器的状态,进程切换的时候,需要根据 TSS 恢复寄存器。
虚拟机也是一个进程,也需要切换,而且切换更加的复杂,可能是两个虚拟机之间切换,也可能是虚拟机切换给内核,虚拟机因为里面还有另一个操作系统,要保存的信息比普通的进程多得多。那就需要有一个结构来保存虚拟机运行的上下文,VMCS 就是是 Intel 实现 CPU 虚拟化,记录 vCPU 状态的一个关键数据结构。
VMCS 数据结构主要包含以下信息。

  • Guest-state area,即 vCPU 的状态信息,包括 vCPU 的基本运行环境,例如寄存器等。
  • Host-state area,是物理 CPU 的状态信息。物理 CPU 和 vCPU 之间也会来回切换,所以,VMCS 中既要记录 vCPU 的状态,也要记录物理 CPU 的状态。
  • VM-execution control fields,对 vCPU 的运行行为进行控制。例如,发生中断怎么办,是否使用 EPT(Extended Page Table)功能等。

接下来,对于 VMCS,有两个重要的操作。
VM-Entry,我们称为从根模式切换到非根模式,也即切换到 guest 上,这个时候 CPU 上运行的是虚拟机。VM-Exit 我们称为 CPU 从非根模式切换到根模式,也即从 guest 切换到宿主机。例如,当要执行一些虚拟机没有权限的敏感指令时。
image.png
为了维护这两个动作,VMCS 里面还有几项内容:

  • VM-exit control fields,对 VM Exit 的行为进行控制。比如,VM Exit 的时候对 vCPU 来说需要保存哪些 MSR 寄存器,对于主机 CPU 来说需要恢复哪些 MSR 寄存器。
  • VM-entry control fields,对 VM Entry 的行为进行控制。比如,需要保存和恢复哪些 MSR 寄存器等。
  • VM-exit information fields,记录下发生 VM Exit 发生的原因及一些必要的信息,方便对 VM Exit 事件进行处理。

至此,内核准备完毕。
我们再回到 qemu 的 kvm_init_vcpu 函数,这里面除了创建内核中的 vcpu 结构之外,还通过 mmap 将内核的 vcpu 结构,映射到 qemu 中 CPUState 的 kvm_run 中,为什么能用 mmap 呢,上面咱们不是说过了吗,vcpu 也是一个文件。
我们再回到这个 vcpu 的线程函数 qemu_kvm_cpu_thread_fn,他在执行 kvm_init_vcpu 创建 vcpu 之后,接下来是一个 do-while 循环,也即一直运行,并且通过调用 kvm_cpu_exec,运行这个虚拟机。

int kvm_cpu_exec(CPUState *cpu)
{
    struct kvm_run *run = cpu->kvm_run;
    int ret, run_ret;
......
    do {
......
        run_ret = kvm_vcpu_ioctl(cpu, KVM_RUN, 0);
......
        switch (run->exit_reason) {
        case KVM_EXIT_IO:
            kvm_handle_io(run->io.port, attrs,
                          (uint8_t *)run + run->io.data_offset,
                          run->io.direction,
                          run->io.size,
                          run->io.count);
            break;
        case KVM_EXIT_IRQ_WINDOW_OPEN:
            ret = EXCP_INTERRUPT;
            break;
        case KVM_EXIT_SHUTDOWN:
            qemu_system_reset_request(SHUTDOWN_CAUSE_GUEST_RESET);
            ret = EXCP_INTERRUPT;
            break;
        case KVM_EXIT_UNKNOWN:
            fprintf(stderr, "KVM: unknown exit, hardware reason %" PRIx64 "\n",(uint64_t)run->hw.hardware_exit_reason);
            ret = -1;
            break;
        case KVM_EXIT_INTERNAL_ERROR:
            ret = kvm_handle_internal_error(cpu, run);
            break;
......
        }
    } while (ret == 0);
......
    return ret;
}
复制代码

在 kvm_cpu_exec 中,我们能看到一个循环,在循环中,kvm_vcpu_ioctl(KVM_RUN) 运行这个虚拟机,这个时候 CPU 进入 VM-Entry,也即进入客户机模式。
如果一直是客户机的操作系统占用这个 CPU,则会一直停留在这一行运行,一旦这个调用返回了,就说明 CPU 进入 VM-Exit 退出客户机模式,将 CPU 交还给宿主机。在循环中,我们会对退出的原因 exit_reason 进行分析处理,因为有了 I/O,还有了中断等,做相应的处理。处理完毕之后,再次循环,再次通过 VM-Entry,进入客户机模式。如此循环,直到虚拟机正常或者异常退出。
我们来看 kvm_vcpu_ioctl(KVM_RUN) 在内核做了哪些事情。
上面我们也讲了,vcpu 在内核也是一个文件,也是通过 ioctl 进行用户态和内核态通信的,在内核中,调用的是 kvm_vcpu_ioctl。

static long kvm_vcpu_ioctl(struct file *filp,
               unsigned int ioctl, unsigned long arg)
{
    struct kvm_vcpu *vcpu = filp->private_data;
    void __user *argp = (void __user *)arg;
    int r;
    struct kvm_fpu *fpu = NULL;
    struct kvm_sregs *kvm_sregs = NULL;
......
    r = vcpu_load(vcpu);
    switch (ioctl) {
    case KVM_RUN: {
        struct pid *oldpid;
        r = kvm_arch_vcpu_ioctl_run(vcpu, vcpu->run);
        break;
    }
    case KVM_GET_REGS: {
        struct kvm_regs *kvm_regs;
        kvm_regs = kzalloc(sizeof(struct kvm_regs), GFP_KERNEL);
        r = kvm_arch_vcpu_ioctl_get_regs(vcpu, kvm_regs);
        if (copy_to_user(argp, kvm_regs, sizeof(struct kvm_regs)))
            goto out_free1;
        break;
    }
    case KVM_SET_REGS: {
        struct kvm_regs *kvm_regs;
        kvm_regs = memdup_user(argp, sizeof(*kvm_regs));
        r = kvm_arch_vcpu_ioctl_set_regs(vcpu, kvm_regs);
        break;
    }
......
}
复制代码

kvm_arch_vcpu_ioctl_run 会调用 vcpu_run,这里面也是一个无限循环。

static int vcpu_run(struct kvm_vcpu *vcpu)
{
    int r;
    struct kvm *kvm = vcpu->kvm;
    for (;;) {
        if (kvm_vcpu_running(vcpu)) {
            r = vcpu_enter_guest(vcpu);
        } else {
            r = vcpu_block(kvm, vcpu);
        }
....
        if (signal_pending(current)) {
            r = -EINTR;
            vcpu->run->exit_reason = KVM_EXIT_INTR;
            ++vcpu->stat.signal_exits;
            break;
        }
        if (need_resched()) {
            cond_resched();
        }
    }
......
    return r;
}
复制代码

在这个循环中,除了调用 vcpu_enter_guest 进入客户机模式运行之外,还有对于信号的响应 signal_pending,也即一台虚拟机是可以被 kill 掉的,还有对于调度的响应,这台虚拟机可以被从当前的物理 CPU 上赶下来,换成别的虚拟机或者其他进程。
我们这里重点看 vcpu_enter_guest。

static int vcpu_enter_guest(struct kvm_vcpu *vcpu)
{
    r = kvm_mmu_reload(vcpu);
    vcpu->mode = IN_GUEST_MODE;
    kvm_load_guest_xcr0(vcpu);
......
    guest_enter_irqoff();
    kvm_x86_ops->run(vcpu);
    vcpu->mode = OUTSIDE_GUEST_MODE;
......
    kvm_put_guest_xcr0(vcpu);
    kvm_x86_ops->handle_external_intr(vcpu);
    ++vcpu->stat.exits;
    guest_exit_irqoff();
    r = kvm_x86_ops->handle_exit(vcpu);
    return r;
......
}
static struct kvm_x86_ops vmx_x86_ops __ro_after_init = {
......
    .run = vmx_vcpu_run,
......
}
复制代码

在 vcpu_enter_guest 中,我们会调用 vmx_x86_ops 的 vmx_vcpu_run 函数,进入客户机模式。

static void __noclone vmx_vcpu_run(struct kvm_vcpu *vcpu)
{
    struct vcpu_vmx *vmx = to_vmx(vcpu);
    unsigned long debugctlmsr, cr3, cr4;
......
    cr3 = __get_current_cr3_fast();
......
    cr4 = cr4_read_shadow();
......
    vmx->__launched = vmx->loaded_vmcs->launched;
    asm(
        /* Store host registers */
        "push %%" _ASM_DX "; push %%" _ASM_BP ";"
        "push %%" _ASM_CX " \n\t" /* placeholder for guest rcx */
        "push %%" _ASM_CX " \n\t"
......
        /* Load guest registers.  Don't clobber flags. */
        "mov %crax, %%" _ASM_AX " \n\t"
        "mov %crbx, %%" _ASM_BX " \n\t"
        "mov %crdx, %%" _ASM_DX " \n\t"
        "mov %crsi, %%" _ASM_SI " \n\t"
        "mov %crdi, %%" _ASM_DI " \n\t"
        "mov %crbp, %%" _ASM_BP " \n\t"
#ifdef CONFIG_X86_64
        "mov %cr8,  %%r8  \n\t"
        "mov %cr9,  %%r9  \n\t"
        "mov %cr10, %%r10 \n\t"
        "mov %cr11, %%r11 \n\t"
        "mov %cr12, %%r12 \n\t"
        "mov %cr13, %%r13 \n\t"
        "mov %cr14, %%r14 \n\t"
        "mov %cr15, %%r15 \n\t"
#endif
        "mov %crcx, %%" _ASM_CX " \n\t" /* kills %0 (ecx) */
        /* Enter guest mode */
        "jne 1f \n\t"
        __ex(ASM_VMX_VMLAUNCH) "\n\t"
        "jmp 2f \n\t"
        "1: " __ex(ASM_VMX_VMRESUME) "\n\t"
        "2: "
        /* Save guest registers, load host registers, keep flags */
        "mov %0, %cwordsize \n\t"
        "pop %0 \n\t"
        "mov %%" _ASM_AX ", %crax \n\t"
        "mov %%" _ASM_BX ", %crbx \n\t"
        __ASM_SIZE(pop) " %crcx \n\t"
        "mov %%" _ASM_DX ", %crdx \n\t"
        "mov %%" _ASM_SI ", %crsi \n\t"
        "mov %%" _ASM_DI ", %crdi \n\t"
        "mov %%" _ASM_BP ", %crbp \n\t"
#ifdef CONFIG_X86_64
        "mov %%r8,  %cr8 \n\t"
        "mov %%r9,  %cr9 \n\t"
        "mov %%r10, %cr10 \n\t"
        "mov %%r11, %cr11 \n\t"
        "mov %%r12, %cr12 \n\t"
        "mov %%r13, %cr13 \n\t"
        "mov %%r14, %cr14 \n\t"
        "mov %%r15, %cr15 \n\t"
#endif
        "mov %%cr2, %%" _ASM_AX "   \n\t"
        "mov %%" _ASM_AX ", %ccr2 \n\t"
        "pop  %%" _ASM_BP "; pop  %%" _ASM_DX " \n\t"
        "setbe %cfail \n\t"
        ".pushsection .rodata \n\t"
        ".global vmx_return \n\t"
        "vmx_return: " _ASM_PTR " 2b \n\t"
......
          );
......
    vmx->loaded_vmcs->launched = 1;
    vmx->exit_reason = vmcs_read32(VM_EXIT_REASON);
......
}
复制代码

在 vmx_vcpu_run 中,出现了汇编语言的代码,比较难看懂,但是没有关系呀,里面有注释呀,我们可以沿着注释来看。

  • 首先是 Store host registers,要从宿主机模式变为客户机模式了,所以原来宿主机运行时候的寄存器要保存下来。
  • 接下来是 Load guest registers,将原来客户机运行的时候的寄存器加载进来。
  • 接下来是 Enter guest mode,调用 ASM_VMX_VMLAUNCH 进入客户机模型运行,或者 ASM_VMX_VMRESUME 恢复客户机模型运行。
  • 如果客户机因为某种原因退出,Save guest registers, load host registers,也即保存客户机运行的时候的寄存器,就加载宿主机运行的时候的寄存器。
  • 最后将 exit_reason 保存在 vmx 结构中。

至此,CPU 虚拟化就解析完了。

总结时刻

CPU 的虚拟化过程还是很复杂的,我画了一张图总结了一下。

  • 首先,我们要定义 CPU 这种类型的 TypeInfo 和 TypeImpl、继承关系,并且声明它的类初始化函数。
  • 在 qemu 的 main 函数中调用 MachineClass 的 init 函数,这个函数既会初始化 CPU,也会初始化内存。
  • CPU 初始化的时候,会调用 pc_new_cpu 创建一个虚拟 CPU,它会调用 CPU 这个类的初始化函数。
  • 每一个虚拟 CPU 会调用 qemu_thread_create 创建一个线程,线程的执行函数为 qemu_kvm_cpu_thread_fn。
  • 在虚拟 CPU 对应的线程执行函数中,我们先是调用 kvm_vm_ioctl(KVM_CREATE_VCPU),在内核的 KVM 里面,创建一个结构 struct vcpu_vmx,表示这个虚拟 CPU。在这个结构里面,有一个 VMCS,用于保存当前虚拟机 CPU 的运行时的状态,用于状态切换。
  • 在虚拟 CPU 对应的线程执行函数中,我们接着调用 kvm_vcpu_ioctl(KVM_RUN),在内核的 KVM 里面运行这个虚拟机 CPU。运行的方式是保存宿主机的寄存器,加载客户机的寄存器,然后调用 ex(ASM_VMX_VMLAUNCH) 或者 ex(ASM_VMX_VMRESUME),进入客户机模式运行。一旦退出客户机模式,就会保存客户机寄存器,加载宿主机寄存器,进入宿主机模式运行,并且会记录退出虚拟机模式的原因。大部分的原因是等待 I/O,因而宿主机调用 kvm_handle_io 进行处理。