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 = 1vcpu->arch.always_catchup = 1
  • vcpu->arch.virtual_tsc_khz为vCPU的vTSCfreq
    • vcpu->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_nseckvm->arch.last_tsc_writekvm->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_nseckvm->arch.cur_tsc_writekvm->arch.cur_tsc_offset,其中offset即上述L1 TSC Offset。重新同步后,可以以此为基点导出任意vCPU的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_generationvcpu->arch.this_tsc_nsecvcpu->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
  • KVM_REQ_MASTER_CLOCK_UPDATE ==> kvm_gen_update_masterclock

pvclock

KVM中定义了如下数据结构(pvclock),用来提供kvmclock:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct pvclock_gtod_data {
seqcount_t seq;

struct { /* extract of a clocksource struct */
int vclock_mode;
u64 cycle_last;
u64 mask;
u32 mult;
u32 shift;
} clock;

u64 boot_ns;
u64 nsec_base;
u64 wall_time_sec;
};

static struct pvclock_gtod_data pvclock_gtod_data;

kvm通过pvclock_gtod_register_notifier向timekeeper层注册了一个回调pvclock_gtod_notify,每当Host Kernel时钟更新时(即timekeeping_update被调用时),就会调用pvclock_gtod_notify

这个函数做了两件事:

第一,调用update_pvclock_gtod,更新pvclock:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
boot_ns = ktime_to_ns(ktime_add(tk->tkr_mono.base, tk->offs_boot));

write_seqcount_begin(&vdata->seq);

/* copy pvclock gtod data */
vdata->clock.vclock_mode = tk->tkr_mono.clock->archdata.vclock_mode;
vdata->clock.cycle_last = tk->tkr_mono.cycle_last;
vdata->clock.mask = tk->tkr_mono.mask;
vdata->clock.mult = tk->tkr_mono.mult;
vdata->clock.shift = tk->tkr_mono.shift;

vdata->boot_ns = boot_ns;
vdata->nsec_base = tk->tkr_mono.xtime_nsec;

vdata->wall_time_sec = tk->xtime_sec;

write_seqcount_end(&vdata->seq);

这里记录了Host Kernel时钟更新时刻的Host Boot Time(CLOCK_BOOTTIME)和Wall Time(CLOCK_REALTIME):

  • wall_time_secnsec_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
2
3
ka->use_master_clock = host_tsc_clocksource && vcpus_matched
&& !ka->backwards_tsc_observed
&& !ka->boot_vcpu_runs_old_kvmclock;

前两个条件分别为Host Clocksource为TSC以及vCPU的TSC同步,后两个条件暂时先不解释,下面会详细展开。

KVM_REQ_CLOCK_UPDATE: kvmclock & Master Clock

kvm_guest_time_update中,首先会获取两个变量kernel_nshost_tsc

  • 若处于Master Clock模式(kvm->arch.use_master_clock = 1),则两者分别取kvm->arch.master_kernel_nskvm->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
2
3
4
5
6
7
8
9
10
struct pvclock_vcpu_time_info {
u32 version; /* 奇数表示Host正在修改,偶数表示已修改完毕 */
u32 pad0;
u64 tsc_timestamp; /* 更新pvti时,vCPU的vTSC */
u64 system_time; /* 更新pvti时,Guest的虚拟时间,单位为纳秒 */
u32 tsc_to_system_mul; /* 用于将tsc转换为nsec */
s8 tsc_shift; /* 用于将tsc转换为nsec */
u8 flags;
u8 pad[2];
} __attribute__((__packed__)); /* 32 bytes */

我们首先设置tsc_to_system_multsc_shift

  • 若支持TSC Scaling,则Guset TSC的运行频率为cpu_tsc_khz * scale
  • 若不支持TSC Scaling,则Guest TSC的运行频率为cpu_tsc_khz,即与Host相同

根据Guest TSC的运行频率,可以获得tsc_to_system_multsc_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_CLOCKioctl来设置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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*
* "timespecX" represents host monotonic time. "tscX" represents
* RDTSC value.
*
* VCPU0 on CPU0 | VCPU1 on CPU1
*
* 1. read timespec0,tsc0
* 2. | timespec1 = timespec0 + N
* | tsc1 = tsc0 + M
* 3. transition to guest | transition to guest
* 4. ret0 = timespec0 + (rdtsc - tsc0) |
* 5. | ret1 = timespec1 + (rdtsc - tsc1)
* | ret1 = timespec0 + N + (rdtsc - (tsc0 + M))
*
* Since ret0 update is visible to VCPU1 at time 5, to obey monotonicity:
*
* - ret0 < ret1
* - timespec0 + (rdtsc - tsc0) < timespec0 + N + (rdtsc - (tsc0 + M))
* ...
* - 0 < N - M => M < N
*
* That is, when timespec0 != timespec1, M < N. Unfortunately that is not
* always the case (the difference between two distinct xtime instances
* might be smaller then the difference between corresponding TSC reads,
* when updating guest vcpus pvclock areas).
*/

由于我们的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
2
3
4
5
start_kernel
--> setup_arch
--> init_hypervisor_platform
--> x86_init.hyper.init_platform ==> kvm_init_platform
--> kvmclock_init

下面我们分析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_timeGPA,并令vcpu->arch.pv_time_enabled = true

接着,我们注册了一系列回调:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kvm_sched_clock_init(flags & PVCLOCK_TSC_STABLE_BIT);

x86_platform.calibrate_tsc = kvm_get_tsc_khz;
x86_platform.calibrate_cpu = kvm_get_tsc_khz;
x86_platform.get_wallclock = kvm_get_wallclock;
x86_platform.set_wallclock = kvm_set_wallclock;
#ifdef CONFIG_X86_LOCAL_APIC
x86_cpuinit.early_percpu_clock_init = kvm_setup_secondary_clock;
#endif
x86_platform.save_sched_clock_state = kvm_save_sched_clock_state;
x86_platform.restore_sched_clock_state = kvm_restore_sched_clock_state;
machine_ops.shutdown = kvm_shutdown;
#ifdef CONFIG_KEXEC_CORE
machine_ops.crash_shutdown = kvm_crash_shutdown;
#endif

最后,我们注册了kvm_clock这个clocksource:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct clocksource kvm_clock = {
.name = "kvm-clock",
.read = kvm_clock_get_cycles,
.rating = 400,
.mask = CLOCKSOURCE_MASK(64),
.flags = CLOCK_SOURCE_IS_CONTINUOUS,
};
EXPORT_SYMBOL_GPL(kvm_clock);

/*
* X86_FEATURE_NONSTOP_TSC is TSC runs at constant rate
* with P/T states and does not stop in deep C-states.
*
* Invariant TSC exposed by host means kvmclock is not necessary:
* can use TSC as clocksource.
*
*/
if (boot_cpu_has(X86_FEATURE_CONSTANT_TSC) &&
boot_cpu_has(X86_FEATURE_NONSTOP_TSC) &&
!check_tsc_unstable())
kvm_clock.rating = 299;

clocksource_register_hz(&kvm_clock, NSEC_PER_SEC);

注意当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_initkvm_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
2
3
4
5
struct pvclock_wall_clock {
u32 version;
u32 sec;
u32 nsec;
} __attribute__((__packed__));

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_CTRLioctl来表示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。