本文介绍ARM64平台KVM的时钟虚拟化的原理和实现。ARM64的KVM时钟虚拟化基本是在内核层的KVM实现的,利用ARM64提供的硬件时钟资源。qemu层只是在必要的(比如迁移)时候会调用ioctl设置虚拟化机时钟的寄存器。
ARM64的时钟硬件叫做ARM generic timer。它的硬件block主要是SOC上的System counter(多个process共享,用来记录时间的流逝)以及附着在各个processor上的Timer(用于触发timer event)组成。
各个cpu的timer是根据system counter的值来触发timer event的,因此,系统中一定有一个机制让System counter的值广播到各个CPU的timer HW block上,同时运行在各个processor上的软件可以通过接口获取System counter的值。
处理器可以通过CNTPCT寄存器来获取system counter的当前值,我们称之physical counter。有physical就有virtual counter,processor可以通过CNTVCT寄存器访问virtual counter,不过,对于不支持security extension和virtualization extension的系统,virtual counter和physical counter是一样的值。但是在支持virtualization extension的系统上virtual counter的值等于physical counter的值减去一个偏移,该偏移保存在一个寄存器上。
每个处理器上有4个timer,安全和非安全物理timer,hypervisor timer和virtal timer。每个timer有3个寄存器,两个比较寄存器和一个控制寄存器。控制寄存器用来控制timer的行为,比如enable和unenable以及mask和unmask等。两个比较定时器用来设置什么时候产生定时中断,两个比较寄存器任意使用其中一个就可以产生定时中断。
时钟虚拟化要为guest os提供两种设备资源:计时资源:clock_source设备和时钟定时器:clock_event设备。
对于clock_event设备,ARM64平台每个处理器有4个timer,3个物理timer和1个虚拟timer。物理机用物理timer,虚拟机用虚拟timer,相互之间并无资源冲突,因此KVM/qemu不需要去模拟一个timer设备,虚拟机直接使用处理器的的虚拟timer。但是在多个虚拟机之间切换时要保存和恢复虚拟timer的运行状态。
对于clock_source设备,按照ARM64的timer的设计,有物理counter和虚拟counter,物理机使用物理counter,虚拟机使用虚拟counter。但是ARM64-KVM并没有这么使用,物理机和虚拟机都使用的虚拟counter,因为操作系统对counter设备的使用很简单,就是读取其counter值,counter寄存器在host和guest之间共享很简单,在进入和退出guest的时候减去或加上一个offset就行了。
除了虚拟机中时钟硬件资源的支持外,还要考虑时钟的中断虚拟化,由于ARM64-KVM的虚拟机直接使用的是物理CPU的虚拟timer设备,因此其物理中断要被映射到虚拟机的中断控制器中。具体情况是虚拟counter的中断被路由到EL2层,当虚拟机执行事,时钟到期,虚拟counter中断产生,这时guest退出到,KVM处理该中断,执行中断注入动作(把中断路由给vGIC),然后再次进入guest时,guest就接收到中断了。
除了虚拟中断注入外,时钟中断虚拟化还要考虑一种特殊情况,即guest退出前设置了一个到期的timer,这时KVM需要维护该timer,以在timer到期时唤醒guest并为guest注入时钟中断。KVM注册了一个hrtimer来专门处理这种情况。hrtimer会判断guest的timer是否到期,如果就唤醒guest并执行中断注入操作。
时钟虚拟化的初始化操作分布在KVM启动过程中的多个节点。我们一个个来看。
a. 在内核启动过程中首先会注册系统时钟驱动( drivers/clocksource/arm_arch_timer.c),host和guest都会使用该驱动。驱动中会注册clock_event设备和clock_source设备。在该驱动中有一些与虚拟化相关的设置:
b. 第二个初始化操作是在KVM启动时kvm_arch_init->init_subsystems->kvm_timer_hyp_init函数。
430 int kvm_timer_hyp_init(void)
431 {
432 struct arch_timer_kvm_info *info;
433 int err;
434
435 info = arch_timer_get_kvm_info();
436 timecounter = &info->timecounter;
437
438 if (info->virtual_irq <= 0) {
439 kvm_err("kvm_arch_timer: invalid virtual timer IRQ: %d\n",
440 info->virtual_irq);
441 return -ENODEV;
442 }
443 host_vtimer_irq = info->virtual_irq;
444
445 host_vtimer_irq_flags = irq_get_trigger_type(host_vtimer_irq);
446 if (host_vtimer_irq_flags != IRQF_TRIGGER_HIGH &&
447 host_vtimer_irq_flags != IRQF_TRIGGER_LOW) {
448 kvm_err("Invalid trigger for IRQ%d, assuming level low\n",
449 host_vtimer_irq);
450 host_vtimer_irq_flags = IRQF_TRIGGER_LOW;
451 }
452
453 err = request_percpu_irq(host_vtimer_irq, kvm_arch_timer_handler,
454 "kvm guest timer", kvm_get_running_vcpus());
455 if (err) {
456 kvm_err("kvm_arch_timer: can't request interrupt %d (%d)\n",
457 host_vtimer_irq, err);
458 goto out;
459 }
460
461 err = __register_cpu_notifier(&kvm_timer_cpu_nb);
462 if (err) {
463 kvm_err("Cannot register timer CPU notifier\n");
464 goto out_free;
465 }
466
467 wqueue = create_singlethread_workqueue("kvm_arch_timer");
468 if (!wqueue) {
469 err = -ENOMEM;
470 goto out_free;
471 }
472
473 kvm_info("virtual timer IRQ%d\n", host_vtimer_irq);
474 on_each_cpu(kvm_timer_init_interrupt, NULL, 1);
475
476 goto out;
477 out_free:
478 free_percpu_irq(host_vtimer_irq, kvm_get_running_vcpus());
479 out:
480 return err;
481 }
435~443行,取出时钟驱动传递给KVM的arch_timer_kvm_info结构体,从里面取出虚拟中断和timercounter结构体,KVM会经常使用timercounter结构体来读取虚拟counter的值。
445~459行,取出虚拟counter并注册中断处理程序,这里注册的中断处理程序是host下的中断处理程序,而虚拟中断实际上是给guest使用的。因此自host下一般情况下不应该产生虚拟中断的。这里注册的中断处理程序什么也不做,直接返回IRQ_HANDED。
461~465行,这几行代码是跟CPU电源管理相关的。当CPU休眠和唤醒的时候执行timer的初始化和退出操作。
467~471行,申请一个工作队列,该工作队列执行中断注入的工作(部分情况下)。
c. 第三步初始化操作在qemu创建虚拟机时,kvm_dev_ioctl_create_vm–>kvm_create_vm–>kvm_timer_init函数。
540 void kvm_timer_init(struct kvm *kvm)
541 {
542 kvm->arch.timer.cntvoff = kvm_phys_timer_read();
543 }
这个函数比较简单,就是使用timecounter读取CPU的虚拟counter,由于物理机和虚拟机都使用虚拟counter来作为clock_source设备,因此两者之间有一个offset,在创建虚拟机时,这个offset就是物理机上的当前虚拟counter值。
第四步初始化在创建vCPU时,kvm_vm_ioctl_create_vcpu–>kvm_vcpu_init–>kvm_arch_vcpu_init–>kvm_timer_vcpu_init函数。
void kvm_timer_vcpu_init(struct kvm_vcpu *vcpu)
{
struct arch_timer_cpu *timer = &vcpu->arch.timer_cpu;
INIT_WORK(&timer->expired, kvm_timer_inject_irq_work);
hrtimer_init(&timer->timer, CLOCK_MONOTONIC, HRTIMER_MODE_ABS);
timer->timer.function = kvm_timer_expire;
}
该函数初始化一个工作队列timer->expired,工作队列函数为kvm_timer_inject_irq_work。当vcpu没有运行,但是在停止运行前设置了一个在未来执行的定时器。该工作队列用来处理这种情况,用来定时器到期时唤醒vcpu?
该函数初始化了一个hrtimer,执行函数为kvm_timer_expire。该定时器函数检查vcpu设置的时钟是否到期如果到期,则唤醒工作队列,执行唤醒vcpu的动作。
第五步初始化是在初始化vCPU时,kvm_arch_vcpu_ioctl_vcpu_init->kvm_reset_vcpu->kvm_timer_vcpu_reset函数中。
31 struct arch_timer_cpu {
33 u32 cntv_ctl; /* Saved/restored */
34 cycle_t cntv_cval; /* Saved/restored */
42 struct hrtimer timer;
45 struct work_struct expired;
48 bool armed;
51 struct kvm_irq_level irq;
54 bool active_cleared_last;
57 bool enabled;
}
333 int kvm_timer_vcpu_reset(struct kvm_vcpu *vcpu,
334 const struct kvm_irq_level *irq)
335 {
336 struct arch_timer_cpu *timer = &vcpu->arch.timer_cpu;
344 timer->irq.irq = irq->irq;
352 timer->cntv_ctl = 0;
353 kvm_timer_update_state(vcpu);
355 return 0;
356 }
每个vCPU结构体中都维护了一个与时钟虚拟化相关的结构体arch_timer_cpu中,该结构体保存了vCPU的虚拟timer的上下文(cntv_ctl,cntv_cval)。另外还保存有一些时钟虚拟化要用到的一些资源,比如前面提到的工作队列和高精度定时器,以及struct kvm_irq_level结构体,该结构体用来进行虚拟时钟中断注入的。
kvm_timer_vcpu_reset函数里面设置kvm_irq_level结构体的irq字段为27,其实27就是timer的虚拟中断的硬件中断号。该结构体本来是qemu模拟设备向guest注入中断时传递给kvm的结构体,但是虚拟timer中断是直接kvm模拟的,不需要qemu来参与,因此内核中使用维护了一个该结构体来作为中断注入的参数。
kvm_timer_vcpu_reset函数接下来把cntv_ctl设置为0,在初始化时时钟中断是没有打开的,因此这里设置的是初始值,在进入guest时该值会恢复到CPU的相关寄存器中。最后一行kvm_timer_update_state函数在这里没有做任何操作,因为timer还没有使能。
第六步初始化是第一次运行vCPU时,kvm_arch_vcpu_ioctl_run->kvm_vcpu_first_run_init->kvm_timer_enable函数中。
491 int kvm_timer_enable(struct kvm_vcpu *vcpu)
492 {
493 struct arch_timer_cpu *timer = &vcpu->arch.timer_cpu;
494 struct irq_desc *desc;
495 struct irq_data *data;
496 int phys_irq;
497 int ret;
498
499 if (timer->enabled)
500 return 0;
505 desc = irq_to_desc(host_vtimer_irq);
506 if (!desc) {
507 kvm_err("%s: no interrupt descriptor\n", __func__);
508 return -EINVAL;
509 }
510
511 data = irq_desc_get_irq_data(desc);
512 while (data->parent_data)
513 data = data->parent_data;
514
515 phys_irq = data->hwirq;
521 ret = kvm_vgic_map_phys_irq(vcpu, timer->irq.irq, phys_irq);
522 if (ret)
523 return ret;
534 if (timecounter && wqueue)
535 timer->enabled = 1;
536
537 return 0;
第505到515行,利用虚拟中断的中断号取出中断描述符,从而得到虚拟时钟中断的物理中断号(其实就是27号中断)。
第521行,调用vgic的映射函数,进行中断映射,在ARM64-KVM的中断虚拟化中,vgic会为虚拟机支持的每个中断维护一个vgic_irq结构体,该函数根据虚拟机的时钟中断(timer->irq.irq)取出该结构体,并设置该中断对应的实际物理中断为phys_irq。(实际上有些虚拟机的中断没有与实际物理中断对应,这是通过vgic_irq的hw字段来区分的,hw置1就说明有物理中断),具体中断虚拟化的原理和分析后面在专门的章节中分析。
到这里,时钟虚拟化的初始化过程就分析完了,做的事情很简单:1.从host的时钟驱动中取出虚拟中断号和虚拟counter取钱函数,初始化虚拟机的虚拟。2. 为时钟虚拟化初始化工作队列和高精度定时器。3. 计算虚拟机和物理机counter的offset。4. 执行虚拟机时钟中断的映射。
ARM64-KVM时钟虚拟化的工作过程主要是进入guest时的时钟中断注入以及guest退出时相关的时钟虚拟化的处理。第一次执行虚拟机时,时钟中断是关闭的,当虚拟机OS启动时,他们注册时钟驱动,从而开启时钟功能。当虚拟机退出时,KVM就要执行与时钟相关的虚拟化处理。我们从虚拟机退出开始分析。
与时钟虚拟化相关的虚拟机退出分两种情况,一种是虚拟机执行时,其时钟中断产生,这时CPU接收到时钟中断进入EL2模式,退出guest。第二种情况是,虚拟机由于其他原因退出(比如IO模拟或WFI/WFE指令模拟)。无论哪种情况,最终都会跳转到__guest_exit,执行guest退出操作,包括保存guest的上下文,恢复host的上下文。其中会保存timer的上下文。
保存timer的上下文为__guest_run–>__timer_save_state函数:
24 /* vcpu is already in the HYP VA space */
25 void __hyp_text __timer_save_state(struct kvm_vcpu *vcpu)
26 {
27 struct arch_timer_cpu *timer = &vcpu->arch.timer_cpu;
28 u64 val;
29
30 if (timer->enabled) {
31 timer->cntv_ctl = read_sysreg_el0(cntv_ctl);
32 timer->cntv_cval = read_sysreg_el0(cntv_cval);
33 }
34
35 /* Disable the virtual timer */
36 write_sysreg_el0(0, cntv_ctl);
37
38 /* Allow physical timer/counter access for the host */
39 val = read_sysreg(cnthctl_el2);
40 val |= CNTHCTL_EL1PCTEN | CNTHCTL_EL1PCEN;
41 write_sysreg(val, cnthctl_el2);
42
43 /* Clear cntvoff for the host */
44 write_sysreg(0, cntvoff_el2);
45 }
30~31行,把guest的虚拟机timer的两个寄存器的值保存在vCPU的arch_timer_cpu结构体中。
36行,禁止timer的虚拟中断,因为进入host后,使用的是物理中断,虚拟中断只要在进入guest后才使用,因此这里需要禁止。
38~41行,使能对物理timer寄存器的访问,进入guest时会禁止该访问。
44行,把cntvoff_el2清0。在物理机和虚拟机里面都是使用的虚拟counter寄存器,不同的是物理机里面虚拟counter和物理counter的值相同,而虚拟机里面则两者不同。两者之间的偏移是通过cnthctl_el2寄存器来设置的,进入物理环境后不需要差值了,因此设置该寄存器为0。
接下来guest退出过程中会调用kvm_timer_sync_hwstate->kvm_timer_update_state函数来同步timer状态。
191 static int kvm_timer_update_state(struct kvm_vcpu *vcpu)
192 {
193 struct arch_timer_cpu *timer = &vcpu->arch.timer_cpu;
201 if (!vgic_initialized(vcpu->kvm) || !timer->enabled)
202 return -ENODEV;
203
204 if (kvm_timer_should_fire(vcpu) != timer->irq.level)
205 kvm_timer_update_irq(vcpu, !timer->irq.level);
206
207 return 0;
208 }
156 bool kvm_timer_should_fire(struct kvm_vcpu *vcpu)
157 {
158 struct arch_timer_cpu *timer = &vcpu->arch.timer_cpu;
159 cycle_t cval, now;
160
161 if (!kvm_timer_irq_can_fire(vcpu))
162 return false;
163
164 cval = timer->cntv_cval;
165 now = kvm_phys_timer_read() - vcpu->kvm->arch.timer.cntvoff;
166
167 return cval <= now;
168 }
201行,错误检测,如果vgic没有初始化或者timer没有使能,则返回错误。
204行,kvm_timer_should_fire函数判断guest的timer中断是否应该产生,判断的方式很简单,判断guest的定时比较寄存器与conter寄存器的大小(之前有保存这些寄存器),如果counter超过了定时比较寄存器,则说明要产生定时中断。显然当guest定时器到期产生定时中断时,这里才会出现这种情况。
205行,如果需要产生中断,则调用 kvm_timer_update_irq函数向guest注入中断。注入中断过程涉及到中断虚拟化原理,这里不涉及。
如果是产生时钟中断导致guest退出,则时钟虚拟化没有其他两个处理,直接在下次进入guest时执行具体的中断注入动作。但是如果是WFI指令模拟退出,一般情况下执行WFI之类模拟代表guest要休眠一段时间,但是guest休眠之前可能设置了一个时钟中断来唤醒vCPU,这时需要处理这种情况,具体是启动一个定时器,当定时器到期时唤醒vCPU。KVM在模拟WFI指令时,会调用kvm_timer_schedul函数。
void kvm_timer_schedule(struct kvm_vcpu *vcpu)
{
struct arch_timer_cpu *timer = &vcpu->arch.timer_cpu;
BUG_ON(timer_is_armed(timer));
if (kvm_timer_should_fire(vcpu))
return;
if (!kvm_timer_irq_can_fire(vcpu))
return;
timer_arm(timer, kvm_timer_compute_delta(vcpu));
}
这个函数处理很简单,如果没有设置定时器、定时器已经到期,则直接返回。否则启动之前初始化过的高精度定时器,定时器到期时间则是guest中设置的时钟中断的到期时间。当定时器到期时,则执行定时器处理函数:
124 static enum hrtimer_restart kvm_timer_expire(struct hrtimer *hrt)
125 {
126 struct arch_timer_cpu *timer;
127 struct kvm_vcpu *vcpu;
128 u64 ns;
129
130 timer = container_of(hrt, struct arch_timer_cpu, timer);
131 vcpu = container_of(timer, struct kvm_vcpu, arch.timer_cpu);
138 ns = kvm_timer_compute_delta(vcpu);
139 if (unlikely(ns)) {
140 hrtimer_forward_now(hrt, ns_to_ktime(ns));
141 return HRTIMER_RESTART;
142 }
144 queue_work(wqueue, &timer->expired);
145 return HRTIMER_NORESTART;
146 }
该函数也很简单,判断定时器是真的到期了,则执行之前初始化过的工作队列,该工作队列执行唤醒vCPU的动作:
88 static void kvm_timer_inject_irq_work(struct work_struct *work)
89 {
90 struct kvm_vcpu *vcpu;
91
92 vcpu = container_of(work, struct kvm_vcpu, arch.timer_cpu.expired);
93 vcpu->arch.timer_cpu.armed = false;
95 WARN_ON(!kvm_timer_should_fire(vcpu));
101 kvm_vcpu_kick(vcpu);
102 }
工作队列函数就是执行kvm_vcpu_kick函数唤醒vCPU。
定时器虚拟化KVM处理的最后一步就是在进入guset时,同步定时中断和恢复timer的上下文到guest。恢复上下文的函数我们就不分析了,很简单,把保存在vCPU中的定时器恢复到物理CPU上就可以了。同步定时中断的函数为kvm_timer_flush_hwstate。
void kvm_timer_flush_hwstate(struct kvm_vcpu *vcpu)
{
struct arch_timer_cpu *timer = &vcpu->arch.timer_cpu;
bool phys_active;
int ret;
if (kvm_timer_update_state(vcpu))
return;
phys_active = timer->irq.level ||
kvm_vgic_map_is_active(vcpu, timer->irq.irq);
if (timer->active_cleared_last && !phys_active)
return;
ret = irq_set_irqchip_state(host_vtimer_irq,
IRQCHIP_STATE_ACTIVE,
phys_active);
WARN_ON(ret);
timer->active_cleared_last = !phys_active;
}
kvm_timer_update_state函数把timer中断同步到vgic。剩下的代码是设置物理机下时钟虚拟中断的状态,这样处理的原因是让guest处理时钟中断时减少不必要的退出。
到这里时钟虚拟化相关的代码就分析完了,当下次进入到guest时,guest就执行时钟中断处理函数了。可以看出KVM时钟虚拟化主要处理两种情况,1. guest的定时中断产生时,向guest注入时钟中断。2. 当guest执行WFI休眠时,通过定时器维护其设置的时钟到期时间并注入中断。