Kvmclock - GiantVM/KVM-Annotation GitHub Wiki

定义

时钟虚拟化通常有两种实现:

  1. 通过时钟中断计数,进而换算得到。但vCPU可能会被切出使得时钟中断无法准时到达guest,导致VM的时间落后。从而影响网络操作,导致迁移问题。
  2. 通过模拟HPET,guest需要时间时去读,这会使得虚拟机频繁VM exit,影响性能。

为此KVM引入了基于半虚拟化(PV)的时钟kvmclock,通过在guest上实现一个kvmclock驱动,guest通过该驱动之间向VMM查询时间。

具体来说,是guest分配一片内存(page),将该内存地址通过写入MSR告诉VMM,VMM向地址写入时间,VM去读,实现时间的更新。

guest会先去检查cpuid(KVM-specific cpuid leaves),在0x40000001中bit3指示kvmclock可用;bit0指示kvmclock可用,但需要使用old MSR。

使用以下两个MSR:

arch/x86/include/uapi/asm/kvm_para.h

#define MSR_KVM_WALL_CLOCK_NEW  0x4b564d00
#define MSR_KVM_SYSTEM_TIME_NEW 0x4b564d01

MSR_KVM_WALL_CLOCK_NEW

一般只会在启动时(boot-time)和suspend-resume时使用,因此设置为一次性值,填充的是pvclock_wall_clock结构的地址。即VM读取完后MSR中写的地址就作废了,VMM不能再把它当做pvclock_wall_clock继续写数据。

struct pvclock_wall_clock {
    u32   version;              // 检验数据可用性
    u32   sec;
    u32   nsec;
} __attribute__((__packed__));

如果guest在读取sec和nsec前后version都不变,同时version为偶数(奇数表示VMM正在更新该结构),则认为该时间是有效的。

MSR_KVM_SYSTEM_TIME_NEW

填充的是pvclock_vcpu_time_info的地址,可以反复使用。即VMM可以通过向该地址反复写值来更新时间。最后一个bit表示是否启用kvmclock。

struct pvclock_vcpu_time_info {
    u32   version;              // 同pvclock_wall_clock,检验数据可用性
    u32   pad0;
    u64   tsc_timestamp;        // 为guest设置的tsc(rdtsc + tsc_offset)。在kvm_guest_time_update中会和system_time一起被更新,表示记录system_time时的时间戳
                                // 但指令间还是有时间差,可以计算delta然后加到system_time
    u64   system_time;          // 最近一次从host读到的时间,作为guest的墙上时间。host通过ktime_get_ts从当前注册的时间源获取该时间
                                // system_time = kernel_ns + v->kvm->arch.kvmclock_offset
                                // 系统启动后的时间减去VM init的时间,即VM init后到现在的时间
    u32   tsc_to_system_mul;    // 时钟频率,1nanosecond对应的cycle数(固定在1GHZ)
    s8    tsc_shift;            // guests must shift
    u8    flags;
    u8    pad[2];
} __attribute__((__packed__)); /* 32 bytes */

兼容性

#define MSR_KVM_WALL_CLOCK  0x11
#define MSR_KVM_SYSTEM_TIME 0x12

旧版本的VMM使用了不同的MSR来保存地址,需要进行兼容。如果cpuid 0x40000001中bit3为0但bit0为1,需要使用旧版本的MSR,即 MSR_KVM_WALL_CLOCK 和 MSR_KVM_SYSTEM_TIME。

在kvm_set_msr_common中VMM对于新旧两种MSR执行相同的逻辑。

开销

考量到存取开销,kvmclock只在某些VM事件后才更新(而不是持续不断地写内存),比如reentering the guest after some VM event

KVM clocksource

static struct clocksource kvm_clock = {
    .name = "kvm-clock",
    .read = kvm_clock_get_cycles,
    .rating = 400,                  // 理想时钟源
    .mask = CLOCKSOURCE_MASK(64),
    .flags = CLOCK_SOURCE_IS_CONTINUOUS,
};

400已经是非常理想的时间源,会优先被选中。

Guest驱动

初始化

arch/x86/kernel/kvmclock.c

VM启动后,运行kvmclock的驱动(kvmclock_init),初始化数据结构pvclock_wall_clock,pvclock_vcpu_time_info,将地址写到MSR中。

kvmclock_init => memblock_alloc                             为每个CPU分配struct pvclock_vsyscall_time_info内存
            => kvm_register_clock                           将pvti的gpa写入到MSR中,以告知VMM
            => pvclock_set_flags(PVCLOCK_TSC_STABLE_BIT)    如果支持steal time,设置之
            => kvm_sched_clock_init                         获取当前的时钟偏移,初始化时钟操作,设置读取时间的函数
            => ...                                          在x86_platform中注册相应函数
            => clocksource_register_hz                      注册时钟源

读取

kvm_get_wallclock

通过读 pvclock_wall_clock 获取VM的当前时间。注册为 x86_platform.get_wallclock

=> native_write_msr(msr_kvm_wall_clock, low, high) 将wall_clock的gpa写入VMCS中,以告知VMM去写 => pvclock_read_wallclock VMM写后,加上VM过去的时间,得到VM此时的当前时间

kvm_clock_read

通过读 pvclock_vcpu_time_info 获取system_time。注册为 pv_time_ops.sched_clock

=> pvclock_clocksource_read

VMM

设置

当VM设置了MSR后,触发VM exit,VMM执行以下流程

handle_wrmsr => kvm_set_msr => kvm_x86_ops->set_msr 即 vmx_x86_ops->set_msr (vmx_set_msr) => kvm_set_msr_common => (MSR_KVM_WALL_CLOCK / MSR_KVM_WALL_CLOCK_NEW) => (MSR_KVM_SYSTEM_TIME / MSR_KVM_SYSTEM_TIME_NEW)

MSR_KVM_WALL_CLOCK / MSR_KVM_WALL_CLOCK_NEW

计算VM启动时间,写入到wall_clock中供VM读取。

设置MSR后立刻执行 kvm_write_wall_clock ,只执行一次。

=> kvm_write_wall_clock
    => kvm_read_guest   读取version
    => kvm_write_guest  更新version
    => getboottime64    获取host启动时的时间戳
    => ...              加上|kvmclock_offset|得到计算VM启动的时间戳
    => kvm_write_guest  更新时间
    => kvm_write_guest  更新version

kvm_read_guest => kvm_read_guest_page 和 kvm_write_guest => kvm_write_guest_page 都是通过VM传过来的gpa,对相应的struct wall_clock进行设置

MSR_KVM_SYSTEM_TIME / MSR_KVM_SYSTEM_TIME_NEW

初始化VM的pvclock_vcpu_time_info区域,该区域被映射到vcpu->arch.pv_time

=> kvmclock_reset 初始化 => set_bit(KVM_REQ_MASTERCLOCK_UPDATE, &vcpu->requests) 设置更新bit => kvm_make_request(KVM_REQ_GLOBAL_CLOCK_UPDATE, vcpu) 产生 KVM_REQ_GLOBAL_CLOCK_UPDATE 请求,在enter guest时会处理 => kvm_gfn_to_hva_cache_init(vcpu->kvm, &vcpu->arch.pv_time, data & ~1ULL, sizeof(struct pvclock_vcpu_time_info))) 初始化pvclock_vcpu_time_info的内存区域,得到guest的hv_clock[cpu].pvti

KVM_REQ_GLOBAL_CLOCK_UPDATE

设置MSR后,每次在 vcpu_enter_guest 时都会去执行 不止更新当前vcpu,还要更新其他vcpu

vcpu_enter_guest => vm_gen_kvmclock_update(vcpu);

=> 产生 KVM_REQ_CLOCK_UPDATE 的请求 => schedule_delayed_work(&kvm->arch.kvmclock_update_work, KVMCLOCK_UPDATE_DELAY); 100毫秒后执行kvmclock_update_work绑定的函数 kvmclock_update_fn

#define KVMCLOCK_UPDATE_DELAY msecs_to_jiffies(100)

kvmclock_update_fn

对每个vcpu设置 KVM_REQ_CLOCK_UPDATE 请求,然后调度一下以更新时间

=> kvm_make_request(KVM_REQ_CLOCK_UPDATE, vcpu) => kvm_vcpu_kick // Kick a sleeping VCPU, or a guest VCPU in guest mode, into host kernel mode

KVM_REQ_CLOCK_UPDATE

更新VM的 pvclock_vcpu_time_info 区域,即更新 vcpu->hv_clock

=> kvm_guest_time_update => use_master_clock            如果host使用TSC作为时间源,直接使用vcpu上的时间即可(passthrough)
                    else => rdtsc / get_kernel_ns       否则需要手动读取
                         => kvm_read_l1_tsc             读VM当前的tsc
                         => compute_guest_tsc           计算guest中此时的tsc应该是多少,如果比从VM中读出的大,说明VM走慢了,修正为host算出的值
                         => kvm_write_guest_cached      依次更新version,更新时间和flag,再次更新version

kvm_write_guest_cached 将vcpu->pv_time中的值更新到VM中对应地址的数据结构中