版权声明:本文为本文为博主原创文章,转载请注明出处https://blog.csdn.net/qq_22654551。如有问题,欢迎指正。
一、问题起源
何为漂移?举个例子两颗32.768kHz晶振 C 1 C_1 C1和 C 2 C_2 C2,由于制造工艺原因或者使用时温度、辅助元件参数等影响,与他们的实际频率一定不是相同的,与32.768kHz有不同的偏差,假如 C 1 C_1 C1实际使用时频率32.766kHz, C 2 C_2 C2实际频率32.770kHz。
假如有那么两个电子手表,使用32.768kHz晶振,每来一个脉冲寄存器计数加1,我们通过这个电路来获取时间,这样计算1s的时间寄存器里应该是32.768kHz(我们认为我们的晶振是没问题的嘛)。好现在用 C 1 C_1 C1来计数就会使我们得到的时间比真实1S长 1 2000 = 0.0005 \frac{1}{2000}=0.0005 20001=0.0005秒,这样下来这个手表会越走越快,即与真实时间的偏移越来越大。同样 C 2 C_2 C2得到的时间比真实1S短 0.0005 0.0005 0.0005秒,与真实时间的偏移越来越小。
两个手表在它们的计时周期(这里举例1秒)存在的偏差就是漂移。
linux 4.4.xx之后的版本构建的xenomai,出现linux内核与xenomai内核两者时钟存在漂移,打xenomai补丁之后,有两个内核分别有各自的时间子系统,只不过xenomai掌管着底层的硬件timer-event
的中断触发设置和处理,linux时间子系统的触发源就退化为xenomai时间子系统管理的软件timer了(linux是xenomai的idle任务嘛,当然要xenomai来提供时钟),本质上它们还是使用同一个硬件timer源。
此问题解决时本人还未阅读xenomai的时间子系统相关源码,所以其中有些解释现在看起来·只见树木不见森林·,懒得改了,关于xenomai的时间子系统后续会有分析文章,敬请关注!!!。
构建xenomai系统后,linux内核与xenomai内核两个时间子系统之间的时间漂移可通过xenomai库编译出的工具clocktest
来查看,其中的dirft
列就是表示该cpu上两个内核之间的时间漂移。如下:
回到问题本身,xenomai来常用来运行ethercat主站(这里使用的是acontis EtherCAT主站),DC模式下以主站作为参考时钟时运行时,出现的现象是启动时从站一直同步不上,导致整个EtherCAT无法正常启动,在使用从站作为参考时钟模式下时出现同步频繁同步校准;
下面分析问题:主站由xenomai实时调度,ethercat主站工作过程中使用的是xenomai时间子系统根据底层硬件timer计算得到的时间,ethercat主站在这个时间上去同步参考时钟,增加或减少偏移量。先不管硬件timer与真实时间的偏移,非常小先忽略,这不是重点,两个内核都使用这个硬件timer,现在出现的问题是两个内核对同一硬件timer的度量不一致,才会存在漂移。
由此可以推断出xenomai时间子系统对硬件timer的度量计算有问题,下面开始从一步步挖掘分析。
二、 clocktest工具分析
clocktest
工具主要用于测试xenomai 时钟(CLOCK_REALTIME
、CLOCK_MONOTONIC
、CLOCK_MONOTONIC_RAW
、CLOCK_HOST_REALTIME
、coreclk默认CLOCK_REALTIME
),相对于Linux绝对时钟CLOCK_MONOTONIC
之间的漂移,clocktest首先为每个CPU创建一个线程cpu_thread,并固定到相应CPU上执行,cpu_thread测试原理为:
-
找一个时间点作为测试起始点,此时xenomai时间表示为
first_clock
,Linux绝对时钟时间表示为first_tod
,它们均为一个数,单位纳秒ns。 -
让测量任务睡眠,睡眠时间是一个范围的随机数,睡眠范围为:[1000000, 200000)纳秒(这里的睡眠时间至少1000000是因为读取linux时钟的函数(
SYS_gettimeofday
)精度只能读取到us级,而xenomai读取到的时间为ns级,为了使差距与us对齐,所以至少经过1ms,简而言之计算周期1ms单位的漂移)。 -
读取睡眠后的各自时钟的计数值,读取xenomai时钟读取到的值为
clock_val
,Linux时间计数值为first_tod
同样的时间段,xenomai时钟计数为clock_val
-first_clock
,Linux时间计数值tod_val
-first_tod
;这个时间段的偏移率为:c l o c k _ v a l − f i r s t _ c l o c k t o d v a l − f i r s t t o d \frac{clock\_val-first\_clock}{tod_val-first_tod} todval−firsttodclock_val−first_clock
三、 读linux时钟时间
在clock工具中读取Linux参考时钟时间使用系统调用syscall(SYS_gettimeofday, &tv, NULL)
;
系统函数do_gettimeofday()
读取全局时钟timekeeper的值xtime_sec
,然后加上系统上一个tick到此时的纳秒数nsece
,nsece
是直接调用timekeeper使用的clocksouse对应的读函数读取clocksouse counter计算得到的,也可看到底层读取到的精度是纳秒级的,只不过在上一个函数将精度丢弃了。
四、 读xenomai时钟时间
在clock工具中读取xenomai时钟时间的函数是static inline uint64_t read_clock(clockid_t clock_id)
;
read_clock()
函数调用系统调用函数clock_gettime()
,这是一个POSIX标准函数,在xenomai中kernel\xenomai\posix\clock.c
实现如下:
clock_gettime()
继续调用__cobalt_clock_gettime()
来获取时钟,在colocktest中传入的参数是:CLOCK_REALTIME
。进而调用内核函数xnclock_read_realtime (struct xnclock *clock)
读取时间,再通过ns2ts函数将读取的到的纳秒转换为需要的timespec结构体中的tv_sec
和tv_nsec
:
xnclock_read_realtime
返回 nkclock的时间加上一个与wallclock的偏移(clock->wallclock_offset), (nkclock是xenomai的时钟源,类型为struct xnclock
,当没有使用外部时钟时,时钟使用X86处理器中的TSC时钟(早期X86CPU中TSC与CPU的频率有关,现在的CPU TSC频率一般是固定的),当使用外部时钟作为xenomai的时钟时是另外一回事)。
xnclock_read_monotonic()
最终调用xnclock_core_read_monotonic()
函数:
xnclock_core_read_monotonic()
函数中由xnclock_core_ticks_to_ns()
函数将xnclock_core_read_raw()
函数返回的TSC CPU tick数转换为纳秒ns返回,这就是读取的xenomai时钟时间。怎样获取CPU的TSC值呢?在X86处理器中有一条指令rdtsc
用于读取TSC值 。
到这,整个xenomai时间读取流程完了,就是读取TSC的值,没其他的了,看似没有什么问题。难道真的x86中的TSC不准?注意到读取的TSC数值还需要转换才能得到时间,转换函数xnarch_llmulshft()
,涉及到这两个变量tsc_scale,tsc_shift
,怀疑是这两个值有问题,继续分析,那这两个值是干嘛用的?
当已知频率F,要将A个cycles数转换成纳秒,具体公式如下:
转 换 后 的 纳 秒 数 = A F ∗ 1000000000 转换后的纳秒数 =\frac{A}{F}*1000000000 转换后的纳秒数=FA∗1000000000
这样的转换公式需要除法,绝大部分的CPU都有乘法器,但是有些处理器是不支持除法,虽然我们无法将除法操作的代码编译成一条除法的汇编指令,但是也可以用代码库中的其他运算来取代除法。这样做的坏处就是性能会受影响。把1/F变成浮点数,这样就可以去掉除法了,但是又引入了浮点运算,kernel是不建议使用浮点运算的。解决方案很简单,使用移位操作,具体可以参考clocksource_cyc2ns
的操作:
通过TSC的xnclock_core_read_raw()
函数获取了tick数目,乘以mult
这个因子然后右移shift
个bit就可以得到纳秒数。这样的操作虽然性能比较好,但是损失了精度(通过另外验证下面代码算出的值,以这台机器的2700M算出的值,带入2700Mcycle得1000000230ns,有200纳秒左右的偏移),还是那句话,设计是平衡的艺术,看你自己的取舍。
那tsc_scale,tsc_shift
在哪里计算的呢?
具体计算在xnarch_init_llmulshft()
函数中计算:
如何获取最佳的tsc_scale
和tsc_shift
组合?当一个公式中有两个可变量的时候,最好的办法就是固定其中一个,求出另外一个,然后带入约束条件进行检验。我们首先固定shift这个参数。mult
这个因子一定是越大越好,mult
越大也就是意味着shift
越大。当然shift
总有一个起始值,我们设定为32bit,因此tsc_shift
从31开始搜索,看看是否满足最大时间范围的要求。如果满足,那么就找到最佳的mult
和shift
组合,否则要tsc_shift
递减,进行下一轮搜索。
先考虑如何计算mult
值。根据公式(cycles * mult) >> shift
可以得到ns数,由此可以得到计算mult
值的公式:
m u l t = n s < < s h i f t c y c l e s mult=\frac{ns<<shift}{cycles} mult=cyclesns<<shift
如果我们设定ns数是10^9纳秒(也就是1秒)的话,cycles数目就是频率值。因此上面的公式可以修改为:
m u l t = ( ( 1 0 9 < < f r e q ) ) mult=((10^9<< freq)) mult=((109<<freq))
看看上面的公式,再对照代码,一切就很清晰了。
那到这自然也就想到,这个freq值就是TSC对应的CPU的频率值了。那上面的函数是由谁调用计算的?CPU的频率值freq在哪获取的?这些值在xenomai核初始化时计算。
五、xenomai xnclock初始化
在xenomai核启动函数xenomai_init()
中,由mach_setup()
函数完成xenomai域相关定时器、中断、时钟设置。在mach_setup()
函数中首先调用ipipe_select_timers()
从全局timers链表中为每一个CPU选择一个具有最高评级的clock_event_device
作为该cpu的percpu_timer
。而timers
是每一个clock_event_device
在register的时候, 由ipipe_host_timer_register()
将该clock_event_device
添加到链表timers
上的。
当一个CPU找到一个合适的clock_event_device
的时候,就回调用install_pcpu_timer()
设置该clock_event_device
为该CPU的percpu_timer
,并配置该ipipe_timer
频率与CPU频率的转换因子(参数c2t_integ
,c2t_frac
, ipipe_timer_set
中用到,而ipipe_timer_set
常被__xnsched_run
调用,推测与任务时间片计算有关,有时间再来分析),同时设置CPU的ipipe_percpu.hrtimer_irq
为该timer
的中断号。而这里使用的 CPU频率值为__ipipe_hrclock_freq
,他的定义如下;
接着mach_setup()
中调用ipipe_get_sysinfo(&sysinfo)
获取系统的信息,系统online的cpu数,cpu的频率,这个频率也是使用上面cpu_khz
的值。另外将0号cpu的hrtimer_irq
作为系统hrtimer的中断号,并且设置sys_hrtimer_freq
的频率(这个频率是具体percpu_timer
的频率lapic-timer或者HPET),同样sys_hrclock_freq
也是**cpu_khz
**的值。这些信息在下面的初始化中要用到。
接下来将cobalt_pipeline.timer_freq
设置为timerfreq_arg
也就是sysinfo
中的sys_hrtimer_freq
。
cobalt_pipeline.clock_freq
为sysinfo中的那个**cpu_khz
**得来的sys_hrclock_freq
。
下面注册两个虚拟中断到ipipe,一个为cobalt_pipeline.apc_virq
,链接到root_domain
,处理linux挂起恢复。另一个为cobalt_pipeline.escalate_virq
,注册域为xnsched_realtime_domain
,handler为__xnsched_run_handler
,一看这就是与xenomai调度相关的。
下面就是这里主要的初始化xnclock的xnclock_init()
函数了。
需要注意的是xnclock_init()
函数传的参数是cobalt_pipeline.clock_freq
,也就是那个**cpu_khz
**得来cobalt_pipeline.clock_freq
。
首先第一步做的就是求转换因子tsc_scale
, tsc_shift
,执行xnclock_update_freq()
,到这就是熟悉的xnarch_init_llmulshft(1000000000, freq, &tsc_scale, &tsc_shift)
,在上面的分析中提到过,下面去看**cpu_khz
**在哪里获取到的。其他的不在这里不分析。
六、 TSC init
\arch\x86\kernel\tsc.c
这里很明了了,cpu_kh
和tsc_khz
是两个值,linux使用的是tsc_khz
,即产生事件的硬件TSC的真实频率,而xenomai使用cpu_khz
去算tsc与纳秒的转换因子, 如果cpu_kh
和tsc_khz
相等那没有问题,但通过添加调试输出,在这个平台上(Kaby Lake-U),这两个值是不等的。
刚好下面的条件判断没有对**tsc_khz
与cpu_khz
**不相等做处理。
这台机器上导致计算tsc_scale
, tsc_shift
时使用的是2700Mhz,而TSC的频率是2712MHZ,用2700MHZ得来的tsc_scale
, tsc_shift
去转换2712MHZ产生的cycles当然不对,每秒就会有12M的漂移,也就是每周期漂移
12
M
H
Z
2700
M
H
Z
=
0.004444444
\frac{12MHZ}{2700MHZ}=0.004444444
2700MHZ12MHZ=0.004444444秒,转换为(微秒/秒)就是
4444.4444
(
u
s
/
s
)
4444.4444(us/s)
4444.4444(us/s).与机器上实际测试相符:
解决办法(二选一):
对Linux代码进行修改:当tsc_khz
不为0时,直接cpu_khz=tsc_khz
或者对xenomai代码修改,统一使用tsc_khz,而不是cpu_khz:
修改后clocktest测试如下:
附:xenomai内核的一些时钟信息:
$cat /proc/xenomai/clock/coreclok
gravity: irq=100 kernel=1341 user=1341
devices: timer=lapic-deadline, clock=tsc
status: on
setup: 100
ticks: 443638843357 (0067 4aef87dd)
修改后:
$cat /proc/xenomai/clock/coreclok
gravity: irq=99 kernel=1334 user=1334
devices: timer=lapic-deadline, clock=tsc
status: on
setup: 99
ticks: 376931548560 (0057 c2defd90)
lapic-deadline 是上面解析的CPU0 的percpu_timer,deadline表示TSC支持deadline事件触发;
关于xenomai的时间子系统后续会有整理分析文章,敬请关注!!!。
这个问题可能与具体X86平台相关,或是与BIOS有关;后面一直没有使用4.14及以上的内核,不知道现在还有没这个问题。大家可以看看,再提个issue或者啥的…
2021.2.2-- 最近发现该问题已被社区修复。