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

2023年 9月 3日 60.6k 0

QEMU之CPU虚拟化
3 虚拟机的创建

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

文章来源:mp.weixin.qq.com/s/-w1d_3U-Z…

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

3.1 QEMU侧虚拟机的创建

3.1.1 QEMU加速器介绍

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

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

qemu-system-x86_64 -accel help

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

  • KVM(Kernel-based Virtual Machine)加速器:KVM是一种在Linux内核中实现的虚拟化解决方案,它可以与QEMU结合使用,提供高性能的硬件虚拟化。KVM通常是QEMU的首选加速器。
  • HAXM(Hardware Accelerated Execution Manager):HAXM是Intel提供的加速器,专门用于在基于Intel处理器的系统上运行虚拟机。
  • HVF(Hypervisor.framework Virtualization Framework):HVF是Apple macOS上的一种虚拟化加速器,用于在Mac上运行虚拟机。
  • 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相关的数据结构,并且完成如下的一些初始化过程:

  • kvm_init函数首先打开/dev/kvm设备得到一个fd,并且会保存到类型为KVMState的变量s的成员fd中;
  • 检查KVM的版本;
  • 检测是否支持KVM的一些扩展特性;
  • 调用ioctl(KVM_CREATE_VM)接口在KVM层面创建一个虚拟机;
  • 将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 memslots[i], &kvm->__memslots[i][0]);
    	}
    
    	for (i = 0; i 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);
    	...
    }
    
  • 首先分配一个KVM结构体,用于表示一台虚拟机对象,用于管理虚拟机的各种信息和状态;
  • 接着执行一系列初始化操作,包括初始化锁、内存管理、事件通知等;
  • 这里需要注意的是,由于虚拟机的内存其实也就是QEMU进程的虚拟内存,因此这里需要引用到当前QEMU进程的mm_struct,并且初始化mmu_lock成员来表示操作虚拟机MMU数据的锁。
  • 第一个for 循环,用于初始化虚拟机的内存插槽;
  • 第二个 for 循环,循环用于初始化虚拟机的I/O总线;kvm_io_bus与Linux中的总线结构没有关系,它的作用是将内核中实现的模拟设备连接起来,有多种总线类型,如KVM_MMIO_BUSKVM_PIO_BUS
  • 调用架构特定的初始化函数kvm_arch_init_vm来进一步初始化虚拟机,这部分主要是初始化KVM中类型为kvm_archarch成员,用于存放与架构相关的数据;
  • 调用hardware_enable_all来启用硬件虚拟化支持,此时是最终开启VMX模式的地方,这是虚拟机正常运行所必需的;hardware_enable_all会只在创建第一个虚拟机的时候对每个CPU调用hardware_enable_nolock,后者则调用kvm_arch_hardware_enable函数来实际完成处理;
  • kvm_init_mmu_notifier(kvm) - 初始化内存管理单元(MMU)通知器,它是一个编译选项决定的函数,或者为空,或者注册一个MMU的通知事件,用于跟踪内存的变化,当Linux的内存子系统在进行一些页面管理的时候会调用到这里注册的一些回调函数;
  • kvm_coalesced_mmio_init(kvm) - 初始化内存映射输入/输出(MMIO)相关的数据结构,这是虚拟机与主机之间进行直接内存访问的一部分;
  • kvm_create_vm_debugfs(kvm, fdname) - 如果启用了调试支持,创建虚拟机的调试文件系统(debugfs)接口;
  • kvm_arch_post_init_vm(kvm) - 架构特定的虚拟机初始化后处理;
  • list_add(&kvm->vm_list, &vm_list) - 将创建的虚拟机添加到虚拟机列表vm_list中;
  • preempt_notifier_inc() - 增加抢占通知器计数器,以确保在虚拟机运行期间能够适当地处理抢占(用于将VCPU线程调度到和调度出CPU);
  • 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();
      ...
    }
    

    参考文献

  • QEMU/KVM源码解析与应用 - 李强
  • 关注微信公众号:Linux内核拾遗

    文章来源:mp.weixin.qq.com/s/-w1d_3U-Z…

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论