【原创】解BUG-xenomai内核与linux内核时间子系统之间存在漂移

版权声明:本文为本文为博主原创文章,转载请注明出处。如有问题,欢迎指正。博客地址:https://www.cnblogs.com/wsg1100/

一、问题起源

何为漂移?举个例子两颗32.768kHz晶振\(C_1\)\(C_2\),由于制造工艺原因或者使用时温度、辅助元件参数等影响,与他们的实际频率一定不是相同的,与32.768kHz有不同的偏差,假如\(C_1\)实际使用时频率32.766kHz,\(C_2\)实际频率32.770kHz。

假如有那么两个电子手表,使用32.768kHz晶振,每来一个脉冲寄存器计数加1,我们通过这个电路来获取时间,这样计算1s的时间寄存器里应该是32.768kHz(我们认为我们的晶振是没问题的嘛)。好现在用\(C_1\)来计数就会使我们得到的时间比真实1S长\(\frac{1}{2000}=0.0005\)秒,这样下来这个手表会越走越快,即与真实时间的偏移越来越大。同样\(C_2\)得到的时间比真实1S短\(0.0005\)秒,越走越慢。

两个手表在它们的计时周期(这里举例1秒)存在的偏差就是漂移。

X86平台上,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上两个内核之间的时间漂移。如下:

image-202007192057651

回到问题本身,xenomai来常用来运行ethercat主站(这里使用的是acontis EtherCAT主站),DC模式下以主站作为参考时钟时运行时,出现的现象是从站一直同步不上,导致整个EtherCAT无法正常启动。在使用从站作为参考时钟时出现同步频繁同步校准;

下面分析问题:主站由xenomai实时调度,ethercat主站工作过程中使用的是xenomai时间子系统根据底层硬件timer计算得到的时间,ethercat主站在这个时间上去同步参考时钟,增加或减少偏移量。先不管硬件timer与真实时间的偏移,非常小先忽略,这不是重点,两个内核都使用这个硬件timer,现在出现的问题是两个内核对同一硬件timer的度量不一致,才会存在漂移。

由此可以推断出xenomai时间子系统对硬件timer的度量计算有问题,下面开始从一步步挖掘分析。

二、 clocktest工具分析

clocktest工具主要用于测试xenomai 时钟(CLOCK_REALTIMECLOCK_MONOTONICCLOCK_MONOTONIC_RAWCLOCK_HOST_REALTIME、coreclk默认CLOCK_REALTIME),相对于Linux绝对时钟CLOCK_MONOTONIC之间的漂移,clocktest首先为每个CPU创建一个线程cpu_thread,并固定到相应CPU上执行,cpu_thread测试原理为:

  1. 找一个时间点作为测试起始点,此时xenomai时间表示为first_clock,Linux绝对时钟时间表示为first_tod,它们均为一个数,单位纳秒ns。

  2. 让测量任务睡眠,睡眠时间是一个范围的随机数,睡眠范围为:[1000000, 200000)纳秒(这里的睡眠时间至少1000000是因为读取linux时钟的函数(SYS_gettimeofday)精度只能读取到us级,而xenomai读取到的时间为ns级,为了使差距与us对齐,所以至少经过1ms,简而言之计算周期1ms单位的漂移)。

  3. 读取睡眠后的各自时钟的计数值,读取xenomai时钟读取到的值为clock_val,Linux时间计数值为first_tod同样的时间段,xenomai时钟计数为clock_val-first_clock,Linux时间计数值tod_val-first_tod;这个时间段的偏移率为:

    \[\frac{clock\_val-first\_clock}{tod\_val-first\_tod}-1 \]

    image-20200702185626887

三、 读linux时钟时间

在clock工具中读取Linux参考时钟时间使用系统调用syscall(SYS_gettimeofday, &tv, NULL);

image-20200702185737947

image-20200702185743615

系统函数do_gettimeofday()读取全局时钟timekeeper的值xtime_sec,然后加上系统上一个tick到此时的纳秒数nsecensece是直接调用timekeeper使用的clocksouse对应的读函数读取clocksouse counter计算得到的,也可看到底层读取到的精度是纳秒级的,只不过在上一个函数将精度丢弃了。

image-20200702185819000

image-20200702185825121

四、 读xenomai时钟时间

在clock工具中读取xenomai时钟时间的函数是static inline uint64_t read_clock(clockid_t clock_id);

image-20200702185859839

read_clock()函数调用系统调用函数clock_gettime(),这是一个POSIX标准函数,在xenomai中kernel\xenomai\posix\clock.c实现如下:

image-20200702185924171

image-20200702185930882

clock_gettime()继续调用__cobalt_clock_gettime()来获取时钟,在colocktest中传入的参数是:CLOCK_REALTIME。进而调用内核函数xnclock_read_realtime (struct xnclock *clock)读取时间,再通过ns2ts函数将读取的到的纳秒转换为需要的timespec结构体中的tv_sectv_nsec

image-20200702190018198

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()函数:

image-20200702190059340

xnclock_core_read_monotonic()函数中由xnclock_core_ticks_to_ns()函数将xnclock_core_read_raw()函数返回的TSC CPU tick数转换为纳秒ns返回,这就是读取的xenomai时钟时间。怎样获取CPU的TSC值呢?在X86处理器中有一条指令rdtsc用于读取TSC值 。

image-20200702190133392

image-20200702190139364

到这,整个xenomai时间读取流程完了,就是读取TSC的值,没其他的了,看似没有什么问题。难道真的x86中的TSC不准?注意到读取的TSC数值还需要转换才能得到时间,转换函数xnarch_llmulshft(),涉及到这两个变量tsc_scale,tsc_shift,怀疑是这两个值有问题,继续分析,那这两个值是干嘛用的?

image-20200702190207700

当已知频率F,要将A个cycles数转换成纳秒,具体公式如下:

\[转换后的纳秒数 =\frac{A}{F}*1000000000 \]

这样的转换公式需要除法,绝大部分的CPU都有乘法器,但是有些处理器是不支持除法,虽然我们无法将除法操作的代码编译成一条除法的汇编指令,但是也可以用代码库中的其他运算来取代除法。这样做的坏处就是性能会受影响。把1/F变成浮点数,这样就可以去掉除法了,但是又引入了浮点运算,kernel是不建议使用浮点运算的。解决方案很简单,使用移位操作,具体可以参考clocksource_cyc2ns的操作:

image-20200702190518788

通过TSC的xnclock_core_read_raw()函数获取了tick数目,乘以mult这个因子然后右移shift个bit就可以得到纳秒数。这样的操作虽然性能比较好,但是损失了精度(通过另外验证下面代码算出的值,以这台机器的2700M算出的值,带入2700Mcycle得1000000230ns,有200纳秒左右的偏移),还是那句话,设计是平衡的艺术,看你自己的取舍。

tsc_scale,tsc_shift在哪里计算的呢?

具体计算在xnarch_init_llmulshft()函数中计算:

image-20200702190602567

如何获取最佳的tsc_scaletsc_shift组合?当一个公式中有两个可变量的时候,最好的办法就是固定其中一个,求出另外一个,然后带入约束条件进行检验。我们首先固定shift这个参数。mult这个因子一定是越大越好,mult越大也就是意味着shift越大。当然shift总有一个起始值,我们设定为32bit,因此tsc_shift从31开始搜索,看看是否满足最大时间范围的要求。如果满足,那么就找到最佳的multshift组合,否则要tsc_shift递减,进行下一轮搜索。

先考虑如何计算mult值。根据公式(cycles * mult) >> shift可以得到ns数,由此可以得到计算mult值的公式:

\[mult=\frac{ns<<shift}{cycles} \]

如果我们设定ns数是10^9纳秒(也就是1秒)的话,cycles数目就是频率值。因此上面的公式可以修改为:

\[mult=((10^9<< freq)) \]

image-20200702191058424

看看上面的公式,再对照代码,一切就很清晰了。

那到这自然也就想到,这个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,他的定义如下;

image-20200702191326526

接着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的值。这些信息在下面的初始化中要用到。

image-20200702191450456

接下来将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()函数了。

image-20200702191609175

image-20200702191613733

需要注意的是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在哪里获取到的。其他的不在这里不分析。

image-20200702191733579

六、 TSC init

\arch\x86\kernel\tsc.c

image-20200702191841659

这里很明了了,cpu_khtsc_khz是两个值,linux使用的是tsc_khz,即产生事件的硬件TSC的真实频率,而xenomai使用cpu_khz去算tsc与纳秒的转换因子, 如果cpu_khtsc_khz相等那没有问题,但通过添加调试输出,在这个平台(Kaby Lake-U)上,这两个值是不等的。

image-20200702191953017

image-20200702191959548

刚好下面的条件判断没有对tsc_khzcpu_khz不相等做处理。

image-20200702192026940

这台机器上导致计算tsc_scale, tsc_shift时使用的是2700Mhz,而TSC的频率是2712MHZ,用2700MHZ得来的tsc_scale, tsc_shift去转换2712MHZ产生的cycles当然不对,每秒就会有12M的漂移,也就是每周期漂移\(\frac{12MHZ}{2700MHZ}=0.004444444\)秒,转换为(微秒/秒)就是\(4444.4444(us/s)\).与机器上实际测试相符:

image-20200721170932224

解决办法(二选一):

修改Linux内核代码,当tsc_khz不为0时,直接 cpu_khz=tsc_khz

image-20200702192048023

或者对xenomai代码修改,统一使用tsc_khz,而不是cpu_khz:

image-20210130141717090

修改后clocktest测试如下:

image-202007192057651

附: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表示lapic-timer支持deadline事件触发;

关于xenomai的时间子系统后续会有整理分析文章,敬请关注!!!。

这个问题可能与具体X86平台相关,或是与BIOS有关;后面一直没有使用4.14及以上的内核,不知道现在还有没这个问题。大家可以看看,再提个issue或者啥的.....

2021.2.2-- 最近发现该问题已被社区修复。

posted @ 2020-09-13 18:18  沐多  阅读(2058)  评论(9编辑  收藏  举报