KVM Time Virtualization
TSC virtualization
Basics
tsc_khz
为Host的pTSCfreq- 用户态通过
ioctl(vcpu, KVM_SET_TSC_KHZ)
设置vCPU的vTSCfreq,即使KVM不支持TSC Scaling kvm_has_tsc_control
表示硬件是否支持TSC Scaling- 若不支持,只要vTSCfreq > pTSCfreq,仍可成功设置,此时
vcpu->arch.tsc_catchup = 1
,vcpu->arch.always_catchup = 1
- 若不支持,只要vTSCfreq > pTSCfreq,仍可成功设置,此时
vcpu->arch.virtual_tsc_khz
为vCPU的vTSCfreqvcpu->arch.virtual_tsc_mult/virtual_tsc_shift
用于将nsec转换为tsc value
vcpu->arch.tsc_scaling_ratio
为vTSCfreq / pTSCfreq,是一定点浮点数(Intel VT-x中高16位为整数部分,低48位为小数部分)
TSC Matching
Host/Guest写入TSC时,会调用kvm_write_tsc
。
0). 写入的值记为vTSC
,则我们要将L1 TSC Offset设置为offset = vTSC - (pTSC * scale)
1). 每次写入完会记录kvm->arch.last_tsc_nsec
、kvm->arch.last_tsc_write
、kvm->arch.last_tsc_khz
,以供下次调用时使用:
- nsec表示写入时刻的Host Boot Time(
CLOCK_BOOTTIME
) - write表示写入的值
- khz表示vCPU的vTSCfreq
此外还会将写入的值记录到vcpu->arch.last_guest_tsc
2). KVM希望各个vCPU的TSC处在同步状态,称之为Master Clock模式,每当vTSC被修改,就有两种可能:
- 破坏了已同步的TSC,此时将L1 TSC offset设置为
offset
即可- 此时TSC进入下一代,体现为
kvm->arch.cur_tsc_generation++
且kvm->arch.nr_vcpus_matched_tsc = 0
- 此时还会记录
kvm->arch.cur_tsc_nsec
、kvm->arch.cur_tsc_write
、kvm->arch.cur_tsc_offset
,其中offset即上述L1 TSC Offset。重新同步后,可以以此为基点导出任意vCPU的TSC值。
- 此时TSC进入下一代,体现为
- vCPU在尝试重新同步TSC,此时不能将L1 offset设置为
offset
- 此时会设置
kvm->arch.nr_vcpus_matched_tsc++
,一旦所有vCPU都处于matched状态,就可以重新回到Master Clock模式
- 此时会设置
在满足vcpu->arch.virtual_tsc_khz == kvm->arch.last_tsc_khz
的前提下(vTSCfreq相同是TSC能同步的前提),以下情形视为尝试同步TSC:
- Host Initiated且写入值为0,视为正在初始化
- 写入的TSC值与
kvm->arch.last_tsc_write
偏差在1秒内
按照Host TSC是否同步(即是否stable),会为L1 TSC offset设置不同的值
- 对于Stable Host,L1 TSC Offset设置为
kvm->arch.cur_tsc_offset
- 对于Unstable Host,则将L1 TSC Offset设置为
(vTSC + nsec_to_tsc(boot_time - kvm->arch.last_tsc_nsec)) - (pTSC * scale)
,即假设本次和上次写入的TSC值相同,然后补偿上Host Boot Time的差值
此外,我们还会记录当前vCPU和哪一代TSC相同步,存放在vcpu->arch.this_tsc_generation
、vcpu->arch.this_tsc_nsec
、vcpu->arch.this_tsc_write
3). 若为Guest模拟了IA32_TSC_ADJUST
这个MSR,则对TSC Offset的改动,需要同步体现在IA32_TSC_ADJUST
,因此需要修改vcpu->arch.ia32_tsc_adjust_msr
PS: Host initiated的
IA32_TSC_ADJUST
修改,保持vTSC不变,不需要同步改动vTSC
4). 若Host Clocksource为TSC,且Guset TSC已同步,则发送KVM_REQ_MASTER_CLOCK_UPDATE
,这会启用Master Clock模式,即设置kvm->arch.use_master_clock = 1
。
反之,若已有kvm->arch.use_master_clock = 1
,也要发送KVM_REQ_MASTER_CLOCK_UPDATE
,它会检查当前是否还满足Master Clock的要求,若不满足则关闭Master Clock模式。
kvmclock & related stuff
Basics
- 每300秒(
KVMCLOCK_SYNC_PERIOD
)调用一次kvmclock_sync_fn
,它会调用kvmclock_update_fn
kvmclock_update_fn
会对每个vCPU发送KVM_REQ_CLOCK_UPDATE
KVM_REQ_CLOCK_UPDATE ==> kvm_guest_time_update(vcpu)
KVM_REQ_GLOBAL_CLOCK_UPDATE ==> kvm_gen_kvmclock_update(vcpu)
- 立即对当前vCPU发送
KVM_REQ_CLOCK_UPDATE
- 100毫秒后触发
kvmclock_update_fn
- 立即对当前vCPU发送
KVM_REQ_MASTER_CLOCK_UPDATE ==> kvm_gen_update_masterclock
pvclock
KVM中定义了如下数据结构(pvclock),用来提供kvmclock:
1 | struct pvclock_gtod_data { |
kvm通过pvclock_gtod_register_notifier
向timekeeper层注册了一个回调pvclock_gtod_notify
,每当Host Kernel时钟更新时(即timekeeping_update
被调用时),就会调用pvclock_gtod_notify
。
这个函数做了两件事:
第一,调用update_pvclock_gtod
,更新pvclock:
1 | boot_ns = ktime_to_ns(ktime_add(tk->tkr_mono.base, tk->offs_boot)); |
这里记录了Host Kernel时钟更新时刻的Host Boot Time(CLOCK_BOOTTIME
)和Wall Time(CLOCK_REALTIME
):
wall_time_sec
和nsec_base
分别表示Wall Time的秒部分和纳秒部分,单位分别为秒和纳秒boot_ns
为Boot Time减去nsec_base
的值,单位为纳秒。
此外,cycle_last
表示该时刻clocksource的Counter读数。
第二,若clocksource不是TSC,但全局变量kvm_guest_has_master_clock
非零,说明clocksource从TSC变为了非TSC,此时向所有vCPU发送KVM_REQ_MASTER_CLOCK_UPDATE
,然后令kvm_guest_has_master_clock = 0
。
在KVM_REQ_MASTER_CLOCK_UPDATE
的handlerkvm_gen_update_masterclock
中做了以下几件事:
- 给所有vCPU发送
KVM_REQ_MCLOCK_INPROGRESS
,将它们踢出Guest模式,该请求没有Handler,因此vCPU无法再进入Guest - 调用
pvclock_update_vm_gtod_copy
- 给所有vCPU发送
KVM_REQ_CLOCK_UPDATE
- 清除所有vCPU的
KVM_REQ_MCLOCK_INPROGRESS
request bit,让vCPU重新进入Guest
pvclock_update_vm_gtod_copy
也是做两件事:
第一,若Host Clocksource为TSC,则读取当前时刻的TSC值,记为pTSC1
,pvclock中记录的TSC值记为pTSC0
,我们据此设置Master Clock:
kvm->arch.master_cycle_now
设置为pTSC1
,即该时刻的Host TSC值kvm->arch.master_kernel_ns
设置为nsec_base + tsc_to_nsec(pTSC1 - pTSC0) + boot_ns
即该时刻的Host Boot Time
第二,更新kvm->arch.use_master_clock
,若更新后use_master_clock = 1
,则令kvm_guest_has_master_clock = 1
。满足以下条件才会设置use_master_clock = 1
:
1 | ka->use_master_clock = host_tsc_clocksource && vcpus_matched |
前两个条件分别为Host Clocksource为TSC以及vCPU的TSC同步,后两个条件暂时先不解释,下面会详细展开。
KVM_REQ_CLOCK_UPDATE
: kvmclock & Master Clock
在kvm_guest_time_update
中,首先会获取两个变量kernel_ns
和host_tsc
:
- 若处于Master Clock模式(
kvm->arch.use_master_clock = 1
),则两者分别取kvm->arch.master_kernel_ns
和kvm->arch.master_cycle_now
- 若不处于Master Clock模式,则两者分别取当前时刻的Host Boot Time和TSC值
这样,在Master Clock模式下,所有vCPU都使用同一组数据,而非Master Clock模式下每个vCPU都自己测量时间。
接下来我们要填充struct pvclock_vcpu_time_info
(简称pvti),这是Host和Guest共享的数据结构,也就是kvmclock的数据源,每个vCPU都有一个:
1 | struct pvclock_vcpu_time_info { |
我们首先设置tsc_to_system_mul
和tsc_shift
:
- 若支持TSC Scaling,则Guset TSC的运行频率为
cpu_tsc_khz * scale
- 若不支持TSC Scaling,则Guest TSC的运行频率为
cpu_tsc_khz
,即与Host相同
根据Guest TSC的运行频率,可以获得tsc_to_system_mul
和tsc_shift
,用于将Guset TSC值转换为纳秒。注意对于Host TSC频率可变的情形,cpu_tsc_khz
可以和KVM_SET_TSC_KHZ
时的tsc_khz
不同。事实上每次pCPU频率改变,就会更新cpu_tsc_khz
并对该pCPU上的所有vCPU发送KVM_REQ_CLOCK_UPDATE
,但scale
始终不变。
这说明kvmclock中的vTSC运行频率就是Guest TSC的实际运行频率,和KVM_SET_TSC_KHZ
设置的vTSCfreq不一定相等(例如在Host TSC频率可变时会不相等)。
然后我们设置tsc_timestamp
,它代表抽样时刻的Guest TSC,其中抽样时刻为pvclock更新即Host Timekeeper更新时(Master Clock模式)或kvm_guest_time_update
时(非Master Clock模式):
- 若不需要catchup,则根据
host_tsc
算出tsc_timestamp = vTSC = offset + (host_tsc * scale)
即可 - 若需要catchup(
vcpu->arch.tsc_catchup = 1
),则我们根据kernel_ns - vcpu->arch.this_tsc_nsec
,计算出从上次写入TSC到抽样时刻的时间差,然后按照vTSCfreq转换成TSC差值,最后加上vcpu->arch.this_tsc_write
即上次写入的TSC值,得到抽样时刻的TSC理论值vTSC_theory
- 如果
vTSC_theory > vTSC
,即Guest的TSC走得比设定的慢,则我们可以进行「catch up」,将Guest的TSC Offset增加vTSC_theory - vTSC
,并将tsc_timestamp
设置为vTSC_theory
- 如果
vTSC_theory < vTSC
,即Guest的TSC走得比设定要快,则我们无法补救,因为TSC必须单调增,因此什么都不做
- 如果
此外,还会降vcpu->arch.last_guest_tsc
设置为tsc_timestamp
的值。
如果在KVM_SET_TSC_KHZ
时设置的vTSCfreq > pTSCfreq,且Host不支持TSC Scaling,便会设置vcpu->arch.always_catchup = 1
,每次VMExit就会发送KVM_REQ_CLOCK_UPDATE
,从而保证TSC Offset不断增加,令Guest TSC不断catchup设定的vTSCfreq下的理论值。
这里我们又可以看出,我们实际上只能处理Guest TSC实际频率低于虚拟频率的情况,而不能处理实际频率高于虚拟频率的情况。由于KVM_SET_TSC_KHZ
只保证了用户传入的vTSCfreq > tsc_khz
即当时的pTSCfreq,因此此后pTSCfreq上升到大于vTSCfreq也是可能的。
最后,我们还要设置system_time = kernel_ns + kvm->arch.kvmclock_offset
,其中kvmclock_offset
的初始值为VM创建时的Host Boot Time的相反数,即令VM创建时的system_time
为0,system_time
表示Guest的Boot Time。同时,用户态可以通过KVM_SET_CLOCK
ioctl来设置kvmclock的system_time
值,从而修改kvmclock_offset
,这就允许QEMU采用不同的初始化行为。
Conclusion: 总之,每个vCPU有一个自己的pvti结构,KVM会定期采样Host TSC和Host Boot Time,最后为pvti填入该时刻的Guest TSC(由Host TSC导出)和System Time(Host Boot Time + Kvmclock Offset)
关于为什么要有Master Clock模式,下面这段注释解释得很好:
1 | /* |
由于我们的kvmclock依赖于Host Boot Time和Host TSC两个量,即使Host TSC同步且Guest TSC同步,在pCPU0和pCPU1分别取两者,前者的差值和后者的差值也可能不相等,并且谁大谁小都有可能,从而可能违反kvmclock的单调性。因此,我们通过只使用一份Master Copy,即Master Clock,来解决这个问题。
More on kvmclock
在Guest Linux Kernel初始化时,会调用kvmclock_init
进行kvmclock的初始化,这个函数在BSP上运行:
1 | start_kernel |
下面我们分析kvmclock_init
进行了什么初始化操作:
首先,我们为所有CPU设置了回调kvmclock_setup_percpu
,当该CPU被bringup时就调用该回调,不过此时尚在BSP初始化的阶段,因此不会立即执行。
上文说过,每个vCPU都有一个pvti,现在先将vCPU0的pvti设置为hv_clock_boot[0]
,其物理地址为GPA
,然后将GPA | 1
写入MSR_KVM_SYSTEM_TIME/MSR_KVM_SYSTEM_TIME_NEW
,注册kvmclock。
KVM对这个MSR的写入的模拟如下:
- 如果写入的是
MSR_KVM_SYSTEM_TIME
,表明Guest使用的是旧版kvmclock,不支持Master Clock模式,此时要设置kvm->arch.boot_vcpu_runs_old_kvmclock = 1
,并对当前vCPU(即vCPU0)发送一个KVM_REQ_MASTER_CLOCK_UPDATE
,这最终会导致kvm->arch.use_master_clock = 0
- 令
vcpu->arch.time = GPA | 1
,该变量用于模拟对该MSR的读取 - 向当前vCPU(即vCPU0)发送
KVM_REQ_GLOBAL_CLOCK_UPDATE
,这最终会导致在所有vCPU上运行kvm_guest_time_update
- 最后,设置
vcpu->arch.pv_time
为GPA
,并令vcpu->arch.pv_time_enabled = true
接着,我们注册了一系列回调:
1 | kvm_sched_clock_init(flags & PVCLOCK_TSC_STABLE_BIT); |
最后,我们注册了kvm_clock
这个clocksource:
1 | struct clocksource kvm_clock = { |
注意当Host和Guest都是stable时,我们实际上直接使用TSC作为Clocksource,kvmclock只用于提供wall clock。
kvmclock_init
之后运行的是early_initcall(kvm_setup_vsyscall_timeinfo)
,该函数也是在BSP上运行,它主要是动态分配并初始化了hv_clock_mem
,以防止静态分配的hv_clock_boot
数组不够用。
然后,当AP启动时会执行Hotplug回调kvmclock_setup_percpu
,将per cpu变量hv_clock_per_cpu
设置为hv_clock_boot/hv_clock_mem
的成员。
最后,AP启动后初始化时(即start_sencondary
函数中),会调用x86_cpuinit.early_percpu_clock_init
即kvm_setup_secondary_clock
,将AP的pvti地址写入MSR_KVM_SYSTEM_TIME/MSR_KVM_SYSTEM_TIME_NEW
,在KVM中注册kvmclock。
kvm_clock
作为一个clocksource,其频率为1GHz,实际上其read
回调返回的值是就是System Time(即Host Boot Time + Kvmclock Offset)的读数,单位为纳秒。read
回调最终调用到pvclock_clocksource_read
,实现如下:
- 首先,获取当前的System Time,即
pvti->system_time + tsc_to_nsec(rdtsc() - pvti->tsc_timestamp)
- 其次,若不在Master Clock模式,则上述求得的System Time可能出现倒退,我们用一个全局变量
last_value
记录上次kvm_clock
读到的值,若当前求得的System Time小于last_value
,则本次读取返回last_value
,否则返回System Time并设置last_value
为System Time
另外kvmclock还提供了读取wall clock的功能(不允许写入),即上文的kvm_get_wallclock
回调。首先,我们有一个类型为struct pvclock_wall_clock
的全局变量wall_clock
:
1 | struct pvclock_wall_clock { |
在kvm_get_wallclock
中,我们首先将wall_clock
的GPA写入MSR_KVM_WALL_CLOCK/MSR_KVM_WALL_CLOCK_NEW
,kvm对此的模拟如下:
- 设置
kvm->arch.wall_clock = GPA
- 通过
getboottime64
获得Host Boot时刻的Wall Clock,再减去kvmclock_offset
,得到的值填入wall_clock
然后,我们调用pvclock_read_wallclock
,首先通过pvclock_clocksource_read
获得当前时刻的System Time,然后加上wall_clock
中的时间,最终即可得到当前时刻的Wall Clock Time。
Others
KVM_REQ_GLOBAL_CLOCK_UPDATE
:
- 在
kvm_arch_vcpu_load
时(即vCPU->pCPU迁移时),若未启用Master Clock,则会发送KVM_REQ_GLOBAL_CLOCK_UPDATE
- 在Guest写入
MSR_KVM_SYSTEM_TIME/MSR_KVM_SYSTEM_TIME_NEW
时,也会发送KVM_REQ_GLOBAL_CLOCK_UPDATE
KVM_REQ_CLOCK_UPDATE
:
- 在系统休眠再恢复后,调用
kvm_arch_hardware_enable
重新启用KVM时,若Host TSC Unstable,需要给当前pCPU上的vCPU发送KVM_REQ_CLOCK_UPDATE
- 用户态调用
KVM_KVMCLOCK_CTRL
ioctl来表示Guest被其暂停,此时会设置vcpu->arch.pvclock_set_guest_stopped_request = true
,然后向vCPU发送KVM_REQ_CLOCK_UPDATE
。最终,在kvm_guest_time_update
中更新pvti时,会设置PVCLOCK_GUEST_STOPPED
这个flag,防止Guest的watchdog认为vCPU发生了soft lockup - 用户态调用
KVM_SET_CLOCK
修改kvmclock offset时,会对所有vCPU发送KVM_REQ_CLOCK_UPDATE
,更新它们的System Time - 当TSC频率发生变化时,要向该pCPU上的所有vCPU发送
KVM_REQ_CLOCK_UPDATE
,更新它们的tsc to nsec转换参数 - 若
vcpu->arch.tsc_always_catchup = 1
,则每次VMExit都要给vCPU发送KVM_REQ_CLOCK_UPDATE
,以实现不断catchup比pTSCfreq更高的vTSCfreq
backward TSC & TSC adjustment
在系统休眠再恢复后,即使Host TSC是Stable的,也可能发生TSC倒退的情况(即休眠时所有CPU的TSC同步重置为零),这种情况下,我们将所有VM instance的kvm->arch.backwards_tsc_observed
设置为true,于是会禁止使用Master Clock模式。
我们取所有VM的所有vCPU的vcpu->arch.last_host_tsc
中最大的,记作oldTSC
,取当前时刻的TSC记作curTSC
,则我们为所有VM的所有vCPU设置vcpu->arch.tsc_offset_adjustment += oldTSC - curTSC
以及vcpu->arch.last_host_tsc = curTSC
,并发送一个KVM_REQ_MASTER_CLOCK_UPDATE
请求(这是为了触发禁用Master Clock模式的操作)。
在vCPU加载时(kvm_arch_vcpu_load
),若vcpu->arch.tsc_offset_adjustment
非零,则会将L1 TSC Offset增加tsc_offset_adjustment * scale
,并将其清零,最后会给自己发送一个KVM_REQ_CLOCK_UPDATE
。
PS: TSC adjustment乘以scale ratio的原因是Guest TSC要保持单调性和连续性,其值应该是
offset + scale * (newTSC + TSC adjustment)
,故TSC Offset应该增加TSC adjustment * scale
TSC catchup
除了Always Catchup模式,还有可能触发catchup行为。在vCPU加载时(kvm_arch_vcpu_load
),如果Host TSC Unstable,则会进行以下操作:
- 根据当前时刻的Host TSC值
pTSC
、vCPU上次记录的TSC值vTSC = vcpu->arch.last_guest_tsc
,求得offset = vTSC - pTSC * scale
,并将其写入L1 TSC Offset - 然后设置
vcpu->arch.tsc_catchup = 1
这是一种保守的策略,我们先将vTSC调整到上次记录的vTSC值,这一定是比理论上的正确vTSC值小的,然后设置vcpu->arch.tsc_catchup
,在接下来的kvm_gen_kvmclock_update
中将vTSC的值修正为理论的正确值。
此后vCPU运行过程中,每次KVM_REQ_CLOCK_UPDATE
请求,都会导致一次catch up,这样至少每隔300秒都会有一次catch up。