前面提到了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。
畅学电子







