简介
在实际业务中,guest执行HLT指令是导致虚拟化overhead的一个重要原因。如[1].
KVM halt polling特性就是为了解决这一个问题被引入的,它在Linux 4.3-rc1被合入主干内核,其基本原理是当guest idle发生vm-exit时,host 继续polling一段时间,用于减少guest的业务时延。进一步讲,在vcpu进入idle之后,guest内核默认处理是执行HLT指令,就会发生vm-exit,host kernel并不马上让出物理核给调度器,而是poll一段时间,若guest在这段时间内被唤醒,便可以马上调度回该vcpu线程继续运行。
polling机制带来时延上的降低,至少是一个线程调度周期,通常是几微妙,但最终的性能提升是跟guest内业务模型相关的。如果在host kernel polling期间,没有唤醒事件发生或是运行队列里面其他任务变成runnable状态,那么调度器就会被唤醒去干其他任务的事。因此,halt polling机制对于那些在很短时间间隔就会被唤醒一次的业务特别有效。
代码流程
guest执行HLT指令发生vm-exit后,kvm处理该异常,在kvm_emulate_halt处理最后调用kvm_vcpu_halt(vcpu)。
int kvm_vcpu_halt(struct kvm_vcpu *vcpu){
++vcpu->stat.halt_exits; if (lapic_in_kernel(vcpu)) {
vcpu->arch.mp_state = KVM_MP_STATE_HALTED; return 1;
} else {
vcpu->run->exit_reason = KVM_EXIT_HLT; return 0;
}
}
将mp_state的值置为KVM_MP_STATE_HALTED,并返回1。
static int vcpu_run(struct kvm_vcpu *vcpu){ int r; struct kvm *kvm = vcpu->kvm;
vcpu->srcu_idx = srcu_read_lock(&kvm->srcu); for (;;) { if (kvm_vcpu_running(vcpu)) {
r = vcpu_enter_guest(vcpu);
} else {
r = vcpu_block(kvm, vcpu);
} if (r <= 0) break; //省略
}
}
由于kvm处理完halt异常后返回1,故主循环不退出,但在下一个循环时kvm_vcpu_running(vcpu)返回false,所以进入vcpu_block()分支,随机调用kvm_vcpu_block()。
通用的halt polling代码在virt/kvm/kvm_main.c文件中的额kvm_vcpu_block()函数中实现。
ktime_t stop = ktime_add_ns(ktime_get(), vcpu->halt_poll_ns);do { /*
* This sets KVM_REQ_UNHALT if an interrupt
* arrives.
*/
if (kvm_vcpu_check_block(vcpu) < 0) {
++vcpu->stat.halt_successful_poll; if (!vcpu_valid_wakeup(vcpu))
++vcpu->stat.halt_poll_invalid; goto out;
}
cur = ktime_get();
} while (single_task_running() && ktime_before(cur, stop));
情况一:如果当前物理核上没有其他task处于running状态,而且在polling时间间隔内,那么就一直等着,直到kvm_vcpu_check_block(vcpu) < 0,即vcpu等待的中断到达,便跳出循环。
out:
block_ns = ktime_to_ns(cur) - ktime_to_ns(start); if (!vcpu_valid_wakeup(vcpu))
shrink_halt_poll_ns(vcpu); else if (halt_poll_ns) { if (block_ns <= vcpu->halt_poll_ns)
; /* we had a long block, shrink polling */
else if (vcpu->halt_poll_ns && block_ns > halt_poll_ns)
shrink_halt_poll_ns(vcpu); /* we had a short halt and our poll time is too small */
else if (vcpu->halt_poll_ns < halt_poll_ns &&
block_ns < halt_poll_ns)
grow_halt_poll_ns(vcpu);
} else
vcpu->halt_poll_ns = 0;
trace_kvm_vcpu_wakeup(block_ns, waited, vcpu_valid_wakeup(vcpu));
kvm_arch_vcpu_block_finish(vcpu);
这段代码主要用于调整下一次polling的等待时间。若block_ns大于halt_poll_ns,即vcpu halt时间很短就被唤醒了,则把下一次的halt_poll_ns调长;否则,减短。
情况二:如果当前物理核上其他task变成running态,或polling时间到期,则唤醒调度器,调度其他任务,如下代码。
for (;;) {
prepare_to_swait(&vcpu->wq, &wait, TASK_INTERRUPTIBLE); if (kvm_vcpu_check_block(vcpu) < 0) break;
waited = true;
schedule();
} |