kvmclock
Ref:
https://zhuanlan.zhihu.com/p/665543594
【kvmclock的由来】
在x86平台,系统可以通过rdtsc来获取TSC值,然后再根据TSC的频率就可以计算出系统从上电开机到现在经历的时间(ns值)。
在虚拟化平台,虚拟机的tsc初始值是由VMM管理器设置的。因为涉及到不同平台、不同cpu型号的模拟以及虚拟机的热迁移等特性,虚拟机的tsc值与Host的tsc值并不相同。比如热迁移场景,两台Host的TSC不一样,如果Dst Host的TSC比Src Host的TSC小,那么可能会让Windows蓝屏或者linux panic。 如果Dst Host的TSC比Src Host的TSC大,那么在Guest中看到tsc瞬间跳变。
kvmclock即设计来解决这些问题。虚拟机想要通过TSC值来得到一个系统时间,最好还要告诉其一个可比较的值,即某个TSC值对应一个特定的时间。假设这个TSC常量值是tsc0 , 对应的时间值是t0 纳秒,TSC时钟频率为Freq,这样虚拟机只要把指令rdtsc得到TSC值tsc1与它进行对比,就能得到具体的时间,公式为
t0+(tsc1-tsc0) * Freq * 109 。是的,kvmclock就是基于TSC来设计的。
【kvmclock的实现原理】
从上面的由来,可以看出,虚拟机需要从主机端得到CPU的TSC时钟频率,以及一个可比较的相对时间。目前kvm的实现是:虚拟机内核会在内存中分配一个结构体来存放这些信息值;为了访问过程减少锁的使用,每个vCPU都有一个对应的结构体,这个结构体定义如下:
struct pvclock_vcpu_time_info {
u32 version; //版本号,每更新一次,就加2
u32 pad0;
u64 tsc_timestamp;//参考时间对应的tsc值
u64 system_time;//参考时间对应的ns值
u32 tsc_to_system_mul;//由TSC时钟频率计算而来,用于和tsc_shift一起将tsc相对值转为了时间ns
s8 tsc_shift;
u8 flags;
u8 pad[2];
}
由于内核中,tsc值除以TSC时钟频率得出时间的这个操作太过频繁,为了提升效率,内核把相对复杂的浮点除法转化为较为高效的整数位移和整数乘法。因此,可以看到这个结构体并没有直接存放TSC时钟频率,而是存放一个位移值tsc_shift和一个缩放乘数值tsc_to_system_mul,具体的运算细节后面再讲。
有了这个结构体,虚拟机内核需要把这个结构体地址告诉Host,建立一个共享映射,由host来填写,guest来读取。KVM设计了一个MSR寄存器MSR_KVM_SYSTEM_TIME_NEW,地址是0x4b564d01,来负责把pvclock_vcpu_time_info结构体的物理地址通知给host
【kvmclock在guest中的初始化】
虚拟机内核启动时,内核会尝试检测虚拟化运行环境是KVM,还是HyperV,调用栈如下:
start_kernel [init\main.c]
——》setup_arch [arch\x86\kernel\setup.c]
——》——》init_hypervisor_platform [arch\x86\kernel\cpu\hypervisor.c]
——》——》——》detect_hypervisor_vendor {通过遍历各个虚拟化环境检测函数}
——》——》——》——》(*p)->detect() {对于KVM来说,调用是函数kvm_detect[arch\x86\kernel\kvm.c]
当detect_hypervisor_vendor返回虚拟化环境是KVM后,接下来就会拷贝kvm的一些初始化函数地址,并执行其中的init_platform,进而调用kvmclock的初始化函数,代码如下:
void __init init_hypervisor_platform(void)
{
h = detect_hypervisor_vendor(); //检测
copy_array(&h->init, &x86_init.hyper, sizeof(h->init));
x86_init.hyper.init_platform(); //实际调用的是kvm_init_platform[arch\x86\kernel\kvm.c]
}
//file:arch\x86\kernel\kvm.c
static void __init kvm_init_platform(void)
{
kvmclock_init();
x86_platform.apic_post_init = kvm_apic_init;
}
接下来重点看下kvmclock_init函数,这个函数位于文件arch\x86\kernel\kvmclock.c。以下列出重要的几行代码。可以看出,它先获取本CPU的时间信息页内存的物理地址,然后通过写MSR告诉Host,让Host在这个内存地址填写详细的时间偏移量信息,最后注册guest内核启动后的第一个时钟源kvmclock:
void __init kvmclock_init(void)
{
......
if (kvm_para_has_feature(KVM_FEATURE_CLOCKSOURCE2)) {
msr_kvm_system_time = MSR_KVM_SYSTEM_TIME_NEW;//设置kvmclock对应的MSR寄存器地址
msr_kvm_wall_clock = MSR_KVM_WALL_CLOCK_NEW;
}
......
this_cpu_write(hv_clock_per_cpu, &hv_clock_boot[0]);//将第1个时间信息页项分配给CPU0
kvm_register_clock("primary cpu clock");//CPU0从主机端获取时间信息页
......
#ifdef CONFIG_X86_LOCAL_APIC
x86_cpuinit.early_percpu_clock_init = kvm_setup_secondary_clock;//指定其他CPU online时调用kvm_register_clock从Host主机端获得时间信息
#endif
......
clocksource_register_hz(&kvm_clock, NSEC_PER_SEC);//注册时钟源
pv_info.name = "KVM";
}
【时间乘数的计算和应用方法】
【kvmclock在host中的实现】
host在截获guest对msr寄存器MSR_KVM_SYSTEM_TIME_NEW的写动作后,会执行模拟操作:
kvm_set_msr_common [arch/x86/kvm/x86.c]
case MSR_KVM_SYSTEM_TIME_NEW:
case MSR_KVM_SYSTEM_TIME: {
struct kvm_arch *ka = &vcpu->kvm->arch;
if (vcpu->vcpu_id == 0 && !msr_info->host_initiated) {
bool tmp = (msr == MSR_KVM_SYSTEM_TIME);
if (ka->boot_vcpu_runs_old_kvmclock != tmp)
kvm_make_request(KVM_REQ_MASTERCLOCK_UPDATE, vcpu);
ka->boot_vcpu_runs_old_kvmclock = tmp;
}
vcpu->arch.time = data;
kvm_make_request(KVM_REQ_GLOBAL_CLOCK_UPDATE, vcpu);
/* we verify if the enable bit is set... */
vcpu->arch.pv_time_enabled = false;
if (!(data & 1))
break;
if (!kvm_gfn_to_hva_cache_init(vcpu->kvm,
&vcpu->arch.pv_time, data & ~1ULL,
sizeof(struct pvclock_vcpu_time_info)))
vcpu->arch.pv_time_enabled = true;
break;
}
通过kvm_gfn_to_hva_cache_init函数,会把传过来的地址参数data记录到pv_time中。这样子就可以通过pv_time来直接修改Guest中的pvclock_vcpu_time_info数据结构。
kvmclock的更新时机:在每次vcpu enter时刻更新。
vcpu_enter_guest [检查vcpu的KVM_REQ_CLOCK_UPDATE flag置位]
kvm_guest_time_update //更新vcpu->hv_clock.tsc_timestamp和vcpu->hv_clock.system_time等
kvm_setup_pvclock_page
【前后端的同步】
有了前后端共享的数据结构(pvclock_vcpu_time_info),后端host负责写,前端guest负责读。那如果前端正在读,但后端也正在修改怎么办?读取的数据是不是可能不准确?
答案就在这version字段上。version表示该结构体信息的版本号。当这个版本号为奇数时,表示主机Host正在修改这个结构体,需要循环读取直至版本号为偶数;当版本号为偶数时,表示当前这个结构体信息已经更新完成,可以用于计算时间。