本篇介绍kvm自身的执行流程,以及与qemu的交互。kvm的代码本身分为两部分,kernel态代码作为kernel module虚拟出一个字符设备,qemu通过对/dev/kvm
发送ioctl
来实现与kvm的交互。
KVM IOCTL
在上一篇中提到过kvm作为加速器的初始化,不过没有详细说,那么首先我们先看看kvm的初始化。
1 | 2607 static void kvm_accel_class_init(ObjectClass *oc, void *data) |
在QOM的介绍里我提到过kvm通过type_init
将初始化的函数指针加入了QOM的Module_Init_Type
队列中,QEMU在初始化的时候则会通过这个链表调用kvm_init
1 | 1558 static int kvm_init(MachineState *ms) { |
可以发现QEMU最终会通过kvm_ioctl
让kernel态的kvm来完成实际的工作。
Initialization of vCPU
vcpu的初始化方式和kvm类似,以arm为例:
1 | 645 static void arm_cpu_realizefn(DeviceState *dev, Error **errp) |
1 | 1101 static void *qemu_kvm_cpu_thread_fn(void *arg) |
从上面的代码可以发现,从vcpu初始化开始,一步步最后调到qemu_kvm_start_vcpu
, 在这里QEMU会为每一个vcpu创建一个thread, thread的入口函数中则会调用kvm_init_vcpu
和kvm_cpu_exec
所以在qemu的vm_start
中resemu所有的vcpu后,这些会以线程的形式开始执行虚拟机的代码;
在Linux调度的头文件中我们可以发现Thread的属性有一个就是vcpu↓:
1 | 2301 /* |
所以与Xen不同的是, kvm复用了Linux自身的调度器,自己只负责处理这些ioctl
Execution and Context Switch
上面调用了ioctl, 那么处理就是由kvm kernel module来完成的,续上,qemu调用了kvm_cpu_exec
后,里面会调用ioctl:
1 | 2524 static long kvm_vcpu_ioctl(struct file *filp, unsigned int ioctl, unsigned long arg) |
1 | 589 int kvm_arch_vcpu_ioctl_run(struct kvm_vcpu *vcpu, struct kvm_run *run) |
module中的代码负责处理kvm_cpu_exec
的就是上面这个函数,module在初始化的时候会创建一个/dev/kvm
设备,同时会注册相应的ioctl的handler, 代码同样在这个文件中,比较简单就不放了。
这里需要注意的是ret = kvm_call_hyp(__kvm_vcpu_run, vcpu);
, arm中的Hypercall调用方式与x86不太一样,虚拟机的运行模式也不太一样,这里的意思就是进入Hypervisor(hyp)模式,并开始执行__kvm_vcpu_run
, 这个函数最终会进入guest mode:
1 | 52 /* |
在存储host状态并恢复guest状态后, 调用eret
, 根据手册中描述:
In a processor that implements the Virtualization Extensions, you can use ERET to perform a return from an exception taken to Hyp mode.
When executed in Hyp mode, ERET loads the PC from
ELR_hyp
and loads theCPSR
fromSPSR_hyp
. When executed in any other mode, apart from User or System, it behaves as:
MOVS PC, LR in the ARM instruction set
SUBS PC, LR, #0 in the Thumb instruction set.
1 | 311 /* |
1 | 124 el1_irq: |
1 | 91 ENTRY(__guest_exit) |
加载了PC后就会执行guest代码了,则会由Exception向量表跳到对应的入口,最后借由__guest_exit
返回重新加载host的context, 那么就会回到进入guest mode前的下一条指令开始处理exit。
关于exit处理在之后的文章里再说.
Hypercall
上面也提了,ret = kvm_call_hyp(__kvm_vcpu_run, vcpu);
这个是借助Hypercall完成的,那么我们看看kvm_call_hyp
:
1 | 25 /* u64 __kvm_call_hyp(void *hypfn, ...); */ |
实际上就是调用了hvc指令,这个指令根据arm手册,x0里面存的是下一条要在hyp mode中执行的代码。
这样就可以完成hypercall的调用,需要注意的是arm里进入hyp mode会切换页表,所以必须保证之后要执行的代码在hyp mode里面有映射
Initialization of Hyp mode
1 | 3909 int kvm_init(void *opaque, unsigned vcpu_size, unsigned vcpu_align, |
1 | 1410 /** |
1 | 1299 static int init_hyp_mode(void) { |
在kvm module被加载初始化的时候,就会初始化hyp mode, 并映射相应的代码,(stage2 page table)也是在这个时候初始化的。
Note
- 到这里应该比较清楚kvm module初始化做了什么(初始化hyp mode, 虚拟一个
/dev/kvm
设备等等), 以及如何进入guest, 如何与qemu进行交互了。 - 与x86的独立于ring0和ring3的non-root (guest mode)不同,arm的hyp mode是在EL1之下的权限级(EL2),而Hypercall的调用则更像一个jump而非x86中向量表的方式。
- x86中Non-root下执行的代码会由VMCS/VMCB来配置下陷,而arm中则是直接跑在上面的EL0和EL1, 然后再根据情况一级级下陷,所以ARM的虚拟化方式更像没有硬件虚拟化支持之前的x86的虚拟化; (kernel跑在ring1, hypervisor跑在ring0的样子)