前言: kvm-clock,tsc,hpet,acpi_pm,pit,rtc。。。这些词看着都晕了@@ 虚拟化场景下,容作者在这里一一道来。 分析: 1,Linux clocksource
以Linux为例,查看操作系统的clocksource: 在/sys/devices/system/clocksource/clocksource0目录下; available_clocksource是当前所有可用的clocksource; current_clocksource是当前正在使用的clocksource。 所以,先不管这些名词是什么东西,怎么实现的,起码kvm-clock、tsc、hpet、acpi_pm是同一类的东西;并且,在Linux上,统一管理它们,并叫“clocksource”。它们提供了timer的能力,给操作系统使用。举例来说,kernel中的调度,就需要使用timer来支持。 因为windows不开源,也不知道它的源代码,最多就是根据外部的一些现象来猜测它的逻辑。具体的代码分析都基于Linux。 2,clocksource management 主要逻辑代码在linux-4.0.4/kernel/time/clocksource.c中实现:
clocksource实现了一个基本框架,提供了clocksource_register给具体的driver使用。 其中关键函数clocksource_enqueue:
可见,所有的clocksource是根据rating参数大小被组织到了一个list中。 linux默认使用rating最高的clocksource,或者用户修改过clocksource,通过clocksource_select函数生效。 在linux-4.0.4/arch/x86/kernel/kvmclock.c:
声明了kvm-clock的rating是400; 同理,在linux-4.0.4/arch/x86/kernel/tsc.c中,可以看到tsc的rating是300; linux-4.0.4/arch/x86/kernel/hpet.c中,hpet的rating是250; linux-4.0.4/drivers/clocksource/acpi_pm.c中,acpi_pm的rating是200; linux-4.0.4/drivers/clocksource/i8253.c中,pit的rating是110。 综上,Linux大致实现了clocksource的管理框架,各个timer实现自己的drvier,并注册到clocksource中。根据rating来看timer的性能:kvmclock>tsc>hpet>acpi_pm>pit。 3,kvmclock kvmclock比较特殊,是在kvm虚拟化的时候使用的,物理机上并没有---这也是和其他的timer很大的一个差别。 在linux-4.0.4/arch/x86/kernel/kvmclock.c中,kvmclock实现的关键函数:
计算出来pvit的物理地址(这个“物理地址”,其实是Guest Physical Address,即GPA),写MSR寄存器,通过msr_kvm_system_time(MSR_KVM_SYSTEM_TIME),告诉Host它的pvit地址。 在Host中: linux-4.0.4/arch/x86/kvm/x86.c文件中kvm_set_msr_common函数:
这里说一下,Guest中使用了wrmsr指令,Host会感知,然后进入到kvm_set_msr_common中处理。可见,通过kvm_gfn_to_hva_cache_init函数,会把传过来的地址参数data记录到pv_time中。这样子就可以通过pv_time来直接修改Guest中的pvit。 因为Host和Guest,使用的是相同的tsc,这里还需要说明一个问题: 参考linux-4.0.4/arch/x86/kvm/x86.c文件中kvm_guest_time_update函数: 关于Guest中的时间的计算,pv time中有两个重要参数:tsc_timestamp和system_time Guest Sytem Time = Guest Sytem Time + offset; 为什么要这么麻烦的计算? 因为热迁移。两台Host的TSC不一样,如果Dst Host的TSC比Src Host的TSC小,那么可能会让Windows蓝屏或者linux panic。 如果Dst Host的TSC比Src Host的TSC大,那么在Guest中看到tsc瞬间跳变。所以需要计算offset来调整。 另外,在Guest中,还需要做一次计算delta: 在linux-4.0.4/arch/x86/include/asm/pvclock.h文件中的__pvclock_read_cycles函数中: 计算Host中读取到Tsc和Guest中读取到的Tsc的差值,再计算出来delta,最后再计算出来Guest中的“kvmclock”。这里把Host和Guest中前后两次的Tsc微小差值都计算进去了,可见精度确实很高。 kvmclock是kvm虚拟化中rating最高的timer;当然,它的计算也真的很麻烦。 4,tsc 如果Guest中使用rdtsc指令,则会被Host拦截,Host中处理后返回给Guest: linux-4.0.4/arch/x86/kvm/emulate.c中:
继续调用,会到linux-4.0.4/arch/x86/kvm/vmx.c中:
先读取出来Host的Tsc,在加上offset。offset的原理也是为了防止热迁移tsc变小。 这里再说一下tsc的timer。因为tsc只是一个单调递增的寄存器,本身不能产生timer的irq。设置了tsc之后,apic会产生timer的irq。 5, hpet hpet是纯粹的qemu在用户态模拟出来的。 代码qemu-2.8.0-rc4/hw/timer/hpet.c
qemu的设备虚拟化逻辑。注意,是为hpet设备注册了callback函数hpet_timer: 前面是计算下次超时的时间,最后一行很关键,向Guest里inject irq了。 所以,hpet的逻辑就很清晰了:qemu模拟了hpet device,并在用户态周期性的inject irq,在Guest中就觉得是一个timer了。 6, pit 代码实现在:linux-4.0.4/arch/x86/kvm/i8254.c中:
关键代码分析,Host为Guest的pit创建了一个内核线程,名称就是“kvm-pit/PID”。所以,启动一个qemu虚拟机之后,ps找到qemu的pid,然后就能看到一个对应的内核线程。稍微说一下,这个线程稍微特殊一点,不是一个常规定义的routine函数,是基于kernel worker机制。
继续分析,当Guest中激活了PIT之后,Host中调用create_pit_timer函数创建一个hr timer。hr timer会在interval后调用callback函数---pit_timer_fn。 pit_timer_fn就是把一个work加入到worker queue中,刚刚创建的kvm-pit线程就可以执行了。kvm-pit真正执行的,就是pit_do_work函数。
好吧,又向Guest中inject irq了。 这里多说一句哈,在Guest Linux上,虽然创建了kvm-pit线程,但是却没有跑;在Guest windows上,kvm-pit线程周期性的执行work。
后记:
总结上述timer的逻辑,或使用不同的硬件,或使用软件,或在内核态,或在用户态,都是周期性地向Guest中inject irq。
KVM虚拟化CPU和管理内存,qemu虚拟化设备,加上上述的timer,还有就是apic,那么龙珠就集齐了,可以召唤神龙了~