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的优势应该是时钟补偿吧
不在于效率