前言:本文归纳了现在的Linux时钟源的种类,并且针对PIT时钟设备分析了KVM是如何模拟时钟的。
1. Linux 时钟
时钟是一个系统工作的灵魂,时钟硬件的发展也非常的快,Linux内核里面兼容的时钟种类也非常的多,为了让大家更了解时钟,我先归纳一下Linux里面的时钟。本文分析的时钟是比较新的linux内核2.6.33,架构是改变最迅速的x86。 linux时钟发展到现在,很多东西已经面目全非了,要理出个头绪来不是很容易。
(1) RTC 实时时钟:
这个是我们系统定时器时间的基准,通过cmos的读写,能够永久存在,开机时读取,需要的时候写入,它能够提供稳定的时钟脉冲,该时钟在IRQ8上周期性的产生信号,频率在2Hz ~ 8192Hz之间,在Linux中,可以用RTC来获取当前的时间。它提供的稳定的脉冲就是可编程计数器的最底层。也就是说它是PIT和HPET的源头。
(2)pit 可编程时钟
这个是众所周知的,古老的8254时钟,它是一个时钟源,通过古老的8259pic 0号中断响应 timer_interrupt 这个中断处理函数。现在的Linux基本上不怎么用它了,当然这个是比较经典的时钟,说白了它就是个硬件实现的计数器系统,通过时钟中断来满足操作系统的定时器需求,通过写某些位来设置定时器,操作系统同时也要维护一套软件的定时器链表,与这个硬件链表进行对应,具体的可以深入研究PIT。
(3)hpet高精准时钟
cpu内部时钟源,同样响应0号中断响应timer_interrupt 。Linux中hpet已经取代了pit,有hpet系统先选择hpet.
(4)tsc 时间戳计数器
它是CPU内部的一个寄存器,根据CPU的周期进行递增,在我看过的很多系统都是通过读取这个时间来作为系统的时间。通过它也可以校准RTC。
(5)local timer 本地时钟
本地时钟,这个时钟是基于总线的时钟,比较特殊,这个是针对smp系统的时钟,当系统没有本地时钟的时候,也可以通过时钟广播的方式模拟这个本地时钟,在smp系统中linux会优先选择这个时钟源,一旦使用了这个时钟,0号中断绑定的时钟源的profiling和update功能将会被switched off。比如pit将会从periodic转变为oneshot。所以只要打开了local apic,将优先使用这个时钟源。它的中断处理函数smp_apic_timer_interrupt。它在x86 arch里面的entry_64.s汇编文件中built。有点复杂。。实现在apic.c中。
(6)kvm_timer等
这些时钟源用处不多,先不做解释了,自己有兴趣可以去看。
说了那么多,都没介绍时钟的基本知识,现在补充两个系统时钟的常识jiffies和xtimer,前者是linux系统时钟的累计数字,后者是系统的墙上时间。。这个就不多讲了。还有一个下面要用到的hrtimer,这个不是时钟源,只是一种高精准软件时钟计算方法,Linux现在一般时钟这个来计算ticker。
2. KVM的8254时钟
我们都知道KVM虚拟机在性能上面最致命的就是VM-exit,它的损耗是非常的大的,而引起的退出的原因有很多,我会在别的文章中归纳出来,而其中比较致命的就是中断。如果减少中断进而减少虚拟机的负担也应该是做一个优秀虚拟机必须考虑的问题。
KVM很多的硬件模拟都是使用QEMU的,但是时钟已经重做了,所以我们在kvm-kmod的x86目录下或者在内核里面的kvm目录下能看到I8254.c这个文件。在这个文件中已经实现了大部分硬件PIT的IO功能和中断功能。
(1). I8254初始化
通过之前文章我们知道如果虚拟一个设备,KVM的8254也是这样虚拟出来的,所以它的初始化我就从KVM内部开始讲起:
kvm_create_pit
说到这个函数我不得不申明一下,我先列出的函数是kmod-2.6.36版本的,这与之前的2.6.33版本有很大改变,这个变化会在后文中进行比较和讨论。
struct kvm_pit *kvm_create_pit(struct kvm *kvm, u32 flags)
{
... ...
... ...
/*初始化工作队列,这个pit_do_work是一个函数,用来向客户机注入时钟中断,作用和硬件PIT的时钟中断触发是一样的。*/
INIT_WORK(&pit->expired, pit_do_work);
kvm->arch.vpit = pit;
pit->kvm = kvm;
pit_state = &pit->pit_state;
pit_state->pit = pit;
/*初始化一个高精准定时器,这个定时器就作为我们虚拟时钟的时钟源,然而定期是的物理时钟源根据不同的硬件而不同*/
hrtimer_init(&pit_state->pit_timer.timer, CLOCK_MONOTONIC, HRTIMER_MODE_ABS);
pit_state->irq_ack_notifier.gsi = 0;
/*时钟中断ack模拟函数*/
pit_state->irq_ack_notifier.irq_acked = kvm_pit_ack_irq;
kvm_register_irq_ack_notifier(kvm, &pit_state->irq_ack_notifier);
/*累加的时钟中断需要重新进行注入*/
pit_state->pit_timer.reinject = true;
mutex_unlock(&pit->pit_state.lock);
kvm_pit_reset(pit);
pit->mask_notifier.func = pit_mask_notifer;
kvm_register_irq_mask_notifier(kvm, 0, &pit->mask_notifier);
/*时钟设备IO挂载,注册IO读写函数,这个在之前IO虚拟化中详细讨论过*/
kvm_iodevice_init(&pit->dev, &pit_dev_ops);
ret = kvm_io_bus_register_dev(kvm, KVM_PIO_BUS, &pit->dev);
if (ret < 0)
goto fail;
/*speaker的模拟*/
if (flags & KVM_PIT_SPEAKER_DUMMY) {
kvm_iodevice_init(&pit->speaker_dev, &speaker_dev_ops);
ret = kvm_io_bus_register_dev(kvm, KVM_PIO_BUS, &pit->speaker_dev);
... ...
... ...
}
上面调用的那些函数我都没有进行展开,但是到这里我们大概能知道时钟模拟都做了些什么,我概总结一下3点:
1. 注册了一段IO,用来接收客户机的读写请求。
2. 创建了一个工作队列,用来注入时钟中断。
3. 注册了一个高精准定时器,作为时钟中断的触发源。
从这里可以得出一个结论,没有真正的时钟源,是没有办法模拟时钟的。而关键就在于时钟中断,有了时钟中断,其他的定时器计数都可以通过软件来实现,所以在i8254.c里面的其他代码基本上是怎么实现模拟定时器的。至于如何根据用户读写IO来管理定时器,我就不进行描述了,这个只有了解8254的人都能看懂。
2. 定时器的创建
为什么会创建定时器,那是因为客户机读写了8254的IO,写了PIT channel的IO,说我需要一个定时器。那么我们在pit_ioport_write里面就需要分析这些IO,看用户是否需要创建定时器,如果需要就调用create_pit_timer函数创建一个PIT。
create_pit_timer
static void create_pit_timer(struct kvm_kpit_state *ps, u32 val, int is_period)
{
struct kvm_timer *pt = &ps->pit_timer;
s64 interval;
/*转化定时器超时时间单位*/
interval = muldiv64(val, NSEC_PER_SEC, KVM_PIT_FREQ);
/*取消之前的定时器*/
hrtimer_cancel(&pt->timer);
/*同步定时器超时*/
cancel_work_sync(&ps->pit->expired);
/*设置定时器的超时时间,是否周期触发,定时器触发调用函数,以及PIT IO处理结构*/
pt->period = interval;
ps->is_periodic = is_period;
pt->timer.function = pit_timer_fn;
pt->t_ops = &kpit_ops;
pt->kvm = ps->pit->kvm;
/*顶空时钟中断累加器*/
atomic_set(&pt->pending, 0);
ps->irq_ack = 1;
/*创建真正的高精准时钟*/
hrtimer_start(&pt->timer, ktime_add_ns(ktime_get(), interval),
HRTIMER_MODE_ABS);
}
这个函数也做了几件事情,可以总结如下2点:
1. 根据用户的需求,设置了一个定时器,作为时钟中断触发的源头。
2. 清空定时器中断参数。
说白了时钟定时器创建就是初始化了时钟中断。
3. 时钟中断的累加
时钟中断累加是通过定时器超时以后,调用我们设置的一个函数,然后将一个整型数字进行累加,这个数字累加的越大,说明积累的中断越多,同时在系统中也会检测如果这个数字有累加,说明有中断产生,就会调用之前设置的时钟注入函数,注入系统时钟中断。
pit_timer_fn这个函数是之前以函数指针形式传给定时器的,定时器超时就调用之,函数如下:
static enum hrtimer_restart pit_timer_fn(struct hrtimer *data)
{
/*得到虚拟的PIT*/
struct kvm_timer *ktimer = container_of(data, struct kvm_timer, timer);
struct kvm_pit *pt = ktimer->kvm->arch.vpit;
/*如果时钟中断需要重新注入,就直接累加,如果不需要重新注入,那么不进行累加,直接合并时钟中断。*/
if (ktimer->reinject || !atomic_read(&ktimer->pending)) {
/*累加中断,触发中断工作队列进队*/
atomic_inc(&ktimer->pending);
queue_work(pt->wq, &pt->expired);
}
/*如果定时器周期触发,则再次启动定时器,否则销毁,这个机制在hrtimer里面能够了解,有兴趣可以去看hrtimer以及ticker的更新*/
if (ktimer->t_ops->is_periodic(ktimer)) {
kvm_hrtimer_add_expires_ns(&ktimer->timer, ktimer->period);
return HRTIMER_RESTART;
} else
return HRTIMER_NORESTART;
}
4. 定时器中断的触发
当定时器将时钟中断pending增加,并且添加完工作队列以后,接着就触发下面的时钟中断注入,如果上一个中断被接收,接着触发下一个。代码如下:
static void pit_do_work(struct work_struct *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;
int inject = 0;
/* 判断上个中断是否被ack */
spin_lock(&ps->inject_lock);
if (ps->irq_ack) {
ps->irq_ack = 0;
inject = 1;
}
spin_unlock(&ps->inject_lock);
if (inject) {
/*模拟一个高电瓶和一个低电瓶,发送给PIC,触发时钟中断。*/
kvm_set_irq(kvm, kvm->arch.vpit->irq_source_id, 0, 1);
kvm_set_irq(kvm, kvm->arch.vpit->irq_source_id, 0, 0);
if (kvm->arch.vapics_in_nmi_mode > 0)
kvm_for_each_vcpu(i, vcpu, kvm)
kvm_apic_nmi_wd_deliver(vcpu);
}
}
kvm_set_irq中断注入过程这里就不展开了,具体的就是最后写入vmcs的中断信息域,然后被客户机识别。具体注入过程在中断注入文章中讲述。
5. 定时器中断的ack
之前在注册pit的时候注册了一个中断ack函数,它会在中断注入以后被中断控制器调用。
static void kvm_pit_ack_irq(struct kvm_irq_ack_notifier *kian)
{
struct kvm_kpit_state *ps = container_of(kian, struct kvm_kpit_state, irq_ack_notifier);
int value;
spin_lock(&ps->inject_lock);
/*注入成功,则中断累加器减一*/
value = atomic_dec_return(&ps->pit_timer.pending);
if(value < 0)
/* 特殊情况,pending本来就是0还减少,说明是无效的ack,如重置,那么恢复pending */
atomic_inc(&ps->pit_timer.pending);
else if (value > 0)
/* 大于0还需要重新注入积累的中断,这个感觉有点问题,会和之前的触发冲突,但是这里用了锁,貌似没有问题,但是不知道会不会影响性能 */
queue_work(ps->pit->wq, &ps->pit->expired);
/*中断结束,设置ack位*/
ps->irq_ack = 1;
spin_unlock(&ps->inject_lock);
}
这样我们就成功的注入一个时钟中断并且有效的返回中断ack了。
以上我们就比较完整的分析了8254PIT设备的全部模拟过程,主要着重于中断。最后我要补充一下前面说的两个版本的区别。
主要不同有两点:
1. 在我们的kvm目录里面有一个timer.c这个文件里有两个函数,这两个函数之前是用来让hrtimer触发的时钟中断累加函数,现在这两个函数已经被改变并且移动到i8254.c中,之前的注入是面向VCPU的,当时钟被累加,VCPU的时钟request位会被改变,所以累加的时候有个VCPU参数,现在的没有,这在之前版本中也提到会进行改进。进化以后也就是我们现在的pit_timer_fn这个函数。
2. 在之前的2.6.33版本里面是没有那个pit_do_work的工作队列的,之前有两个函数来实现中断注入分别是kvm_inject_pit_timer_irqs和__inject_pit_timer_intr,他们在虚拟机退出以后,检查完时钟中断位以后,如果有,就会被调用,然后进行时钟中断的注入。现在是一旦触发,加入工作队列,即时注入,可能实时性要好点吧,之前的注入频率取决于系统的本身退出频率,当然这个频率也是远远高于时钟注入频率的,所以很少会出现丢失或不及时的情况。
这两点有点抽象,刚刚接触虚拟化的需要慢慢琢磨,KVM在发展过程中改变了很多机制,也有很多好的想法在后面的代码中覆盖掉了,这个也是后来的研究人员不容易注意到的。
总结:本文简要总结了Linux现在的时钟源,并且针对8254时钟详细的阐述了KVM虚拟时钟的过程。这个也是怎么样用软件去模拟一个实际的有中断的硬件设备的标准方法。希望对大家学习KVM有所帮助,也希望喜欢虚拟化的人能够越来越多。
阅读(1437) | 评论(0) | 转发(1) |