0%

Linux 虚拟化 KVM-Qemu 分析(7)之 timer 虚拟化.md

背景

  • Read the fucking source code! –By 鲁迅
  • A picture is worth a thousand words. –By 高尔基

说明:

  1. KVM 版本:5.9.1
  2. QEMU 版本:5.0.0
  3. 工具:Source Insight 3.5, Visio
  4. 文章同步在博客园:https://www.cnblogs.com/LoyenWang/

1. 概述

先从操作系统的角度来看一下 timer 的作用吧:

image-20240416110453083

通过 timer 的中断,OS 实现的功能包括但不局限于上图:

  • 定时器的维护,包括用户态和内核态,当指定时间段过去后触发事件操作,比如 IO 操作注册的超时定时器等;
  • 更新系统的运行时间、wall time 等,此外还保存当前的时间和日期,以便能通过time()等接口返回给用户程序,内核中也可以利用其作为文件和网络包的时间戳;
  • 调度器在调度任务分配给 CPU 时,也会去对 task 的运行时间进行统计计算,比如 CFS 调度,Round-Robin 调度等;
  • 资源使用统计,比如系统负载的记录等,此外用户使用 top 命令也能进行查看;

timer 就像是系统的脉搏,重要性不言而喻。ARMv8 架构处理器提供了一个 Generic Timer,与 GIC 类似,Generic Timer 在硬件上也支持了虚拟化,减少了软件模拟带来的 overhead。

本文将围绕着 ARMv8 的 timer 虚拟化来展开。

2. ARMv8 Timer 虚拟化

2.1 Generic Timer

看一下 ARMv8 架构下的 CPU 内部图:

image-20240416110458595

  • Generic Timer提供了一个系统计数器,用于测量真实时间的消逝;
  • Generic Timer支持虚拟计数器,用于测量虚拟的时间消逝,一个虚拟计数器对应一个虚拟机;
  • Timer可以在特定的时间消逝后触发事件,可以设置成count-up计数或者count-down计数;

来看一下Generic Timer的简图:

image-20240416110502169

或者这个:

image-20240416110505795

  • System Counter位于Always-on电源域,以固定频率进行系统计数的增加,System Counter的值会广播给系统中的所有核,所有核也能有一个共同的基准了,System Counter的频率范围为 1-50MHZ,系统计数值的位宽在 56-64bit 之间;
  • 每个核有一组 timer,这些 timer 都是一些比较器,与System Counter广播过来的系统计数值进行比较,软件可以配置固定时间消逝后触发中断或者触发事件;
  • 每个核提供的 timer 包括:1)EL1 Physical timer;2)EL1 Virtual timer;此外还有在 EL2 和 EL3 下提供的 timer,具体取决于 ARMv8 的版本;
  • 有两种方式可以配置和使用一个 timer:1)CVAL(comparatoer)寄存器,通过设置比较器的值,当System Count >= CVAL时满足触发条件;2)TVAL寄存器,设置TVAL寄存器值后,比较器的值CVAL = TVAL + System Counter,当System Count >= CVAL时满足触发条件,TVAL是一个有符号数,当递减到 0 时还会继续递减,因此可以记录 timer 是在多久之前触发的;
  • timer 的中断是私有中断PPI,其中EL1 Physical Timer的中断号为 30,EL1 Virtual Timer的中断号为 27;
  • timer 可以配置成触发事件产生,当 CPU 通过WFE进入低功耗状态时,除了使用SEV指令唤醒外,还可以通过Generic Timer产生的事件流来唤醒;

2.2 虚拟化支持

Generic Timer的虚拟化如下图:

image-20240416110509533

  • 虚拟的 timer,同样也有一个 count 值,计算关系:Virtual Count = Physical Count - <offset>,其中 offset 的值放置在CNTVOFF寄存器中,CNTPCT/CNTVCT分别用于记录当前物理 / 虚拟的 count 值;
  • 如果 EL2 没有实现,则将 offset 设置为 0,,物理的计数器和虚拟的计数器值相等;
  • Physical Timer直接与System counter进行比较,Virtual TimerPhysical Timer的基础上再减去一个偏移;
  • Hypervisor 负责为当前调度运行的 vCPU 指定对应的偏移,这种方式使得虚拟时间只会覆盖 vCPU 实际运行的那部分时间;

示例如下:

image-20240416110513314

  • 6ms 的时间段里,每个 vCPU 运行 3ms,Hypervisor 可以使用偏移寄存器来将 vCPU 的时间调整为其实际的运行时间;

3. 流程分析

3.1 初始化

先简单看一下数据结构吧:

image-20240416110517205

  • 在 ARMv8 虚拟化中,使用struct arch_timer_cpu来描述Generic Timer,从结构体中也能很清晰的看到层次结构,创建 vcpu 时,需要去初始化 vcpu 架构相关的字段,其中就包含了 timer;
  • struct arch_timer_cpu包含了两个 timer,分别对应物理 timer 和虚拟 timer,此外还有一个高精度定时器,用于 Guest 处在非运行时的计时工作;
  • struct arch_timer_context用于描述一个 timer 需要的内容,包括了几个字段用于存储寄存器的值,另外还描述了中断相关的信息;

初始化分为两部分:

  1. 架构相关的初始化,针对所有的 CPU,在 kvm 初始化时设置:

  • kvm_timer_hyp_init函数完成相应的初始化工作;
  • arch_timer_get_kvm_info从 Host Timer 驱动中去获取信息,主要包括了虚拟中断号和物理中断号,以及 timecounter 信息等;
  • vtimer 中断设置包括:判断中断的触发方式(只支持电平触发),注册中断处理函数kvm_arch_timer_handler,设置中断到 vcpu 的 affinity 等;
  • ptimer 中断设置与 vtimer 中断设置一样,同时它的中断处理函数也是kvm_arch_timer_handler,该处理函数也比较简单,最终会调用kvm_vgic_inject_irq函数来完成虚拟中断注入给 vcpu;
  • cpuhp_setup_state用来设置 CPU 热插拔时 timer 的响应处理,而在kvm_timer_starting_cpu/kvm_timer_dying_cpu两个函数中实现的操作就是中断的打开和关闭,仅此而已;
  1. vcpu 相关的初始化,在创建 vcpu 时进行初始化设置:

image-20240416110543191

  • 针对 vcpu 的 timer 相关初始化比较简单,回到上边那张数据结构图看一眼就明白了,所有的初始化工作都围绕着struct arch_timer_cpu结构体;
  • vcpu_timer:用于获取 vcpu 包含的struct arch_timer_cpu结构;
  • vcpu_vtimer/vcpu_ptimer:用于获取struct arch_timer_cpu结构体中的struct arch_timer_context,分别对应 vtimer 和 ptimer;
  • update_vtimer_cntvoff:用于更新 vtimer 中的 cntvoff 值,读取物理 timer 的 count 值,更新 VM 中所有 vcpu 的 cntvoff 值;
  • hrtimer_init:用于初始化高精度定时器,包含有三个,struct arch_timer_cpu结构中有一个bg_timer,vtimer 和 ptimer 所对应的struct arch_timer_context中分别对应一个;
  • kvm_bg_timer_expirebg_timer的到期执行函数,当需要调用kvm_vcpu_block让 vcpu 睡眠时,需要先启动bg_timerbg_timer到期时再将 vcpu 唤醒;
  • kvm_hrtimer_expire:vtimer 和 ptimer 的到期执行函数,最终通过调用kvm_timer_update_irq来向 vcpu 注入中断;

3.2 用户层访问

可以从用户态对 vtimer 进行读写操作,比如 Qemu 中,流程如下:

image-20240416110538838

  • 用户态创建完 vcpu 后,可以通过 vcpu 的文件描述符来进行寄存器的读写操作;
  • 以 ARM 为例,ioctl 通过KVM_SET_ONE_REG/KVM_GET_ONE_REG将最终触发寄存器的读写;
  • 如果操作的是 timer 的相关寄存器,则通过kvm_arm_timer_set_regkvm_arm_timer_get_reg来完成;
  • 读写的寄存器包括虚拟 timer 的 CTL/CVAL,以及物理 timer 的 CTL/CVAL 等;

3.3 Guest 访问

Guest 对 Timer 的访问,涉及到系统寄存器的读写,将触发异常并 Trap 到 Hyp 进行处理,流程如下:

image-20240416110534774

  • Guest OS 访问系统寄存器时,Trap 到 Hypervisor 进行处理;
  • Hypervisor 对异常退出进行处理,如果发现是访问系统寄存器造成的异常,则调用kvm_handle_sys_reg来处理;
  • kvm_handle_sys_reg:调用emulate_sys_reg来对系统寄存器进行模拟,在该函数中首先会查找访问的是哪一个寄存器,然后再去调用相应的回调函数;
  • kvm 中维护了struct sys_reg_desc sys_reg_descs[]系统寄存器的描述表,其中struct sys_reg_desc结构体中包含了对该寄存器操作的函数指针,用于指向最终的操作函数,比如针对 Timer 的kvm_arm_timer_write_sysreg/kvm_arm_timer_read_sysreg读写操作函数;
  • Timer 的读写操作函数,主要在kvm_arm_timer_read/kvm_arm_timer_write中完成,实现的功能就是根据物理的 count 值和 offset 来计算等;

timer 的虚拟化还是比较简单,就此打住了。

PS:

按计划,接下里该写 IO 虚拟化了,然后紧接着 Qemu 的源码相关分析。不过,在写 IO 虚拟化之前,我会先去讲一下 PCIe 的驱动框架,甚至可能还会去研究一下网络,who knows,反正这些也都是 IO 相关。
Any way,I will be back soon!

参考

《AArch64 Programmer's Guides Generic Timer》
《Arm Architecture Reference Manual》