-
Notifications
You must be signed in to change notification settings - Fork 42
Kvmclock
时钟虚拟化通常有两种实现:
- 通过时钟中断计数,进而换算得到。但vCPU可能会被切出使得时钟中断无法准时到达guest,导致VM的时间落后。从而影响网络操作,导致迁移问题。
- 通过模拟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
一般只会在启动时(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正在更新该结构),则认为该时间是有效的。
填充的是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
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已经是非常理想的时间源,会优先被选中。
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 注册时钟源
通过读 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此时的当前时间
通过读 pvclock_vcpu_time_info 获取system_time。注册为 pv_time_ops.sched_clock
=> pvclock_clocksource_read
当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)
计算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进行设置
初始化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
设置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)
对每个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
更新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中对应地址的数据结构中