一、问题概述
虚拟化的实现方式有很多种,比较常见的一类是基于KVM/QEMU架构实现的虚拟化,相关的架构如下图所示:
在虚机启动之后正常运行的过程中,虚机就是在不断的经历ioctl进入,返回,进入,返回的循环过程,如下面的伪码所示。
上述伪码是在用户空间中QEMU代码的实现模型,在内核中的KVM实现中,相关代码和用户空间类似,也是在进入虚机运行,等待虚机退出之后,会依据退出事件类型调用不同的处理钩子函数。唯一不同的是,如果退出事件内核判断内核层就可以处理完毕,就不需要再退出到用户层处理,而是直接在内核层就直接转入虚机运行态。这个过程如下图所示:
虚机的退出事件处理是虚机能正常运行的必要条件,对虚机运行意义重大,一个方面是退出事件的处理过程消耗对虚机性能表现影响巨大,例如virtio本质上也是为了解决频繁的退出事件对虚机的影响提出的优化解决方案,另一方面是退出事件是对虚机运行情况最直观的反映,因为虚机在进入vt-x模式之后,里面的运行情况外部来看其实是一个黑盒,只有虚机在触发一定条件后退出vt-x模式,宿主机才可以通过退出时携带的状态数据推测出当前虚机运行的关键信息,进而可以定位或者解决问题。
二、VM-EXIT事件
触发虚机退出的事件有很多,罗列如下:
FAILED_VMENTRY、EXCEPTION_NMI、EXTERNAL_INTERRUPT、TRIPLE_FAULT、PENDING_INTERRUPT、NMI_WINDOW、TASK_SWITCH、CPUID、HLT、INVD、INVLPG、RDPMC、RDTSC、VMCALL、VMCLEAR、VMLAUNCH、VMPTRLD、VMPTRST、VMREAD、VMRESUME、VMWRITE、VMOFF、VMON、CR_ACCESS、DR_ACCESS、IO_INSTRUCTION、MSR_READ、MSR_WRITE、INVALID_STATE、MSR_LOAD_FAIL、MWAIT_INSTRUCTION、MONITOR_TRAP_FLAG、MONITOR_INSTRUCTION、PAUSE_INSTRUCTION、MCE_DURING_VMENTRY、TPR_BELOW_THRESHOLD、APIC_ACCESS、EOI_INDUCED、GDTR_IDTR、LDTR_TR、EPT_VIOLATION、EPT_MISCONFIG、INVEPT、RDTSCP、PREEMPTION_TIMER、INVVPID、WBINVD、XSETBV、APIC_WRITE、INVPCID、PML_FULL、XSAVES、XRSTORS。
但在虚机的正常运行过程中,最常见的大概有那么几个,后面部分会分不同小结对此进行陈述。
2.1辅助工具
要跟踪虚机的退出事件,需要使用必要的工具,参考虚机退出的处理流程,相对的可以有不同的工具来辅助处理。例如在内核层截获这个退出事件可以使用perf工具或者使用SystemTap,在用户层截获退出事件并调试可以参考使用gdb工具。perf及gdb都是比较好用的工具,功能比较强大,详细使用可以参考帮助手册。
2.1.1 Perf
Perf是一个linux平台上的性能调优工具,多用于软件性能分析,安装和使用Perf非常的容易。Perf主要是利用PMU、tracepoint和内核中的特殊计数器来进行事件统计,通过这个可以定位性能问题,也可以作为探测工具用于发现系统的一些其他运行信息。
这是使用Perf工具对虚机运行情况的一次跟踪,从信息可以看出虚机在这段时间内,是有很多次不同事件引起了vm-exit。
Perf的功能很强大,子命令很丰富,详细的可以参考帮助文档。
2.1.2 Gdb
Gdb是GNU开源组织发布的一个强大的UNIX下的程序调试工具。我们在这里提到使用Gdb跟踪vm-exit事件主要是考虑可以跟踪进入ioctl之后退出逻辑的处理,在这个退出逻辑里,可以使用Gdb跟踪用户层程序qemu对于vm-exit事件的处理流程。
2.1.3 Qemu-event
在qemu里面追踪event事件这个特性是由qemu提供的,该功能主要用来进行debug、性能分析或者监测qemu运行状况。要使用该功能,需要在编译qemu的时候增加配置项--enable-trace-backends=simple,但打开这个配置项之后,qemu的运行性能会有一定的损耗,可以跟踪的qemu-event可以通过qemu提供的交互命令info trace-events列出来,具体的使用可以参考qemu提供的功能说明文档进行操作。不过因为使用这个工具需要重新编译qemu,使用上会有一些不方便。
2.2 VM-RUN整体处理逻辑
从前面的叙述也可以知道,VM-Run的最初发起方肯定是qemu的一个vcpu线程,其通过调用ioctl系统函数进入kvm的vcpu处理逻辑,之后会通过判断虚机当前的状态,一切都合适的话会陷入vmx-t状态,在虚机触发vm-exit之后,会返回宿主机上下文,然后处理当前的退出事件,之后判断是否需要再次进入vmx-t状态。其在kvm中,代码上面的流程为:
static long kvm_vcpu_ioctl(struct file *filp, unsigned int ioctl, unsigned long arg)
{
.
.
.
r =vcpu_load(vcpu);
if (r)
returnr;
switch(ioctl) {
caseKVM_RUN:
r= -EINVAL;
if(arg)
gotoout;
if(unlikely(vcpu->pid != current->pids[PIDTYPE_PID].pid)) {
/*The thread running this VCPU changed. */
structpid *oldpid = vcpu->pid;
structpid *newpid = get_task_pid(current, PIDTYPE_PID);
rcu_assign_pointer(vcpu->pid,newpid);
if(oldpid)
synchronize_rcu();
put_pid(oldpid);
}
r = kvm_arch_vcpu_ioctl_run(vcpu,vcpu->run);
trace_kvm_userspace_exit(vcpu->run->exit_reason,r);
break;
caseKVM_GET_REGS: {
.
.
.
default:
r= kvm_arch_vcpu_ioctl(filp, ioctl, arg);
}
out:
vcpu_put(vcpu);
kfree(fpu);
kfree(kvm_sregs);
returnr;
}
这里主要的调用函数是kvm_arch_vcpu_iocto_run函数,会依据当前的架构体系调用不同的函数,x86上面的实现函数是:
int kvm_arch_vcpu_ioctl_run(struct kvm_vcpu *vcpu,struct kvm_run *kvm_run)
{
.
.
.
/*re-sync apic's tpr */
if(!lapic_in_kernel(vcpu)) {
if(kvm_set_cr8(vcpu, kvm_run->cr8) != 0) {
r= -EINVAL;
gotoout;
}
}
if(unlikely(vcpu->arch.complete_userspace_io)) {
int(*cui)(struct kvm_vcpu *) = vcpu->arch.complete_userspace_io;
vcpu->arch.complete_userspace_io= NULL;
r= cui(vcpu);
if(r
gotoout;
} else
if(kvm_run->immediate_exit)
r= -EINTR;
else
r = vcpu_run(vcpu);
out:
post_kvm_run_save(vcpu);
if(vcpu->sigset_active)
sigprocmask(SIG_SETMASK,&sigsaved, NULL);
returnr;
}
在这个函数处理体中,会再次进行一些状态判断,例如本线程是否有待处理信号,并对多核架构的系统判断多核系统是否初始化完毕,没有的话,会在这里进行一个挂起操作。本函数最主要的入口点是vcpu_run,在这个函数里面,会真正实现循环进入虚机的处理体。
static int vcpu_run(struct kvm_vcpu *vcpu)
{
.
for(;;) {
if(kvm_vcpu_running(vcpu)) {
r= vcpu_enter_guest(vcpu);
}else {
r= vcpu_block(kvm, vcpu);
}
if(r
break;
clear_bit(KVM_REQ_PENDING_TIMER,&vcpu->requests);
if(kvm_cpu_has_pending_timer(vcpu))
kvm_inject_pending_timer_irqs(vcpu);
.
if(need_resched()) {
srcu_read_unlock(&kvm->srcu,vcpu->srcu_idx);
cond_resched();
vcpu->srcu_idx= srcu_read_lock(&kvm->srcu);
}
}
srcu_read_unlock(&kvm->srcu,vcpu->srcu_idx);
returnr;
}
从这个while(1)的循环中,可以看出这里的退出逻辑是进入虚机的调用返回小于等于的值,所以返回大于的话,用户空间的ioctl调用会一直没有返回,但即使没有返回用户层,也不会导致本物理核上的其他进程迟迟得不到响应,因为在最后会有一个类似schedule的调用。
本阶段最重要的实现函数是vcpu_enter_guest,在这个函数里面会完成直接的状态判断,模式切换,并判断处理最后的vmexit事件。
static int vcpu_enter_guest(struct kvm_vcpu *vcpu)
{
.
if(vcpu->requests) {
if(kvm_check_request(KVM_REQ_MMU_RELOAD, vcpu))
kvm_mmu_unload(vcpu);
}
.类似的vcpu状态检查。}
if(kvm_check_request(KVM_REQ_EVENT, vcpu) || req_int_win) {
++vcpu->stat.req_event;
kvm_apic_accept_events(vcpu);
if(vcpu->arch.mp_state == KVM_MP_STATE_INIT_RECEIVED) {
r= 1;
gotoout;
}
if(inject_pending_event(vcpu, req_int_win) != 0)
req_immediate_exit= true;
else{
if(vcpu->arch.smi_pending && !is_smm(vcpu))
req_immediate_exit= true;
if(vcpu->arch.nmi_pending)
kvm_x86_ops->enable_nmi_window(vcpu);
if(kvm_cpu_has_injectable_intr(vcpu) || req_int_win)
kvm_x86_ops->enable_irq_window(vcpu);
}
if(kvm_lapic_enabled(vcpu)) {
update_cr8_intercept(vcpu);
kvm_lapic_sync_to_vapic(vcpu);
}
}
r =kvm_mmu_reload(vcpu); //装载内存空间
if(unlikely(r)) {
gotocancel_injection;
}
preempt_disable();//关抢占
kvm_x86_ops->prepare_guest_switch(vcpu);
kvm_load_guest_fpu(vcpu);
/*
* Disable IRQs before settingIN_GUEST_MODE. Posted interrupt
* IPI are then delayed after guest entry,which ensures that they
* result in virtual interrupt delivery.
*/
local_irq_disable(); //关中
vcpu->mode= IN_GUEST_MODE;
srcu_read_unlock(&vcpu->kvm->srcu,vcpu->srcu_idx);
/*
* 1) We should set ->mode before checking->requests. Please see
* the comment in kvm_make_all_cpus_request.
*
* 2) For APICv, we should set ->mode beforechecking PIR.ON. This
* pairs with the memory barrier implicit inpi_test_and_set_on
* (see vmx_deliver_posted_interrupt).
*
* 3) This also orders the write to mode fromany reads to the page
* tables done while the VCPU is running. Please see the comment
* in kvm_flush_remote_tlbs.
*/
smp_mb__after_srcu_read_unlock();
/*
* This handles the case where a postedinterrupt was
* notified with kvm_vcpu_kick.
*/
if(kvm_lapic_enabled(vcpu)) {
if(kvm_x86_ops->sync_pir_to_irr && vcpu->arch.apicv_active)
kvm_x86_ops->sync_pir_to_irr(vcpu);
}
if(vcpu->mode == EXITING_GUEST_MODE || vcpu->requests
|| need_resched() ||signal_pending(current)) {
vcpu->mode= OUTSIDE_GUEST_MODE;
smp_wmb();
local_irq_enable();
preempt_enable();
vcpu->srcu_idx= srcu_read_lock(&vcpu->kvm->srcu);
r= 1;
gotocancel_injection;
}
kvm_load_guest_xcr0(vcpu);
if(req_immediate_exit) {
kvm_make_request(KVM_REQ_EVENT,vcpu);
smp_send_reschedule(vcpu->cpu);
}
trace_kvm_entry(vcpu->vcpu_id);
wait_lapic_expire(vcpu);
guest_enter_irqoff();
if(unlikely(vcpu->arch.switch_db_regs)) {
set_debugreg(0,7);
set_debugreg(vcpu->arch.eff_db[0],0);
set_debugreg(vcpu->arch.eff_db[1],1);
set_debugreg(vcpu->arch.eff_db[2],2);
set_debugreg(vcpu->arch.eff_db[3],3);
set_debugreg(vcpu->arch.dr6,6);
vcpu->arch.switch_db_regs&= ~KVM_DEBUGREG_RELOAD;
}
kvm_x86_ops->run(vcpu);
/*
* If the guest has used debug registers, atleast dr7
* will be disabled while returning to thehost.
* If we don't have active breakpoints in thehost, we don't
* care about the messed up debug addressregisters. But if
* we have some of them active, restore the oldstate.
*/
if(hw_breakpoint_active())
hw_breakpoint_restore();
vcpu->arch.last_guest_tsc= kvm_read_l1_tsc(vcpu, rdtsc());
vcpu->mode= OUTSIDE_GUEST_MODE;
smp_wmb();
kvm_put_guest_xcr0(vcpu);
kvm_x86_ops->handle_external_intr(vcpu);
++vcpu->stat.exits;
guest_exit_irqoff();
local_irq_enable(); //开中
preempt_enable(); //开抢占
vcpu->srcu_idx= srcu_read_lock(&vcpu->kvm->srcu);
r = kvm_x86_ops->handle_exit(vcpu);
returnr;
cancel_injection:
kvm_x86_ops->cancel_injection(vcpu);
if (unlikely(vcpu->arch.apic_attention))
kvm_lapic_sync_from_vapic(vcpu);
out:
returnr;
}
本函数是虚拟化切换最直接的处理函数,功能实现的关键点涉及最多,进入vmx-t模式运行是通过调用体系钩子函数kvm_x86_ops->run(vcpu)实现,在之后的分类事件处理是通过体系注册钩子函数这个kvm_x86_ops->handle_exit(vcpu)实现,对于vmx架构,真实的函数体是在vmx.c文件里面的staticint (*const kvm_vmx_exit_handlers[])(struct kvm_vcpu *vcpu)。可以参考具体函数跟踪其实现。
领取专属 10元无门槛券
私享最新 技术干货