kvmclock时钟虚拟化源代码分析
代码版本:linux-git v4.10.0-rc3
1.kvm clock时钟
struct pvclock_vcpu_time_info {
u32 version;
u32 pad0;
//guest的TSC时间戳,在kvm_guest_time_update中会被更新
u64 tsc_timestamp;
//guest的墙上时间(1970年距今的绝对日期),和上者在一起更新
//system_time = kernel_ns + v->kvm->arch.kvmclock_offset
//系统启动后的时间减去VM init的时间,即guest init后到现在的时间
u64 system_time;
//KVMCLOCK时钟频率固定在1GHZ
u32 tsc_to_system_mul;
s8 tsc_shift;
u8 flags;
u8 pad[2];
} __attribute__((__packed__)); /* 32 bytes */
struct pvclock_wall_clock {
u32 version;
u32 sec;
u32 nsec;
} __attribute__((__packed__));
1)KVM clock 在guest中:
kvmclock_init负责在guest启动过程中初始化kvm clock,首先更新了两个MSR值:
#define MSR_KVM_WALL_CLOCK_NEW 0x4b564d00
#define MSR_KVM_SYSTEM_TIME_NEW 0x4b564d01
然后为每个CPU分配struct pvclock_vsyscall_time_info内存,
获取了首要CPU的pvti对应的物理地址,将其写入到msr_kvm_system_time MSR中
src = &hv_clock[cpu].pvti; low = (int)slow_virt_to_phys(src) | 1; high = ((u64)slow_virt_to_phys(src) >> 32); ret = native_write_msr_safe(msr_kvm_system_time, low, high); //启用STEAL time机制 if (kvm_para_has_feature(KVM_FEATURE_CLOCKSOURCE_STABLE_BIT)) pvclock_set_flags(PVCLOCK_TSC_STABLE_BIT); 在kvm_sched_clock_init中, kvm_sched_clock_offset = kvm_clock_read();
获取当前的时钟偏移,
在guest 虚拟的硬件平台上指定函数 x86_platform.calibrate_tsc = kvm_get_tsc_khz; x86_platform.calibrate_cpu = kvm_get_tsc_khz; //get_wallclock本质是获取1970年到现在的时间差,本质是绝对日期 x86_platform.get_wallclock = kvm_get_wallclock; x86_platform.set_wallclock = kvm_set_wallclock;
重点就看上面4个函数, kvm_get_tsc_khz src = &hv_clock[cpu].pvti; tsc_khz = pvclock_tsc_khz(src);
获取pv clock信息转换成hz
kvm_get_wallclock //获取wall clock变量的物理地址,写入到msr中, low = (int)__pa_symbol(&wall_clock); high = ((u64)__pa_symbol(&wall_clock) >> 32); native_write_msr(msr_kvm_wall_clock, low, high);
写完msr后wall_clock就是更新后的墙上时间,即guest启动的日期。
然后再加上pvclock_clocksource_read(vcpu_time)即guest启动时间,则就是当前guest中的real time。
2)KVM clock 在host中:
(1) MSR_KVM_SYSTEM_TIME_NEW
vcpu->arch.time = data; //time就是kvm clock对应的guest pvclock_vsyscall_time_info地址, 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; kvm_make_request(KVM_REQ_GLOBAL_CLOCK_UPDATE, vcpu);
(2) MSR_KVM_WALL_CLOCK_NEW
//wall_clock是guest中对应的wall_clock地址
vcpu->kvm->arch.wall_clock = data;
kvm_write_wall_clock(vcpu->kvm, data);
//此处获取的是host的启动的绝对时间,即从1970到现在的时间差
getboottime64(&boot);
if (kvm->arch.kvmclock_offset) {
//kvmclock_offset是host启动后到guest启动的相对值,即几分几秒
struct timespec64 ts = ns_to_timespec64(kvm->arch.kvmclock_offset);
boot = timespec64_sub(boot, ts);
}
wc.sec = (u32)boot.tv_sec; /* overflow in 2106 guest time */
wc.nsec = boot.tv_nsec;
在kvm_arch_init_vm中有
kvm->arch.kvmclock_offset = -ktime_get_boot_ns();
因为kvmclock_offset为负值,相减即相加,host启动日期加上guest启动的距离host启动时间的差值等于guest启动的日期,所以write msr的结果就是这样。
2.kvm_guest_time_update分析
static int kvm_guest_time_update(struct kvm_vcpu *v)
{
use_master_clock = ka->use_master_clock;
if (use_master_clock) {
host_tsc = ka->master_cycle_now;
kernel_ns = ka->master_kernel_ns;
}
if (!use_master_clock) {
host_tsc = rdtsc();
kernel_ns = ktime_get_boot_ns();
}
//获取host中tsc与TSC时钟1KHZ的比例
tgt_tsc_khz = __this_cpu_read(cpu_tsc_khz);
//读取guest中当前tsc
tsc_timestamp = kvm_read_l1_tsc(v, host_tsc);
if (vcpu->tsc_catchup) {
u64 tsc = compute_guest_tsc(v, kernel_ns);
if (tsc > tsc_timestamp) {
adjust_tsc_offset_guest(v, tsc - tsc_timestamp);
tsc_timestamp = tsc;
}
}
if (kvm_has_tsc_control)
//将1KHZ TSC转换成guest TSC
tgt_tsc_khz = kvm_scale_tsc(v, tgt_tsc_khz);
//如果当前guest时钟(kvmclock)的频率不同,则更新转换比例
if (unlikely(vcpu->hw_tsc_khz != tgt_tsc_khz)) {
kvm_get_time_scale(NSEC_PER_SEC, tgt_tsc_khz * 1000LL,
&vcpu->hv_clock.tsc_shift,
&vcpu->hv_clock.tsc_to_system_mul);
//即是guest tsc转换成kvmclock的比例
vcpu->hw_tsc_khz = tgt_tsc_khz;
}
//当前kvmclock下的TSC值
vcpu->hv_clock.tsc_timestamp = tsc_timestamp;
//当前kvmclock下的guest了多少ns
vcpu->hv_clock.system_time = kernel_ns + v->kvm->arch.kvmclock_offset;
vcpu->last_guest_tsc = tsc_timestamp;
pvclock_flags = 0;
if (use_master_clock)
pvclock_flags |= PVCLOCK_TSC_STABLE_BIT;
vcpu->hv_clock.flags = pvclock_flags;
if (vcpu->pv_time_enabled)
kvm_setup_pvclock_page(v);
if (v == kvm_get_vcpu(v->kvm, 0))
kvm_hv_setup_tsc_page(v->kvm, &vcpu->hv_clock);
}
3.关于use_master_clock
在kvm_write_tsc中,本次tsc写和上次的tsc写比较,得到elapsed和usdiff
ns = ktime_get_boot_ns(); elapsed = ns - kvm->arch.last_tsc_nsec; usdiff = data - kvm->arch.last_tsc_write;
用usdiff与elapsed进行对冲,如果二者差值小于usdiff < USEC_PER_SEC则证明tsc是稳定的
因为last_tsc_write和last_tsc_nsec都是在KVM下而非vcpu下,就是证明所有tsc是稳定的意义
if (!matched) {
kvm->arch.nr_vcpus_matched_tsc = 0;
} else if (!already_matched) {
kvm->arch.nr_vcpus_matched_tsc++;
}
4.KVMCLOCK的优点
kvm_get_wallclock替代mach_get_cmos_time获取rtc时间,mach_get_cmos_time函数在guest中执行需要多个pio vmexit才能完成,而kvm_get_wallclock只需要一个msr write即可,简便了操作,也不要在QEMU RTC的支持。
通过0x70,0x71端口操作。
参考LINUX内核:mach_get_cmos_time(),启动的时候获取日期时间。虽然内核也可以在每次需要的得到当前时间的时候读取 RTC,但这是一个 IO 调用,性能低下。实际上,在得到了当前时间后,Linux 系统会立即启动 tick 中断。此后,在每次的时钟中断处理函数内,Linux 更新当前的时间值,并保存在全局变量 xtime 内。比如时钟中断的周期为 10ms,那么每次中断产生,就将 xtime 加上 10ms。
unsigned char rtc_cmos_read(unsigned char addr)
{
unsigned char val;
lock_cmos_prefix(addr);
outb(addr, RTC_PORT(0));
val = inb(RTC_PORT(1));
lock_cmos_suffix(addr);
return val;
}
对于calibrate_tsc和calibrate_cpu同理。因为kvmclock效率只在启动的时候有体现,整体看替代效率并不明显。
关键在于时钟源的读取不再依赖于xtime的中断:
static struct clocksource kvm_clock = {
.name = "kvm-clock",
.read = kvm_clock_get_cycles,
.rating = 400,
.mask = CLOCKSOURCE_MASK(64),
.flags = CLOCK_SOURCE_IS_CONTINUOUS,
};
static u64 kvm_clock_read(void)
{
struct pvclock_vcpu_time_info *src;
u64 ret;
int cpu;
preempt_disable_notrace();
cpu = smp_processor_id();
src = &hv_clock[cpu].pvti;
ret = pvclock_clocksource_read(src);
preempt_enable_notrace();
return ret;
}
直接获取虚拟的clock时间。
而tsc的时钟源是
static struct clocksource clocksource_tsc = {
.name = "tsc",
.rating = 300,
.read = read_tsc,
.mask = CLOCKSOURCE_MASK(64),
.flags = CLOCK_SOURCE_IS_CONTINUOUS |
CLOCK_SOURCE_MUST_VERIFY,
.archdata = { .vclock_mode = VCLOCK_TSC },
.resume= tsc_resume,
};
和TSC相比,kvmclock优势并不明显,除非TSC进行了迁移。
kvmclock时钟虚拟化源代码分析来自于OENHAN
链接为:https://oenhan.com/kvm-pv-kvmclock-tsc/
kvmclock的优势应该是时钟补偿吧
不在于效率