x86虚拟机中的时钟
【概述】
虚拟机中看到的关于时间的硬件都是假的,rtc/pit/hpet/tsc/lapic local timer都可以是假的,那么guest读clock当前时间就会导致exit出来,exit出来后kvm计算出一个值返回给guest。guest写timer的超时时间就会导致exit出来,exit出来后kvm给一个软件定时器设置超时时间,等这个软件定时器超后,kvm生成一个时间虚拟中断,把这个中断注入给虚拟机。
虚拟机里看clocksource和clockevent,clocksource用的是kvm-clock那就是因为kernel检测到自己运行在kvm上,并且kvm提供了kvmclock特性。
[root@syh2021v ~]# cat /sys/devices/system/clocksource/clocksource0/available_clocksource
kvm-clock tsc hpet acpi_pm
[root@syh2021v ~]# cat /sys/devices/system/clocksource/clocksource0/current_clocksource
kvm-clock
[root@syh2021v ~]# cat /sys/devices/system/clockevents/broadcast/current_device
hpet
[root@syh2021v ~]# cat /sys/devices/system/clockevents/clockevent0/current_device
lapic-deadline
kvm虚拟机中的clocksource有:kvm-clock、tsc、hpet、acpi_pm、pit等
【clocksource management】
主要逻辑在kernel/time/clocksource.c
clocksource_register_hz
__clocksource_register_scale
clocksource_enqueue
/*
* Enqueue the clocksource sorted by rating
*/
static void clocksource_enqueue(struct clocksource *cs)
{
struct list_head *entry = &clocksource_list;
struct clocksource *tmp;
list_for_each_entry(tmp, &clocksource_list, list) {
/* Keep track of the place, where to insert */
if (tmp->rating < cs->rating)
break;
entry = &tmp->list;
}
list_add(&cs->list, entry);
}
可见,所有的clocksource是根据rating参数大小被组织到了一个list中。linux默认使用rating最高的clocksource,或者用户修改过clocksource,通过clocksource_select函数指定clocksource。
在arch/x86/kernel/kvmclock.c中:
struct clocksource kvm_clock = {
.name = "kvm-clock",
.read = kvm_clock_get_cycles,
.rating = 400,
.mask = CLOCKSOURCE_MASK(64),
.flags = CLOCK_SOURCE_IS_CONTINUOUS,
};
设置了,kvmclock的rating是400;
同理,在arch/x86/kernel/tsc.c中,可以看到tsc的rating是300;
arch/x86/kernel/hpet.c中,hpet的rating是250;
drivers/clocksource/acpi_pm.c中,acpi_pm的rating是200;
drivers/clocksource/i8253.c中,pit的rating是110。
所以,linux内核中clocksource框架选择的顺序是:kvmclock>tsc>hpet>acpi_pm>pit 。
【kvmclock】
参见单独文章。
【TSC】
Guest中可以配置为TSC Passthrough,也可以配置为vmm截获。这在vmcb或vmcs中都有相应的配置,下图是vmcb中的配置位:
以配置为vmm截获为例,如果Guest中使用rdtsc指令,则会被Host拦截,Host中处理后返回给Guest:
em_rdtsc [arch/x86/kvm/emulate.c]
svm_get_msr/vmx_get_msr
kvm_get_msr_common
case MSR_IA32_TSC:
msr_info->data = kvm_scale_tsc(vcpu, rdtsc()) + vcpu->arch.tsc_offset;
break;
这里会将host tsc值进行缩放(scaling)并添加上offset之后,返回guest。为什么要scaling和添加offset?猜测scaling是因为不同cpu平台的模拟(host和guest的cpu不同,当然tsc频率也就不同);而offset主要是解决热迁移问题。
【HPET】
Hpet在kvm+qemu平台是个用户态纯模拟出来的:qemusource_root_dir/hw/timer/hpet.c 。
/*作为clocksource,提供counter*/
hpet_init
memory_region_init_io //注册hpet_ram_ops
hpet_ram_ops提供read函数hpet_ram_read、和write函数hpet_ram_write供前端读写设置hpet counter。
/*作为clockevent,提供timer定时器中断*/
hpet_realize
hpet_timer
update_irq
qemu_irq_raise / qemu_irq_pulse
qemu模拟了hpet device,并在用户态周期性的inject irq,在Guest中就觉得是一个timer了。
【PIT】
实现代码在linux kvm源码arch/x86/kvm/i8254.c中。
kvm_create_pit
pit->worker = kthread_create_worker(0, "kvm-pit/%d", pid_nr);
kthread_init_work(&pit->expired, pit_do_work);
hrtimer_init(&pit_state->timer, CLOCK_MONOTONIC, HRTIMER_MODE_ABS);
pit_state->timer.function = pit_timer_fn;
static enum hrtimer_restart pit_timer_fn(struct hrtimer *data)
{
struct kvm_kpit_state *ps = container_of(data, struct kvm_kpit_state, timer);
struct kvm_pit *pt = pit_state_to_pit(ps);
if (atomic_read(&ps->reinject))
atomic_inc(&ps->pending);
kthread_queue_work(pt->worker, &pt->expired);
if (ps->is_periodic) {
hrtimer_add_expires_ns(&ps->timer, ps->period);
return HRTIMER_RESTART;
} else
return HRTIMER_NORESTART;
}
static void pit_do_work(struct kthread_work *work)
{
struct kvm_pit *pit = container_of(work, struct kvm_pit, expired);
struct kvm *kvm = pit->kvm;
struct kvm_vcpu *vcpu;
int i;
struct kvm_kpit_state *ps = &pit->pit_state;
if (atomic_read(&ps->reinject) && !atomic_xchg(&ps->irq_ack, 0))
return;
kvm_set_irq(kvm, pit->irq_source_id, 0, 1, false);
kvm_set_irq(kvm, pit->irq_source_id, 0, 0, false);
/*
* Provides NMI watchdog support via Virtual Wire mode.
* The route is: PIT -> LVT0 in NMI mode.
*
* Note: Our Virtual Wire implementation does not follow
* the MP specification. We propagate a PIT interrupt to all
* VCPUs and only when LVT0 is in NMI mode. The interrupt can
* also be simultaneously delivered through PIC and IOAPIC.
*/
if (atomic_read(&kvm->arch.vapics_in_nmi_mode) > 0)
kvm_for_each_vcpu(i, vcpu, kvm)
kvm_apic_nmi_wd_deliver(vcpu);
}
Host为Guest的pit创建了一个内核线程,名称就是“kvm-pit/PID”。所以,启动一个qemu虚拟机之后,ps找到qemu的pid,然后就能看到一个对应的内核线程。这个内核线程稍微特殊一点,不是一个常规定义的routine函数,是基于kworker机制。
Host中调用hrtimer_init函数创建一个hr timer。hr timer会在interval后调用callback函数---pit_timer_fn。 pit_timer_fn就是把一个work加入到worker queue中,刚刚创建的kvm-pit kworker线程就可以执行了。kvm-pit真正执行的,就是pit_do_work函数。
当然,pit也是既可以作为timer,也可以作为clocksource。同hpet一样,它也提供了read、write函数来供前端读取配置及counter值信息。
【说明】
1、
需要说明的是,即使有这么多clocksource,一个虚拟机当然没必要全部都用,尤其是拥有靠前的几个高精度时钟源,pit、hpet都可以选择性配置,如下:
<clock offset='utc'>
<timer name='pit' tickpolicy='delay'/>
<timer name='rtc' tickpolicy='catchup'/>
<timer name='hpet' present='no'/>
</clock>
-rtc base=utc,driftfix=slew -global kvm-pit.lost_tick_policy=delay -no-hpet
看这台虚拟机就模拟rtc和pit,没有hpet。
2、
细心的读者可以发现,前面在介绍pit的时候,linux内核出现了两个路径:
1>drivers/clocksource/i8253.c
2>arch/x86/kvm/i8254.c
第一个路径其实是guest在注册clocksource的时候使用的pit驱动文件;第二个路径则是host在模拟pit设备时,使用的驱动文件。一个作为前端,另一个作为后端。
另外,在x86中8254和8253基本可以理解为同一个东西-PIT(Pro
grammable Interval Timer),8254是8253的改良版,支持更精确的时钟频率,通常具有更高的稳定性和准确性。