【原创】解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上两个内核之间的时间漂移。如下:

回到问题本身,xenomai来常用来运行ethercat主站,主站DC模式下同步运行时,出现的现象是主站本地时间永远无法与参考时钟同步,导致每周期主站都需要读回参考时钟进行调整;
下面分析问题:主站由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;这个时间段的偏移率为:\[\frac{clock\_val-first\_clock}{tod_val-first_tod}
\]
三、 读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数转换成纳秒,具体公式如下:
\]
这样的转换公式需要除法,绝大部分的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值的公式:
\]
如果我们设定ns数是10^9纳秒(也就是1秒)的话,cycles数目就是频率值。因此上面的公式可以修改为:
\]

看看上面的公式,再对照代码,一切就很清晰了。
那到这自然也就想到,这个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是两个值,而xenomai使用cpu_khz去算tsc与纳秒的转换因子,如果cpu_kh和tsc_khz相等那没有问题,但通过添加调试输出,这两个值是不等的。


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

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

解决办法:
当tsc_khz不为0时,直接 cpu_khz=tsc_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表示lapic-timer支持deadline事件触发;
关于xenomai的时间子系统后续会有整理分析文章,敬请关注!!!。
后面一直没有使用4.14及以上的内核,不知道现在还有没这个问题。大家可以看看,再提个issue或者啥的.....
【原创】解BUG-xenomai内核与linux内核时间子系统之间存在漂移的更多相关文章
- Android内核和Linux内核的区别
1.Android系统层面的底层是Linux,并且在中间加上了一个叫做Dalvik的Java虚拟机,从表面层看是Android运行库.每个Android应用都运行在自己的进程上,享有Dalvik虚拟机 ...
- 内核操作系统Linux内核变迁杂谈——感知市场的力量
本篇文章个人在青岛游玩的时候突然想到的...今天就有想写几篇关于内核操作系统的博客,所以回家到以后就奋笔疾书的写出来发表了 Jack:什么是操作系统? 我:你买了一台笔记本,然后把整块硬盘彻底格式化, ...
- Linux内核分析——Linux内核学习总结
马悦+原创作品转载请注明出处+<Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 Linux内核学习总结 一 ...
- 【内核】linux内核启动流程详细分析
Linux内核启动流程 arch/arm/kernel/head-armv.S 该文件是内核最先执行的一个文件,包括内核入口ENTRY(stext)到start_kernel间的初始化代码, 主要作用 ...
- 【内核】linux内核启动流程详细分析【转】
转自:http://www.cnblogs.com/lcw/p/3337937.html Linux内核启动流程 arch/arm/kernel/head-armv.S 该文件是内核最先执行的一个文件 ...
- 【内核】Linux内核Initrd机制解析,内核更新步骤,grub配置说明
什么是Initrd initrd的英文含义是 boot loader initialized RAM disk,就是由boot loader初始化的内存盘.在 linux内核启动前, boot loa ...
- 《Linux内核--分析Linux内核创建一个新进程的过程 》 20135311傅冬菁
20135311傅冬菁 分析Linux内核创建一个新进程的过程 一.学习内容 进程控制块——PCB task_struct数据结构 PCB task_struct中包含: 进程状态.进程打开的文件. ...
- /var/log目录下的20个Linux日志文件功能详解 分类: 服务器搭建 linux内核 Raspberry Pi 2015-03-27 19:15 80人阅读 评论(0) 收藏
如果愿意在Linux环境方面花费些时间,首先就应该知道日志文件的所在位置以及它们包含的内容.在系统运行正常的情况下学习了解这些不同的日志文件有助于你在遇到紧急情况时从容找出问题并加以解决. 以下介绍的 ...
- [内核同步]Linux内核同步机制之completion
转自:http://blog.csdn.net/bullbat/article/details/7401688 内核编程中常见的一种模式是,在当前线程之外初始化某个活动,然后等待该活动的结束.这个活动 ...
随机推荐
- C#LeetCode刷题之#682-棒球比赛(Baseball Game)
问题 该文章的最新版本已迁移至个人博客[比特飞],单击链接 https://www.byteflying.com/archives/4028 访问. 你现在是棒球比赛记录员. 给定一个字符串列表,每个 ...
- Remix+Geth 实现智能合约部署和调用详解
Remix编写智能合约 编写代码 在线调试 实现部署 调用接口 Geth实现私有链部署合约和调用接口 部署合约 调用合约 获得合约实例 通过实例调用合约接口 Remix编写智能合约 编写代码 Remi ...
- Linux内核之 基本概念
一直想写写Linux内核的文章,特别是进程这方面的,说实话,不好写,也不太敢写:)直到遇到了一本好书.<Linux内核设计与实现>,原书<Linux Kernel Developme ...
- 一个基于 Beego 的,能快速创建个人博客,cms 的系统
学习beego时候开发的一个博客系统,在持续完善,有不足之处,望大佬们多多体谅,并且指出.感谢! Go Blog 一个基于Beego的,能快速创建个人博客,cms 的系统 包含功能 查看 Go Blo ...
- 【ASP.NET Core学习】使用JWT认证授权
概述 认证授权是很多系统的基本功能 , 在以前PC的时代 , 通常是基于cookies-session这样的方式实现认证授权 , 在那个时候通常系统的用户量都不会很大, 所以这种方式也一直很好运行, ...
- Mybatis-01-什么是Mybatis以及第一个Mybatis
Mybatis-9.28 环境: jdk 1.8 Mysql 5.7 maven 3.6.1 IDEA 回顾: JDBC Mysql Java基础 Maven Junit SSM框架:配置文件,最好的 ...
- 每日一道 LeetCode (19):合并两个有序数组
每天 3 分钟,走上算法的逆袭之路. 前文合集 每日一道 LeetCode 前文合集 代码仓库 GitHub: https://github.com/meteor1993/LeetCode Gitee ...
- eric4 中pyqt 字符串 输入 获取
在eric4中使用pyqt需要注意: 输入 中文 时,前面加 u ,例如: from PyQt4.QtGui import * from PyQt4.QtCore import * QMessageB ...
- Spring Boot系列(三):Spring Boot整合Mybatis源码解析
一.Mybatis回顾 1.MyBatis介绍 Mybatis是一个半ORM框架,它使用简单的 XML 或注解用于配置和原始映射,将接口和Java的POJOs(普通的Java 对象)映射成数据库中的记 ...
- 进阶6:连接查询 一、sql92标准
#进阶6:连接查询/*含义:又称多表查询,当查询的字段来自于多个表时,就会用到连接查询 笛卡尔乘积现象:表1 有m行,表2有n行,结果=m*n行 发生原因:没有有效的连接条件如何避免:添加有效的连接条 ...