QEMU之CPU虚拟化
3 虚拟机的创建
关注微信公众号:Linux内核拾遗
要创建一个KVM虚拟机,需要用户侧的QEMU发起请求,然后KVM配合完成虚拟机的创建,本文结合QEMU和KVM两个方面来介绍KVM虚拟机创建过程。
3.1 QEMU侧虚拟机的创建
3.1.1 QEMU加速器介绍
QEMU作为一个开源虚拟化和模拟平台,支持多种加速器和后端选项,以提高虚拟机性能和功能。这些加速器和后端选项可以根据不同的用例和需求进行配置。
QEMU的具体加速器和后端选项可能会因QEMU的版本和配置而异。可以使用以下命令查看当前版本的QEMU支持的加速器和后端选项,:
shell
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分支:
c
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
。
c
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
。
c
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
。
c
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
主要代码如下:
c
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
函数来进行处理:
c
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_operations
为kvm_vm_fopsd
的匿名file
,私有数据就是刚刚创建的虚拟机,这个file
对应的fd
返回给用户态QEMU,表示一台虚拟机,QEMU之后就可以通过该fd
对虚拟机进行操作了。
3.2.1 kvm_create_vm
去掉不重要过程以及错误处理路径等,kvm_create_vm
的主要过程如下:
c
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);
...
}
- 首先分配一个KVM结构体,用于表示一台虚拟机对象,用于管理虚拟机的各种信息和状态;
- 接着执行一系列初始化操作,包括初始化锁、内存管理、事件通知等;
- 这里需要注意的是,由于虚拟机的内存其实也就是QEMU进程的虚拟内存,因此这里需要引用到当前QEMU进程的
mm_struct
,并且初始化mmu_lock
成员来表示操作虚拟机MMU数据的锁。
- 这里需要注意的是,由于虚拟机的内存其实也就是QEMU进程的虚拟内存,因此这里需要引用到当前QEMU进程的
- 第一个
for
循环,用于初始化虚拟机的内存插槽; - 第二个
for
循环,循环用于初始化虚拟机的I/O总线;kvm_io_bus
与Linux中的总线结构没有关系,它的作用是将内核中实现的模拟设备连接起来,有多种总线类型,如KVM_MMIO_BUS
和KVM_PIO_BUS
; - 调用架构特定的初始化函数
kvm_arch_init_vm
来进一步初始化虚拟机,这部分主要是初始化KVM中类型为kvm_arch
的arch
成员,用于存放与架构相关的数据; - 调用
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
的代码如下所示:
c
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模式的开启:
c
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
回调函数,该函数的主要作用是设置CR4
的VMXE
位(对应开启条件6),并且调用VMXON
指令开启VMX
(对应开启条件8):
c
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内核拾遗