ARM64-KVM时钟虚拟化原理分析

基本介绍

        本文介绍ARM64平台KVM的时钟虚拟化的原理和实现。ARM64的KVM时钟虚拟化基本是在内核层的KVM实现的,利用ARM64提供的硬件时钟资源。qemu层只是在必要的(比如迁移)时候会调用ioctl设置虚拟化机时钟的寄存器。

ARM64的硬件时钟介绍

        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等。两个比较定时器用来设置什么时候产生定时中断,两个比较寄存器任意使用其中一个就可以产生定时中断。

ARM64-KVM的时钟虚拟化基本原理

        时钟虚拟化要为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并执行中断注入操作。

ARM64-KVM时钟虚拟化实现分析

ARM64-KVM时钟虚拟化的初始化

        时钟虚拟化的初始化操作分布在KVM启动过程中的多个节点。我们一个个来看。
        a. 在内核启动过程中首先会注册系统时钟驱动( drivers/clocksource/arm_arch_timer.c),host和guest都会使用该驱动。驱动中会注册clock_event设备和clock_source设备。在该驱动中有一些与虚拟化相关的设置:

  1. host驱动使用的是物理timer,guest的是虚拟timer。
  2. guest和host的clock_source设备都使用的是虚拟conter。
  3. 该驱动初始化了一个arch_timer_kvm_info结构供KVM使用,该结构体记录了虚拟中断的中断号和读取虚拟conter的timerconter结构。

        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时钟虚拟化的工作过程

        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休眠时,通过定时器维护其设置的时钟到期时间并注入中断。

你可能感兴趣的:(虚拟化技术)