写点什么

QEMU 之 CPU 虚拟化(三):虚拟机的创建

  • 2023-09-03
    浙江
  • 本文字数:6019 字

    阅读完需:约 20 分钟

QEMU之CPU虚拟化(三):虚拟机的创建

QEMU 之 CPU 虚拟化

最近在阅读李强编著的《QEMU/KVM 源码解析与应用》这本书来学习 Linux 内核虚拟化相关知识,通过读书笔记的方式来提炼和归纳书中重要的知识点。本系列主要内容是关于 QEMU 中 CPU 虚拟化方面的介绍。

3 虚拟机的创建

关注微信公众号:Linux 内核拾遗

文章来源:https://mp.weixin.qq.com/s/-w1d_3U-Zdm2RYvFvcQ0zA


要创建一个 KVM 虚拟机,需要用户侧的 QEMU 发起请求,然后 KVM 配合完成虚拟机的创建,本文结合 QEMU 和 KVM 两个方面来介绍 KVM 虚拟机创建过程。

3.1 QEMU 侧虚拟机的创建

3.1.1 QEMU 加速器介绍

QEMU 作为一个开源虚拟化和模拟平台,支持多种加速器和后端选项,以提高虚拟机性能和功能。这些加速器和后端选项可以根据不同的用例和需求进行配置。

QEMU 的具体加速器和后端选项可能会因 QEMU 的版本和配置而异。可以使用以下命令查看当前版本的 QEMU 支持的加速器和后端选项,:

qemu-system-x86_64 -accel help
复制代码

以下是一些常见的 QEMU 加速器和后端选项:

  1. KVM(Kernel-based Virtual Machine)加速器:KVM 是一种在 Linux 内核中实现的虚拟化解决方案,它可以与 QEMU 结合使用,提供高性能的硬件虚拟化。KVM 通常是 QEMU 的首选加速器。

  2. HAXM(Hardware Accelerated Execution Manager):HAXM 是 Intel 提供的加速器,专门用于在基于 Intel 处理器的系统上运行虚拟机。

  3. HVF(Hypervisor.framework Virtualization Framework):HVF 是 Apple macOS 上的一种虚拟化加速器,用于在 Mac 上运行虚拟机。

  4. TCG(Tiny Code Generator)后端:如果硬件虚拟化不可用,QEMU 可以使用 TCG 后端进行模拟。TCG 是一种基于解释的虚拟机,性能通常较低,但可以在不支持硬件虚拟化的系统上工作。

3.1.2 虚拟机创建流程

当要使用 KVM 作为加速器和后端选项时,可以在 QEMU 的启动命令行中加入--enable-kvm,接下来参数解析会进入下面的 case 分支:

void qemu_init(int argc, char **argv){  ...    case QEMU_OPTION_enable_kvm:      qdict_put_str(machine_opts_dict, "accel", "kvm");      break;  ...  configure_accelerators(argv[0]);  phase_advance(PHASE_ACCEL_CREATED);  ...}
复制代码

这里会给machine_opts_dict参数列表中加入accel=kvm参数项,之后main函数就会调用configure_accelerators函数,用于从machine的参数列表中取出accel值,并找到所属的类型,然后调用accel_init_machine

static int do_configure_accelerator(void *opaque, QemuOpts *opts, Error **errp){    const char *acc = qemu_opt_get(opts, "accel");    AccelClass *ac = accel_find(acc);    AccelState *accel;
... accel = ACCEL(object_new_with_class(OBJECT_CLASS(ac))); ... ret = accel_init_machine(accel, current_machine); ...}
static void configure_accelerators(const char *progname){ ... if (!qemu_opts_foreach(qemu_find_opts("accel"), do_configure_accelerator, &init_failed, &error_fatal)) { ... } ...}
复制代码

如下所示,在accel_init_machine从,QEMU 会根据accel的类型获取对应AccelClass类型的对象实例,然后调用相应的对象方法acc->init_machine来完成加速器的初始化。对于 KVM 来说,这里的AccelClass本质上就是一个KVMState

int accel_init_machine(AccelState *accel, MachineState *ms){    AccelClass *acc = ACCEL_GET_CLASS(accel);    int ret;    ms->accelerator = accel;    *(acc->allowed) = true;    ret = acc->init_machine(ms);    ...    return ret;}
复制代码

在 QEMU 面向对象模型 QOM 中,AccelClass作为抽象类,其中的类方法由具体的实现类在初始化的时候进行赋值。对于 KVM 而言,其AccelClass的具体实现类为kvm_accel_type,其类初始化函数是kvm_accel_class_init,在该函数中将init_machine方法赋值为kvm_init

static void kvm_accel_class_init(ObjectClass *oc, void *data){    AccelClass *ac = ACCEL_CLASS(oc);    ac->name = "KVM";    ac->init_machine = kvm_init;    ...}
static const TypeInfo kvm_accel_type = { .name = TYPE_KVM_ACCEL, .parent = TYPE_ACCEL, .instance_init = kvm_accel_instance_init, .class_init = kvm_accel_class_init, .instance_size = sizeof(KVMState),};
复制代码

kvm_init主要代码如下:

static int kvm_init(MachineState *ms){    MachineClass *mc = MACHINE_GET_CLASS(ms);        ...    KVMState *s;
... s = KVM_STATE(ms->accelerator); ... s->fd = qemu_open_old("/dev/kvm", O_RDWR); ... ret = kvm_ioctl(s, KVM_GET_API_VERSION, 0); ... kvm_immediate_exit = kvm_check_extension(s, KVM_CAP_IMMEDIATE_EXIT); s->nr_slots = kvm_check_extension(s, KVM_CAP_NR_MEMSLOTS); ... do { ret = kvm_ioctl(s, KVM_CREATE_VM, type); } while (ret == -EINTR); ... s->vmfd = ret; ... missing_cap = kvm_check_extension_list(s, kvm_required_capabilites); ... s->coalesced_mmio = kvm_check_extension(s, KVM_CAP_COALESCED_MMIO); s->coalesced_pio = s->coalesced_mmio && kvm_check_extension(s, KVM_CAP_COALESCED_PIO); ...#ifdef KVM_CAP_VCPU_EVENTS s->vcpu_events = kvm_check_extension(s, KVM_CAP_VCPU_EVENTS);#endif ... s->irq_set_ioctl = KVM_IRQ_LINE; ...
kvm_state = s;
ret = kvm_arch_init(ms, s); ... if (s->kernel_irqchip_allowed) { kvm_irqchip_create(s); } ...}
复制代码

kvm_init中,QEMU 使用 KVMState 结构体来表示 KVM 相关的数据结构,并且完成如下的一些初始化过程:

  1. kvm_init函数首先打开/dev/kvm设备得到一个 fd,并且会保存到类型为KVMState的变量 s 的成员 fd 中;

  2. 检查 KVM 的版本;

  3. 检测是否支持 KVM 的一些扩展特性;

  4. 调用ioctl(KVM_CREATE_VM)接口在 KVM 层面创建一个虚拟机;

  5. 将 s 赋值到一个全局变量kvm_state,这样其他地方可以引用它。

最后kvm_init也会调用kvm_arch_init完成一些架构相关的初始化。

3.2 KVM 侧虚拟机的创建

函数kvm_init最重要的一步是调用/dev/kvm设备文件的ioctl(KVM_CREATE_VM)接口,在 KVM 模块中创建一台虚拟机,本质上一台虚拟机在 QEMU 层面来看就是一个 QEMU 进程,而在 KVM 模块中使用结构体struct kvm来表示虚拟机。

KVM 中对于/dev/kvm设备的ioctl接口的处理函数是kvm_dev_ioctl,而对应于KVM_CREATE_VM请求,KVM 通过kvm_dev_ioctl_create_vm函数来进行处理:

static int kvm_dev_ioctl_create_vm(unsigned long type){    char fdname[ITOA_MAX_LEN + 1];    int r, fd;    struct kvm *kvm;      fd = get_unused_fd_flags(O_CLOEXEC);    if (fd < 0)        return fd;
snprintf(fdname, sizeof(fdname), "%d", fd);
kvm = kvm_create_vm(type, fdname); ... file = anon_inode_getfile("kvm-vm", &kvm_vm_fops, kvm, O_RDWR); ... fd_install(fd, file); return fd;}
static long kvm_dev_ioctl(struct file *filp, unsigned int ioctl, unsigned long arg){ int r = -EINVAL;
switch (ioctl) { ... case KVM_CREATE_VM: r = kvm_dev_ioctl_create_vm(arg); break; ...}
复制代码

该函数的主要任务是调用kvm_create_vm创建虚拟机实例,每一个虚拟机实例用一个struct kvm结构表示,然后通过anon_inode_getfd创建了一个file_operationskvm_vm_fopsd的匿名file,私有数据就是刚刚创建的虚拟机,这个file对应的fd返回给用户态 QEMU,表示一台虚拟机,QEMU 之后就可以通过该fd对虚拟机进行操作了。

3.2.1 kvm_create_vm

去掉不重要过程以及错误处理路径等,kvm_create_vm的主要过程如下:

static struct kvm *kvm_create_vm(unsigned long type, const char *fdname){    struct kvm *kvm = kvm_arch_alloc_vm();    struct kvm_memslots *slots;    int r = -ENOMEM;    int i, j;
... KVM_MMU_LOCK_INIT(kvm); mmgrab(current->mm); kvm->mm = current->mm; kvm_eventfd_init(kvm); mutex_init(&kvm->lock); mutex_init(&kvm->irq_lock); mutex_init(&kvm->slots_lock); mutex_init(&kvm->slots_arch_lock); spin_lock_init(&kvm->mn_invalidate_lock); rcuwait_init(&kvm->mn_memslots_update_rcuwait); xa_init(&kvm->vcpu_array);
INIT_LIST_HEAD(&kvm->gpc_list); spin_lock_init(&kvm->gpc_lock);
INIT_LIST_HEAD(&kvm->devices); kvm->max_vcpus = KVM_MAX_VCPUS;
... for (i = 0; i < KVM_ADDRESS_SPACE_NUM; i++) { ... rcu_assign_pointer(kvm->memslots[i], &kvm->__memslots[i][0]); }
for (i = 0; i < KVM_NR_BUSES; i++) { rcu_assign_pointer(kvm->buses[i], kzalloc(sizeof(struct kvm_io_bus), GFP_KERNEL_ACCOUNT)); ... }
r = kvm_arch_init_vm(kvm, type); ... r = hardware_enable_all(); ... r = kvm_init_mmu_notifier(kvm); ... r = kvm_coalesced_mmio_init(kvm); ... r = kvm_create_vm_debugfs(kvm, fdname); ... r = kvm_arch_post_init_vm(kvm); ...
mutex_lock(&kvm_lock); list_add(&kvm->vm_list, &vm_list); mutex_unlock(&kvm_lock);
preempt_notifier_inc(); kvm_init_pm_notifier(kvm); ...}
复制代码


  1. 首先分配一个 KVM 结构体,用于表示一台虚拟机对象,用于管理虚拟机的各种信息和状态;

  2. 接着执行一系列初始化操作,包括初始化锁、内存管理、事件通知等;

  3. 这里需要注意的是,由于虚拟机的内存其实也就是 QEMU 进程的虚拟内存,因此这里需要引用到当前 QEMU 进程的mm_struct,并且初始化mmu_lock成员来表示操作虚拟机 MMU 数据的锁。

  4. 第一个for 循环,用于初始化虚拟机的内存插槽;

  5. 第二个 for 循环,循环用于初始化虚拟机的 I/O 总线;kvm_io_bus与 Linux 中的总线结构没有关系,它的作用是将内核中实现的模拟设备连接起来,有多种总线类型,如KVM_MMIO_BUSKVM_PIO_BUS

  6. 调用架构特定的初始化函数kvm_arch_init_vm来进一步初始化虚拟机,这部分主要是初始化 KVM 中类型为kvm_archarch成员,用于存放与架构相关的数据;

  7. 调用hardware_enable_all来启用硬件虚拟化支持,此时是最终开启 VMX 模式的地方,这是虚拟机正常运行所必需的;hardware_enable_all会只在创建第一个虚拟机的时候对每个 CPU 调用hardware_enable_nolock,后者则调用kvm_arch_hardware_enable函数来实际完成处理;

  8. kvm_init_mmu_notifier(kvm) - 初始化内存管理单元(MMU)通知器,它是一个编译选项决定的函数,或者为空,或者注册一个 MMU 的通知事件,用于跟踪内存的变化,当 Linux 的内存子系统在进行一些页面管理的时候会调用到这里注册的一些回调函数;

  9. kvm_coalesced_mmio_init(kvm) - 初始化内存映射输入/输出(MMIO)相关的数据结构,这是虚拟机与主机之间进行直接内存访问的一部分;

  10. kvm_create_vm_debugfs(kvm, fdname) - 如果启用了调试支持,创建虚拟机的调试文件系统(debugfs)接口;

  11. kvm_arch_post_init_vm(kvm) - 架构特定的虚拟机初始化后处理;

  12. list_add(&kvm->vm_list, &vm_list) - 将创建的虚拟机添加到虚拟机列表vm_list中;

  13. preempt_notifier_inc() - 增加抢占通知器计数器,以确保在虚拟机运行期间能够适当地处理抢占(用于将 VCPU 线程调度到和调度出 CPU);

  14. kvm_init_pm_notifier(kvm) - 初始化虚拟机的电源管理通知器。

3.2.2 hardware_enable_all

hardware_enable_all的代码如下所示:

static int hardware_enable_all(void) {    int r;
... cpus_read_lock(); mutex_lock(&kvm_lock);
r = 0;
kvm_usage_count++; if (kvm_usage_count == 1) { on_each_cpu(hardware_enable_nolock, &failed, 1); ... }
mutex_unlock(&kvm_lock); cpus_read_unlock();
return r;}
复制代码

hardware_enable_all在每次调用的时候都是递增 KVM 使用计数变量kvm_usage_count,如果递增后取值为 1 则表示当前创建的是第一台虚拟机,此时需要在每个 CPU 上完成 VMX 模式的开启,这个过程通过hardware_enable_nolock函数来完成。

如下所示,hardware_enable_nolock最终调用kvm_arch_hardware_enable函数来完成 VMX 模式的开启:

int kvm_arch_hardware_enable(void){    ...    ret = kvm_x86_check_processor_compatibility();    ...    ret = static_call(kvm_x86_hardware_enable)();    ...}
static int __hardware_enable_nolock(void){ if (__this_cpu_read(hardware_enabled)) return 0;
if (kvm_arch_hardware_enable()) { pr_info("kvm: enabling virtualization on CPU%d failed\n", raw_smp_processor_id()); return -EIO; }
__this_cpu_write(hardware_enabled, true); return 0;}
static void hardware_enable_nolock(void *failed){ if (__hardware_enable_nolock()) atomic_inc(failed);}
复制代码

在 Intel 平台上,kvm_arch_hardware_enable主要调用的是 Intel VMX 实现的vmx_hardware_enable回调函数,该函数的主要作用是设置CR4VMXE位(对应开启条件 6),并且调用VMXON指令开启VMX(对应开启条件 8):

static int kvm_cpu_vmxon(u64 vmxon_pointer){    u64 msr;
cr4_set_bits(X86_CR4_VMXE);
asm_volatile_goto("1: vmxon %[vmxon_pointer]\n\t" _ASM_EXTABLE(1b, %l[fault]) : : [vmxon_pointer] "m"(vmxon_pointer) : : fault); return 0;
fault: WARN_ONCE(1, "VMXON faulted, MSR_IA32_FEAT_CTL (0x3a) = 0x%llx\n", rdmsrl_safe(MSR_IA32_FEAT_CTL, &msr) ? 0xdeadbeef : msr); cr4_clear_bits(X86_CR4_VMXE);
return -EFAULT;}
static int vmx_hardware_enable(void){ int cpu = raw_smp_processor_id(); u64 phys_addr = __pa(per_cpu(vmxarea, cpu)); int r;
if (cr4_read_shadow() & X86_CR4_VMXE) return -EBUSY; ... r = kvm_cpu_vmxon(phys_addr); ... if (enable_ept) ept_sync_global(); ...}
复制代码

参考文献

  1. QEMU/KVM 源码解析与应用 - 李强


关注微信公众号:Linux 内核拾遗

文章来源:https://mp.weixin.qq.com/s/-w1d_3U-Zdm2RYvFvcQ0zA

发布于: 刚刚阅读数: 4
用户头像

聚沙成塔 2023-01-12 加入

分享Linux内核开发相关的编程语言、开发调试工具链、计算机组成及操作系统内核知识、Linux社区最新资讯等

评论

发布
暂无评论
QEMU之CPU虚拟化(三):虚拟机的创建_Linux Kenel_Linux内核拾遗_InfoQ写作社区