天天看点

【原创】Linux虚拟化KVM-Qemu分析(七)之timer虚拟化

<code>Read the fucking source code!</code> --By 鲁迅

<code>A picture is worth a thousand words.</code> --By 高尔基

说明:

KVM版本:5.9.1

QEMU版本:5.0.0

工具:Source Insight 3.5, Visio

文章同步在博客园:<code>https://www.cnblogs.com/LoyenWang/</code>

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

【原创】Linux虚拟化KVM-Qemu分析(七)之timer虚拟化

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

定时器的维护,包括用户态和内核态,当指定时间段过去后触发事件操作,比如IO操作注册的超时定时器等;

更新系统的运行时间、wall time等,此外还保存当前的时间和日期,以便能通过<code>time()</code>等接口返回给用户程序,内核中也可以利用其作为文件和网络包的时间戳;

调度器在调度任务分配给CPU时,也会去对task的运行时间进行统计计算,比如CFS调度,Round-Robin调度等;

资源使用统计,比如系统负载的记录等,此外用户使用top命令也能进行查看;

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

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

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

【原创】Linux虚拟化KVM-Qemu分析(七)之timer虚拟化

<code>Generic Timer</code>提供了一个系统计数器,用于测量真实时间的消逝;

<code>Generic Timer</code>支持虚拟计数器,用于测量虚拟的时间消逝,一个虚拟计数器对应一个虚拟机;

<code>Timer</code>可以在特定的时间消逝后触发事件,可以设置成<code>count-up</code>计数或者<code>count-down</code>计数;

来看一下<code>Generic Timer</code>的简图:

【原创】Linux虚拟化KVM-Qemu分析(七)之timer虚拟化

或者这个:

【原创】Linux虚拟化KVM-Qemu分析(七)之timer虚拟化

<code>System Counter</code>位于<code>Always-on</code>电源域,以固定频率进行系统计数的增加,<code>System Counter</code>的值会广播给系统中的所有核,所有核也能有一个共同的基准了,<code>System Counter</code>的频率范围为1-50MHZ,系统计数值的位宽在56-64bit之间;

每个核有一组timer,这些timer都是一些比较器,与<code>System Counter</code>广播过来的系统计数值进行比较,软件可以配置固定时间消逝后触发中断或者触发事件;

每个核提供的timer包括:1)<code>EL1 Physical timer</code>;2)<code>EL1 Virtual timer</code>;此外还有在EL2和EL3下提供的timer,具体取决于ARMv8的版本;

有两种方式可以配置和使用一个timer:1)<code>CVAL(comparatoer)</code>寄存器,通过设置比较器的值,当<code>System Count &gt;= CVAL</code>时满足触发条件;2)<code>TVAL</code>寄存器,设置<code>TVAL</code>寄存器值后,比较器的值<code>CVAL = TVAL + System Counter</code>,当<code>System Count &gt;= CVAL</code>时满足触发条件,<code>TVAL</code>是一个有符号数,当递减到0时还会继续递减,因此可以记录timer是在多久之前触发的;

timer的中断是私有中断<code>PPI</code>,其中<code>EL1 Physical Timer</code>的中断号为30,<code>EL1 Virtual Timer</code>的中断号为27;

timer可以配置成触发事件产生,当CPU通过<code>WFE</code>进入低功耗状态时,除了使用<code>SEV</code>指令唤醒外,还可以通过<code>Generic Timer</code>产生的事件流来唤醒;

<code>Generic Timer</code>的虚拟化如下图:

【原创】Linux虚拟化KVM-Qemu分析(七)之timer虚拟化

虚拟的timer,同样也有一个count值,计算关系:<code>Virtual Count = Physical Count - &lt;offset&gt;</code>,其中offset的值放置在<code>CNTVOFF</code>寄存器中,<code>CNTPCT/CNTVCT</code>分别用于记录当前物理/虚拟的count值;

如果EL2没有实现,则将offset设置为0,,物理的计数器和虚拟的计数器值相等;

<code>Physical Timer</code>直接与<code>System counter</code>进行比较,<code>Virtual Timer</code>在<code>Physical Timer</code>的基础上再减去一个偏移;

Hypervisor负责为当前调度运行的vCPU指定对应的偏移,这种方式使得虚拟时间只会覆盖vCPU实际运行的那部分时间;

示例如下:

【原创】Linux虚拟化KVM-Qemu分析(七)之timer虚拟化

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

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

【原创】Linux虚拟化KVM-Qemu分析(七)之timer虚拟化

在ARMv8虚拟化中,使用<code>struct arch_timer_cpu</code>来描述<code>Generic Timer</code>,从结构体中也能很清晰的看到层次结构,创建vcpu时,需要去初始化vcpu架构相关的字段,其中就包含了timer;

<code>struct arch_timer_cpu</code>包含了两个timer,分别对应物理timer和虚拟timer,此外还有一个高精度定时器,用于Guest处在非运行时的计时工作;

<code>struct arch_timer_context</code>用于描述一个timer需要的内容,包括了几个字段用于存储寄存器的值,另外还描述了中断相关的信息;

初始化分为两部分:

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

【原创】Linux虚拟化KVM-Qemu分析(七)之timer虚拟化

<code>kvm_timer_hyp_init</code>函数完成相应的初始化工作;

<code>arch_timer_get_kvm_info</code>从Host Timer驱动中去获取信息,主要包括了虚拟中断号和物理中断号,以及timecounter信息等;

vtimer中断设置包括:判断中断的触发方式(只支持电平触发),注册中断处理函数<code>kvm_arch_timer_handler</code>,设置中断到vcpu的affinity等;

ptimer中断设置与vtimer中断设置一样,同时它的中断处理函数也是<code>kvm_arch_timer_handler</code>,该处理函数也比较简单,最终会调用<code>kvm_vgic_inject_irq</code>函数来完成虚拟中断注入给vcpu;

<code>cpuhp_setup_state</code>用来设置CPU热插拔时timer的响应处理,而在<code>kvm_timer_starting_cpu/kvm_timer_dying_cpu</code>两个函数中实现的操作就是中断的打开和关闭,仅此而已;

vcpu相关的初始化,在创建vcpu时进行初始化设置:

【原创】Linux虚拟化KVM-Qemu分析(七)之timer虚拟化

针对vcpu的timer相关初始化比较简单,回到上边那张数据结构图看一眼就明白了,所有的初始化工作都围绕着<code>struct arch_timer_cpu</code>结构体;

<code>vcpu_timer</code>:用于获取vcpu包含的<code>struct arch_timer_cpu</code>结构;

<code>vcpu_vtimer/vcpu_ptimer</code>:用于获取<code>struct arch_timer_cpu</code>结构体中的<code>struct arch_timer_context</code>,分别对应vtimer和ptimer;

<code>update_vtimer_cntvoff</code>:用于更新vtimer中的cntvoff值,读取物理timer的count值,更新VM中所有vcpu的cntvoff值;

<code>hrtimer_init</code>:用于初始化高精度定时器,包含有三个,<code>struct arch_timer_cpu</code>结构中有一个<code>bg_timer</code>,vtimer和ptimer所对应的<code>struct arch_timer_context</code>中分别对应一个;

<code>kvm_bg_timer_expire</code>:<code>bg_timer</code>的到期执行函数,当需要调用<code>kvm_vcpu_block</code>让vcpu睡眠时,需要先启动<code>bg_timer</code>,<code>bg_timer</code>到期时再将vcpu唤醒;

<code>kvm_hrtimer_expire</code>:vtimer和ptimer的到期执行函数,最终通过调用<code>kvm_timer_update_irq</code>来向vcpu注入中断;

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

【原创】Linux虚拟化KVM-Qemu分析(七)之timer虚拟化

用户态创建完vcpu后,可以通过vcpu的文件描述符来进行寄存器的读写操作;

以ARM为例,ioctl通过<code>KVM_SET_ONE_REG/KVM_GET_ONE_REG</code>将最终触发寄存器的读写;

如果操作的是timer的相关寄存器,则通过<code>kvm_arm_timer_set_reg</code>和<code>kvm_arm_timer_get_reg</code>来完成;

读写的寄存器包括虚拟timer的CTL/CVAL,以及物理timer的CTL/CVAL等;

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

【原创】Linux虚拟化KVM-Qemu分析(七)之timer虚拟化

Guest OS访问系统寄存器时,Trap到Hypervisor进行处理;

Hypervisor对异常退出进行处理,如果发现是访问系统寄存器造成的异常,则调用<code>kvm_handle_sys_reg</code>来处理;

<code>kvm_handle_sys_reg</code>:调用<code>emulate_sys_reg</code>来对系统寄存器进行模拟,在该函数中首先会查找访问的是哪一个寄存器,然后再去调用相应的回调函数;

kvm中维护了<code>struct sys_reg_desc sys_reg_descs[]</code>系统寄存器的描述表,其中<code>struct sys_reg_desc</code>结构体中包含了对该寄存器操作的函数指针,用于指向最终的操作函数,比如针对Timer的<code>kvm_arm_timer_write_sysreg/kvm_arm_timer_read_sysreg</code>读写操作函数;

Timer的读写操作函数,主要在<code>kvm_arm_timer_read/kvm_arm_timer_write</code>中完成,实现的功能就是根据物理的count值和offset来计算等;

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

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

<code>Any way,I will be back soon!</code>

<code>《AArch64 Programmer's Guides Generic Timer》</code>

<code>《Arm Architecture Reference Manual》</code>

欢迎关注个人公众号,不定期更新内核相关技术文章

【原创】Linux虚拟化KVM-Qemu分析(七)之timer虚拟化

作者:LoyenWang

出处:https://www.cnblogs.com/LoyenWang/

公众号:<b>LoyenWang</b>

版权:本文版权归作者和博客园共有

转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任