在虚拟机中,一个相当重要的环节就是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_in)(struct preempt_notifier *notifier, int cpu);
该回调在线程被调度执行之前调用。
-
void (*sched_out)(struct preempt_notifier *notifier, struct task_struct *next);
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);
preempt_notifier_init(&vcpu->preempt_notifier, &kvm_preempt_ops);
kvm_main.c
-
int vcpu_load(struct kvm_vcpu *vcpu)
-
{
-
……
-
cpu = get_cpu();
-
preempt_notifier_register(&vcpu->preempt_notifier);
-
kvm_arch_vcpu_load(vcpu, cpu);
-
put_cpu();
-
return 0;
-
}
-
void vcpu_put(struct kvm_vcpu *vcpu)
-
{
-
preempt_disable();
-
kvm_arch_vcpu_put(vcpu);
-
kvm_fire_urn();
-
preempt_notifier_unregister(&vcpu->preempt_notifier);
-
preempt_enable();
-
mutex_unlock(&vcpu->mutex);
-
}
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);
-
}
-
-
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);
-
kvm_fire_urn();
-
}
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调度的最后一篇文章中,我们将把整个流程串起来。
阅读(2113) | 评论(0) | 转发(1) |