0012提过高精度时钟中断,这篇来讲下定时器。
传统操作系统内核一般都有个时间片tick,即相隔一定时间发生一次时钟中断,比如1毫秒,10毫秒,dos时代的1/18.5秒,有高精度时钟之后,我们通过定时器来模拟时间片。
在上一篇0014实验的时候用到了定时器,那个定时器是主cpu每1毫秒产生一个时钟中断,系统有个毫秒计数器msec_count。这个精度只有1毫秒。
现在来改造一下,把时钟中断改成单次触发而不是循环触发,精度改成纳秒(实际精度10纳秒)。
lapic timer每个cpu单独,比较适合做定时器。因此我们用lapic timer,而不是用hpet来做定时器。
lapic timer的频率是系统频率,需要检测一下。另外lapic timer有个tsc-deadline模式,这个模式更加精准,而且是64位计数,不像普通模式是32位计数器。但是也要计算一下频率。
增加一个文件,arch/lapic_timer.c
HPET可以读取到频率,因此利用HPET来计算lapic timer的频率。
lapic_timer分2次初始化。
core_initcall_sync(init_apic_timer_call);
postcore_initcall_sync(init_apic_timer_second);
第一次是init_apic_timer_call
static __init int init_apic_timer_call(bool isbp)
{
if(isbp) {
ASSERT(hpet_period);
lapic_write(APIC_SPIV, APIC_SPIV_APIC_ENABLED | SPURIOUS_APIC_VECTOR);
lapic_write(APIC_TDCR, APIC_TDR_DIV_16);
lapic_write(APIC_LVTT, LOCAL_TIMER_VECTOR);
lapic_write(APIC_TMICT, 0xFFFFFFFF);
start_uptime = hpet_uptime();
start_tsc = rdtsc();
printk("cpuid_apic_id:%d,isbp:%d\n",cpuid_apic_id(),isbp);
}
return 0;
}
只初始化主cpu(其他cpu频率相同)。
这个初始化要在hpet之后,因此用的是core_initcall_sync,hpet用的是coreinitcall,后面带sync的比不带的要晚(同一个初始化循环,但是排在后面,在kernel.ld里面设置)。
lapic timer先写入0xffffffff,最大值,然后读取当前时间跟tsc。hpet_uptime()放回hpet初始化之后的纳秒计数。
在第二次初始化的时候,读取新的纳秒计数,再对比lapic_timer里面的计数器就可以算出lapic_timer的频率。包括tsc的频率。
static __init int init_apic_timer_second(bool isbp)
{
return (isbp && calibrate_APIC_clock()) || setup_APIC_timer();
}
第二次同样只处理主cpu,先计算频率,再设置
static __init int calibrate_APIC_clock()
{
unsigned long flags;
flags = local_irq_save();
ulong now_uptime = hpet_uptime();
ulong now_tsc = rdtsc();
ulong remain = lapic_read(APIC_TMCCT);
local_irq_restore(flags);
ulong start = start_uptime;
ulong delta = now_uptime - start;
ASSERT(delta<0x400000000);//纳秒单位大概16秒,超过可能apic_timer32位寄存器溢出
ulong clock = delta*1000/(APIC_DIVISOR*(0xffffffff-remain));
clock+=5;
clock/=10;
clock*=10000UL;
clk_fsecs= clock;
clk_khz = FSEC_PER_MSEC / clk_fsecs;
lapic_timer_period = FSEC_PER_MSEC / APIC_DIVISOR / clock; //1ms clocks
clock = 1000*delta/(now_tsc - start_tsc);
clock*=1000UL;
tsc_fsecs = clock;
tsc_khz = FSEC_PER_MSEC / tsc_fsecs;
return 0;
}
由于lapic_timer是32位寄存器,为了避免溢出,使用主频除16,APIC_DIVISOR为16。
第一次初始化的时候设置了lapic_write(APIC_TDCR, APIC_TDR_DIV_16);
ulong clock = delta*1000/(APIC_DIVISOR*(0xffffffff-remain));
clock算出来的是计数1次所需要的皮秒。+5再除以/10是四舍五入,最后算出来的是计数1次所需要的飞秒(1/1000000纳秒)。khz是千赫兹,即1秒内计数器跳到多少千次。
static int setup_APIC_timer(void)
{
struct clock_event_device *levt = this_cpu_ptr(&the_cev);
memcpy(levt, &lapic_clockevent, sizeof(*levt));
if (boot_cpu_has(X86_FEATURE_TSC_DEADLINE_TIMER)