爱悠闲 > Kvm代码解析连载 (三) :vcpu的调度(preempt notifier的妙用)

Kvm代码解析连载 (三) :vcpu的调度(preempt notifier的妙用)

分类: KVM  |  标签: linux内核,kvm,虚拟化技术,Intel VT,内核  |  作者: fanwenyi 相关  |  发布日期 : 2015-04-11  |  热度 : 1794°

        在虚拟机中,一个相当重要的环节就是host和guest之间的切换,我称之为World Switch,在World Switch完成的前后,完全是两个世界(从host切换到guest或者从guest切回host)。对于硬件辅助虚拟化技术来说,最关键的点即为vmlauch(vmx)和vmrun(svm)指令,该指令执行之后,就开始执行guest的代码了。这里看起来很简单,很清晰,但是实际上在这之前是需要做很多很多的工作的,在这之后也一样,我们这次先来看一下从qemu进入kmod之后,是如何完成host->guest->host切换这一流程的(以vmx为例)。

        关于preemptnotifier:在vmx环境下,我们知道需要在vmcs中保存各种信息,比如host和guest的state,以及某些控制字段。目前绝大多数的cpu都是多核的,这里就存在一个问题:考虑如下情况:

        

               

         这时,关注如下几点:

                1、  在这个点,有可能cpu 1根本就没开启vmx模式,这里就直接oops了。

                2、  如果侥幸赶上cpu 1开启了vmx,这里也可能会失败,因为cpu1的vmcs pointer指向的不一定是 vmcs 0。

                3、  如果1和2都成功,这里很大的几率会挂掉,因为这时从vmcs中恢复的host state,极有可能是错误的,这时很大几率host就会挂掉。

        在实际运行中,由于world switch频率极大,如果出现了上述情况,虚拟机一启动host会瞬间挂掉。

        下面我们来看一下如何解决这个问题:

                1、  在vcpu n 的线程进入kmod的时候,禁用抢占。

                2、  在vcpu n的线程被sched out和sched in的时候,采用某种机制来得知这个事件,并且对vmcs等作出处理。

         方案1中,无疑是最简单的,但是禁用抢占之后,对host的影响比较大,因为world switch整个过程中的指令流是比较多的,这里会对其他进程造成影响。

         方案2看起来是很不错的解决方案,但是这里存在一个问题,就是host os需要有一种通知机制来通知vmm,使得vmm在这几个关键的点上,及时作出处理,避免出现问题。在windows内核中,是没有这种机制的,但是linux内核可以直接利用preempt notifier。

         在preempt notifier机制中,如果发生了内核抢占,如果该进程注册了preempt notifier,有两个回调函数:

         

    void (*sched_in)(struct preempt_notifier *notifier, int cpu);
        

        该回调在线程被调度执行之前调用。

        

    void (*sched_out)(struct preempt_notifier *notifier, struct task_struct *next);
        

        

        该回调在线程刚刚被抢占之后执行。

        现在linux内核有这种机制,我们就可以高效的解决这个问题了,下面我们看一下kvm是怎么做的:

在vcpu创建的时候,会初始化该vcpu的notifier:

        
    preempt_notifier_init(&vcpu->preempt_notifier, &kvm_preempt_ops);

kvm_main.c

int vcpu_load(struct kvm_vcpu *vcpu)
{
		……
		cpu = get_cpu();   //先关掉抢占,同时获取当前物理cpu的id
		preempt_notifier_register(&vcpu->preempt_notifier);   //注册一下preempt notifier
		kvm_arch_vcpu_load(vcpu, cpu);   //load vcpu的state,可以理解为把vcpu和物理cpu帮顶起来
		put_cpu();   //打开抢占
		return 0;
}
void vcpu_put(struct kvm_vcpu *vcpu)
{
	preempt_disable();  
	kvm_arch_vcpu_put(vcpu);  //把当前vcpu从物理cpu解下来,主要是做一些关闭vmx以及清理的操作
	kvm_fire_urn();    //fire掉user return notifier,这个我们以后会讲
	preempt_notifier_unregister(&vcpu->preempt_notifier);  //反注册一下
	preempt_enable();
	mutex_unlock(&vcpu->mutex);
}

由此可见,在worldswitch开始和结束的时候,kmod会对preempt notifier进行操作,以在被抢占时,能够做出处理。下面我们来看在两个回调中是如何处理的:

kvm_main.c:

static inline
struct kvm_vcpu *preempt_notifier_to_vcpu(struct preempt_notifier *pn)
{
	return container_of(pn, struct kvm_vcpu, preempt_notifier);  //封装一下preempt notifier到vcpu的转换
}

static void kvm_sched_in(struct preempt_notifier *pn, int cpu)
{
	struct kvm_vcpu *vcpu = preempt_notifier_to_vcpu(pn);
	if (vcpu->preempted)
		vcpu->preempted = false;  //置个标志

	kvm_arch_vcpu_load(vcpu, cpu);    //绑定
}

static void kvm_sched_out(struct preempt_notifier *pn,
			  struct task_struct *next)
{
	struct kvm_vcpu *vcpu = preempt_notifier_to_vcpu(pn);

	if (current->state == TASK_RUNNING)
		vcpu->preempted = true;  //置个标志
	kvm_arch_vcpu_put(vcpu);    //被强占了,解绑,释放当前的物理cpu
	kvm_fire_urn();             //fire掉user return notifier
}

总结:

从上文可以看到,kvm对linux内核利用的极其巧妙,而linux内核中灵活的各种notifier,也是kvm能够在宿主虚拟机架构下仍然能够保持较高性能的必要条件。fvm在windows上运行的时候,由于windows内核的封闭,我们很难做出完美的处理,只能选择禁用抢占这一方法。

在后续的文章中,我们会逐一分析kvm在对vcpu调度的过程中的种种细节。在vcpu调度的最后一篇文章中,我们将把整个流程串起来。