前面提到了Linux下的时间相关的硬件。TSC PIT,HPET,ACPI_PM,这些硬件以一定的频率产生时钟中断,来帮助我们计时。Linux为了管理这些硬件,抽象出来clocksource。
structclocksource{
/*
*Hotpathdata,fitsinasinglecachelinewhenthe
*clocksourceitselfiscachelinealigned.
*/
cycle_t(*read)(structclocksource*cs);
cycle_tcycle_last;
cycle_tmask;
u32mult;
u32shift;
u64max_idle_ns;
u32maxadj;
#ifdefCONFIG_ARCH_CLOCKSOURCE_DATA
structarch_clocksource_dataarchdata;
#endif
constchar*name;
structlist_headlist;
intrating;
int(*enable)(structclocksource*cs);
void(*disable)(structclocksource*cs);
unsignedlongflags;
void(*suspend)(structclocksource*cs);
void(*resume)(structclocksource*cs);
/*private:*/
#ifdefCONFIG_CLOCKSOURCE_WATCHDOG
/*Watchdogrelateddata,usedbytheframework*/
structlist_headwd_list;
cycle_tcs_last;
cycle_twd_last;
#endif
}____cacheline_aligned;
这些参数当中,比较重要的是rating,shift,mult。其中rating在上一篇博文提到了:
1--99: 不适合于用作实际的时钟源,只用于启动过程或用于测试;
100--199:基本可用,可用作真实的时钟源,但不推荐;
200--299:精度较好,可用作真实的时钟源;
300--399:很好,精确的时钟源;
400--499:理想的时钟源,如有可能就必须选择它作为时钟源;
我们基本在前面看到:
include/linux/acpi_pmtmr.h
------------------------------------------
#definePMTMR_TICKS_PER_SEC3579545
drivers/clocksource/acpi_pm.c
---------------------------------------------
staticstructclocksourceclocksource_acpi_pm={
.name="acpi_pm",
.rating=200,
.read=acpi_pm_read,
.mask=(cycle_t)ACPI_PM_MASK,
.mult=0,/*tobecalculated*/
.shift=22,
.flags=CLOCK_SOURCE_IS_CONTINUOUS,
};
dmesgoutput
------------------------
[0.664201]hpet0:8comparators,64-bit14.318180MHzcounter
arch/86/kernel/hpet.c
--------------------------------
staticstructclocksourceclocksource_hpet={
.name="hpet",
.rating=250,
.read=read_hpet,
.mask=HPET_MASK,
.flags=CLOCK_SOURCE_IS_CONTINUOUS,
.resume=hpet_resume_counter,
#ifdefCONFIG_X86_64
.archdata={.vclock_mode=VCLOCK_HPET},
#endif
};
dmesgoutput:
-----------------------------
[0.004000]Detected2127.727MHzprocessor.
arch/x86/kernel/tsc.c
--------------------------------------
staticstructclocksourceclocksource_tsc={
.name="tsc",
.rating=300,
.read=read_tsc,
.resume=resume_tsc,
.mask=CLOCKSOURCE_MASK(64),
.flags=CLOCK_SOURCE_IS_CONTINUOUS|
CLOCK_SOURCE_MUST_VERIFY,
#ifdefCONFIG_X86_64
.archdata={.vclock_mode=VCLOCK_TSC},
#endif
};
从上面可以看到,acpi_pm,hpet tsc的rating分别是200,250,300,他们的rating基本是和他们的frequency符合,TSC以2127.727MHz的频率技压群雄,等级rating=300最高,被选择成current_clocksource:
root@manu:~#cat/sys/devices/system/clocksource/clocksource0/available_clocksource
tschpetacpi_pm
root@manu:~#cat/sys/devices/system/clocksource/clocksource0/current_clocksource
tsc
除此外,还有两个参数shift和mult,这两个参数是干啥的呢?
我们想一下,假如我们需要给你个以一定频率输出中断的硬件,你如何计时?比如我有一个频率是1000Hz的硬件,当前时钟源计数是3500,过了一段时间,我抬头看了下时钟源计数至是5500,过去了2000cycles,我就知道了过去了2000/1000 =2 second。
times_elapse=cycles_interval/frequency
从上面的例子中,我抬头看了下当前计数值这个肯定是瞎掰了,实际上要想获取时钟源还是需要和硬件打交道的。在clocksource中有一个成员变量是read,这个就是一个时钟源注册的时候,提供的一个函数,如果你想获得我的当前计数值,请调用这个read 函数。以TSC时钟为例:
staticstructclocksourceclocksource_tsc={
.name="tsc",
.rating=300,
.read=read_tsc,
.resume=resume_tsc,
.mask=CLOCKSOURCE_MASK(64),
.flags=CLOCK_SOURCE_IS_CONTINUOUS|
CLOCK_SOURCE_MUST_VERIFY,
#ifdefCONFIG_X86_64
.archdata={.vclock_mode=VCLOCK_TSC},
#endif
};
/*---------arch/x86/kernel/tsc.c-------------------*/
staticcycle_tread_tsc(structclocksource*cs)
{
cycle_tret=(cycle_t)get_cycles();
returnret>=clocksource_tsc.cycle_last?
ret:clocksource_tsc.cycle_last;
}
/*-------arch/x86/include/asm/tsc.h----------------------*/
staticinlinecycles_tget_cycles(void)
{
unsignedlonglongret=0;
#ifndefCONFIG_X86_TSC
if(!cpu_has_tsc)
return0;
#endif
rdtscll(ret);
returnret;
}
/*------arch/x86/include/asm/msr.h-----------------*/
#definerdtscll(val)\
((val)=__native_read_tsc())
static__always_inlineunsignedlonglong__native_read_tsc(void)
{
DECLARE_ARGS(val,low,high);
asmvolatile("rdtsc":EAX_EDX_RET(val,low,high));
returnEAX_EDX_VAL(val,low,high);
}
根据这个脉络,我们知道,最终就是rdtsc这条指令来获取当前计数值cycles。
扯了半天read这个成员变量,可以回到shift和mult了。其实shift和mult是为了解决下面这个公式的:
times_elapse=cycles_interval/frequency
就像上面的公式,有频率就足以计时了。为啥弄出来个shift和mult。原因在于kernel搞个除法不太方便,必须转化乘法和移位。Kernel中有很多这种把除法转化成乘法的样例。那么公式变成了:
times_elapse=cycles_interval*mult>>shift
Kernel用乘法+移位来替换除法:根据cycles来计算过去了多少ns。
/**
*clocksource_cyc2ns-convertsclocksourcecyclestonanoseconds
*@cycles:cycles
*@mult:cycletonanosecondmultiplier
*@shift:cycletonanosecondpisor(poweroftwo)
*
*Convertscyclestonanoseconds,usingthegivenmultandshift.
*
*XXX-Thiscouldusesomemult_lxl_ll()asmoptimization
*/
staticinlines64clocksource_cyc2ns(cycle_tcycles,u32mult,u32shift)
{
return((u64)cycles*mult)>>shift;
}
单纯从精度上讲,肯定是mult越大越好,但是计算过程可能溢出,所以mult也不能无限制的大,这个计算中有个magic number 600 :
void__clocksource_updatefreq_scale(structclocksource*cs,u32scale,u32freq)
{
u64sec;
/*
*Calcthemaximumnumberofsecondswhichwecanrunbefore
*wrappingaround.Forclocksourceswhichhaveamask>32bit
*weneedtolimitthemaxsleeptimetohaveagood
*conversionprecision.10minutesisstillareasonable
*amount.Thatresultsinashiftvalueof24fora
*clocksourcewithmask>=40bitandf>=4GHz.Thatmapsto
*~0.06ppmgranularityforNTP.Weapplythesame12.5%
*marginaswedoinclocksource_max_deferment()
*/
sec=(cs->mask-(cs->mask>>3));
do_p(sec,freq);
do_p(sec,scale);
if(!sec)
sec=1;
elseif(sec>600&&cs->mask>UINT_MAX)
sec=600;
clocks_calc_mult_shift(&cs->mult,&cs->shift,freq,
NSEC_PER_SEC/scale,sec*scale);
/*
*forclocksourcesthathavelargemults,toavoidoverflow.
*Sincemultmaybeadjustedbyntp,addansafetyextramargin
*
*/
cs->maxadj=clocksource_max_adjustment(cs);
while((cs->mult+cs->maxadj<cs->mult)
||(cs->mult-cs->maxadj>cs->mult)){
cs->mult>>=1;
cs->shift--;
cs->maxadj=clocksource_max_adjustment(cs);
}
cs->max_idle_ns=clocksource_max_deferment(cs);
}
这个600的意思是600秒,表示的Timer两次计算当前计数值的差不会超过10分钟。主要考虑的是系统进入IDLE状态之后,时间信息不会被更新,10分钟内只要退出IDLE,clocksource还是可以成功的转换时间。当然了,最后的这个时间不一定就是10分钟,它由clocksource_max_deferment计算并将结果存储在max_idle_ns中。
筒子比较关心的问题是如何计算,精度如何,其实我不太喜欢这种计算,Kernel总是因为某些原因把代码写的很蛋疼。反正揣摩代码意图要花不少时间,收益嘛其实也不太大.如何实现我也不解释了,我以TSC为例子我评估下这种mult+shift的精度。
#include<stdio.h>
#include<stdlib.h>
typedefunsignedintu32;
typedefunsignedlonglongu64;
#defineNSEC_PER_SEC1000000000L
void
clocks_calc_mult_shift(u32*mult,u32*shift,u32from,u32to,u32maxsec)
{
u64tmp;
u32sft,sftacc=32;
/*
**Calculatetheshiftfactorwhichislimitingtheconversion
**range:
**/
tmp=((u64)maxsec*from)>>32;
while(tmp){
tmp>>=1;
sftacc--;
}
/*
**Findtheconversionshift/multpairwhichhasthebest
**accuracyandfitsthemaxsecconversionrange:
**/
for(sft=32;sft>0;sft--){
tmp=(u64)to<<sft;
tmp+=from/2;
//do_p(tmp,from);
tmptmp=tmp/from;
if((tmp>>sftacc)==0)
break;
}
*mult=tmp;
*shift=sft;
}
intmain()
{
u32tsc_mult;
u32tsc_shift;
u32tsc_frequency=2127727000/1000;//TSCfrequency(KHz)
clocks_calc_mult_shift(&tsc_mult,&tsc_shift,tsc_frequency,NSEC_PER_SEC/1000,600*1000);//NSEC_PER_SEC/1000是因为TSC的注册是clocksource_register_khz
fprintf(stderr,"mult=%dshift=%d\n",tsc_mult,tsc_shift);
return0;
}
600是根据TSC clocksource的MASK算出来的的入参,感兴趣可以自己推算看下结果:
mult=7885042shift=24
root@manu:~/code/c/self/time#python
Python2.7.3(default,Apr102013,05:46:21)
[GCC4.6.3]onlinux2
Type"help","copyright","credits"or"license"formoreinformation.
>>>(2127727000*7885042)>>24
1000000045L
>>>
我们知道TSC的frequency是2127727000Hz,如果cycle走过2127727000,就意味过去了1秒,或者说10^9(us)。按照我们的算法得出的时间是1000000045us.。这个误差是多大呢,每走10^9秒,误差是45秒,换句话说,运行257天,产生1秒的计算误差。考虑到NTP的存在,这个运算精度还可以了。
接下来是注册和各大clocksource PK。
各大clocksource会调用clocksource_register_khz或者clocksource_register_hz来注册。
HPET(arch/x86/kernel/hpet)
----------------------------------------
hpet_enable
|_____hpet_clocksource_register
|_____clocksource_register_hz
TSC(arch/x86/kernel/tsc.c)
----------------------------------------
device_initcall(init_tsc_clocksource);
init_tsc_clocksource
|_____clocksource_register_khz
ACPI_PM(drivers/cloclsource/acpi_pm.c)
-------------------------------------------
fs_initcall(init_acpi_pm_clocksource);
init_acpi_pm_clocksource
|_____clocksource_register_hz
最终都会调用__clocksource_register_scale.
int__clocksource_register_scale(structclocksource*cs,u32scale,u32freq)
{
/*Initializemult/shiftandmax_idle_ns*/
__clocksource_updatefreq_scale(cs,scale,freq);
/*Addclocksourcetotheclcoksourcelist*/
mutex_lock(&clocksource_mutex);
clocksource_enqueue(cs);
clocksource_enqueue_watchdog(cs);
clocksource_select();
mutex_unlock(&clocksource_mutex);
return0;
}
第一函数是__clocksource_updatefreq_scale,计算shift,mult还有max_idle_ns,前面讲过了。
clocksource_enqueue是将clocksource链入全局链表,根据的是rating,rating高的放前面。
clocksource_select会选择最好的clocksource记录在全局变量curr_clocksource,同时会通知timekeeping,切换最好的clocksource会有内核log:
manu@manu:~$dmesg|grepSwitching
[0.673002]Switchingtoclocksourcehpet
[1.720643]Switchingtoclocksourcetsc
clocksource_enqueue_watchdog会将clocksource挂到watchdog链表。watchdog顾名思义,监控所有clocksource:
#defineWATCHDOG_INTERVAL(HZ>>1)
#defineWATCHDOG_THRESHOLD(NSEC_PER_SEC>>4)
如果0.5秒内,误差大于0.0625s,表示这个clocksource精度极差,将rating设成0。