SylixOS里的时间【14】--- 高精度时间修正的高精度分析
一、概念
操作系统时间主要是基于系统心跳(tick)的,系统心跳来自硬件定时器周期性中断,中断中进行计数,心跳数乘以心跳周期便是系统的累计运行时间。基于心跳计数就可以获取时间,计算耗时,设置超时,进行系统延时等操作。
基于系统心跳的时间误差最大是一个心跳周期,因为起始时刻可能是心跳周期中的任意位置,而截止时刻是某次心跳中断时刻,所以第一个心跳计数对应的实际时间在0~1个心跳周期之间。
如下图,要延时4个tick,但实际时间在3~4个tick之间。

系统心跳频率越高,时间精度越高,但心跳频率不能设的太高,否则CPU消耗在心跳服务上的占比就会较高,影响系统性能。系统心跳频率一般为10 ~ 1000 Hz。只依靠心跳精度的话,计时时间越长误差比率越小,计时时间越小误差比率越大,尤其接近或小于一个心跳时误差比率就很大了。假如系统心跳频率是100,即每个心跳周期10ms,延时10s就是计数1000个心跳,误差是千分之一;如果延时10毫秒,就是计数一个心跳,实际延时时间则在0~10ms之间,误差为百分之百。
很多情况下需要高精度时间,如测量中断服务耗时,测量系统实时性等。这里的高精度是相比于心跳周期而言的,操作系统利用硬件特性,获取远高于心跳周期的时间精度。系统虽然提供了timeval(时间精度微秒)和timespec(时间精度纳秒)格式的时间表示,但实际时间精度是由系统硬件决定的,而不是数据格式。
SylixOS中,通过实现bspLib.c文件下的bspTickHighResolution函数接口来为系统提供高精度时间修正。bspTickHighResolution函数会为传入的时间加上据上次心跳中断后已运行的时间(心跳内时间)。
如下图,同样是延时4个tick时间,有高精度支持时,会补偿Tn到tstart之间的时间。

二、接口
SylixOS通过bspTickHighResolution进行高精度时间补偿,但此函数会被系统自动回调,不需要用户直接调用。SylixOS中主要是通过nanosleep函数和lib_clock_gettime函数使用高精度时间的。这俩函数通过调用bspTickHighResolution实现高精度,而其他一些时间接口又是通过调用nanosleep函数或lib_clock_gettime函数间接实现的。
/*********************************************************************************************************
** 函数名称: bspTickHighResolution
** 功能描述: 修正从最近一次 tick 到当前的精确时间.
** 输 入 : ptv 需要修正的时间,既是输入也是输出
** 输 出 : NONE
*********************************************************************************************************/
VOID bspTickHighResolution (struct timespec *ptv)
bspTickHighResolution 函数会对输入的时间加上从上次 tick中断时刻到当前时刻的精确时间,以获取心跳精度以内的补偿时间,此函数由具体bsp驱动实现,如果不需要高精度时间此函数不进行任何处理即可。
/*********************************************************************************************************
** 函数名称: lib_clock_gettime
** 功能描述: 获取系统高精度时间
** 输 入 : clockid 时间类型
tv 输出地址
** 输 出 : 错误号
*********************************************************************************************************/
INT lib_clock_gettime (clockid_t clockid, struct timespec *tv)
lib_clock_gettime 函数,等同于POSIX中的clock_gettime 函数,它能获取各时间类型的高精度时间。系统包括以下四类时间:
| 类型 | 说明 |
|---|---|
| CLOCK_REALTIME | 系统实时时间(墙上时间,钟表时间),可以被校时 |
| CLOCK_MONOTONIC | 系统累计运行时,单调时间,不可校时 |
| CLOCK_PROCESS_CPUTIME_ID | 进程累计耗时 |
| CLOCK_THREAD_CPUTIME_ID | 线程累计耗时 |
| 其实现原理是先获取各时间类型的tick精度时间,再通过bspTickHighResolution函数进行修正。最常用的就是获取高精度CLOCK_MONOTONIC时间,通过这个绝对时间的时间差可以计算一段代码的耗时。 |
/*********************************************************************************************************
** 函数名称: nanosleep
** 功能描述: 使线程睡眠一个指定的时间, 睡眠过程中可能被信号唤醒. (POSIX)
** 输 入 : rqtp 睡眠的时间
** rmtp 保存剩余时间的结构.
** 输 出 : ERROR_NONE or PX_ERROR,error == EINTR 表示被信号激活.
*********************************************************************************************************/
INT nanosleep (const struct timespec *rqtp, struct timespec *rmtp)
nanosleep函数用于高精度延时,其使用的是CLOCK_MONOTONIC类型时间。对于一段延时,整tick数的时间是通过让出CPU线程休眠方式进行的,待唤醒后剩余不足1tick的时间是通过轮询高精度时间实现的,会一直占用CPU。usleep函数是通过nanosleep实现的,所以也具备高精度延时特性。而sleep函数是通过API_TimeSleepEx函数实现的,是tick精度的延时。
三、用法
高精度周期性任务应用举例
方法一
int main (int argc, char **argv)
{
struct timespec timestart;
while (1) {
lib_clock_gettime(CLOCK_MONOTONIC, ×tart);
printf("Hello SylixOS! time = %lld.%09lu\n", timestart.tv_sec, timestart.tv_nsec);
sleep(1);
}
return (0);
}
[root@sylixos:/root]# /apps/test/test
Hello SylixOS! time = 835.125670074
Hello SylixOS! time = 836.125776519
Hello SylixOS! time = 837.125795796
Hello SylixOS! time = 838.125816750
Hello SylixOS! time = 839.125841056
Hello SylixOS! time = 840.125864524
Hello SylixOS! time = 841.125886316
Hello SylixOS! time = 842.125906432
Hello SylixOS! time = 843.125929062
Hello SylixOS! time = 844.125951692
Hello SylixOS! time = 845.125973483
Hello SylixOS! time = 846.125995275
Hello SylixOS! time = 847.126015391
Hello SylixOS! time = 848.126034668
Hello SylixOS! time = 849.126054784
Hello SylixOS! time = 850.126079928
[sh]Warning: Program is killed (SIGKILL) by shell.
Restart SylixOS is recommended!
[root@sylixos:/root]#
分析:因为printf执行需要耗费时间,且因为缓存,抢占等原因这个耗时是会有变化的,实际的执行周期就会是1秒多一点。从结果也能看出,连续运行一段时间后。执行时刻的毫秒数在增加。
方法二
static VOID __timespecSub2 (struct timespec *ptv,
const struct timespec *ptv1,
const struct timespec *ptv2)
{
ptv->tv_sec = ptv1->tv_sec - ptv2->tv_sec;
ptv->tv_nsec = ptv1->tv_nsec - ptv2->tv_nsec;
if (ptv->tv_nsec >= __TIMEVAL_NSEC_MAX) {
ptv->tv_sec++;
ptv->tv_nsec -= __TIMEVAL_NSEC_MAX;
} else if (ptv->tv_nsec < 0) {
ptv->tv_sec--;
ptv->tv_nsec += __TIMEVAL_NSEC_MAX;
}
}
int main (int argc, char **argv)
{
struct timespec period = {1, 0};
struct timespec temp;
struct timespec timestart;
struct timespec timeend;
while (1) {
lib_clock_gettime(CLOCK_MONOTONIC, ×tart);
printf("Hello SylixOS! time = %lld.%09lu\n", timestart.tv_sec, timestart.tv_nsec);
lib_clock_gettime(CLOCK_MONOTONIC, &timeend);
__timespecSub2(&temp, &timeend, ×tart);
__timespecSub2(&temp, &period, &temp);
nanosleep(&temp, LW_NULL);
}
return (0);
}
[root@sylixos:/root]# /apps/test/test
Hello SylixOS! time = 529.054533544
Hello SylixOS! time = 530.054634960
Hello SylixOS! time = 531.054659267
Hello SylixOS! time = 532.054681897
Hello SylixOS! time = 533.054705365
Hello SylixOS! time = 534.054726319
Hello SylixOS! time = 535.054747272
Hello SylixOS! time = 536.054765712
Hello SylixOS! time = 537.054788342
Hello SylixOS! time = 538.054808457
Hello SylixOS! time = 539.054826896
Hello SylixOS! time = 540.054849526
Hello SylixOS! time = 541.054869642
Hello SylixOS! time = 542.054889757
Hello SylixOS! time = 543.054912387
Hello SylixOS! time = 544.054931665
Hello SylixOS! time = 545.054953457
Hello SylixOS! time = 546.054860422
[sh]Warning: Program is killed (SIGKILL) by shell.
Restart SylixOS is recommended!
[root@sylixos:/root]#
分析:这里通过高精度延时函数nanosleep实现,且延时时间去掉了printf执行耗时,所以执行周期是更严格的1秒。
四、实现
bspTickHighResolution函数会为传入的时间加上据上次心跳中断后已运行的时间(心跳内时间)。那这个函数又是如何实现的呢?
一般的系统心跳中断是由一个硬件定时器实现的,而硬件定时器其实就是对一个固定时钟信号进行计数,计数等于设定值后触发中断,计数值清零并继续计数。比如某个硬件定时器,它有计数寄存器和周期寄存器两个寄存器,计数寄存器每一个系统时钟加1,当和周期寄存器值相等时触发中断并归零,计数寄存器的值是随时可以读取的,且计数值对应的时间也是可计算的,这个时间就是心跳内高精度时间。
设定时器时钟源频率为100Mhz,则一个主频周期为10纳秒;心跳频率为1000hz,则心跳周期为1毫秒。定时计数器需要计数100 * 1000000 / 1000 = 100 * 1000个。如果是递增计数器,应该是从0记到99999,0 -->1, 1–>2,…99998–>99999之间都是一个主频周期。计数到99999后,再等10微秒数据溢出,数值重新回到0,即99999–>0也是一个主频周期。如果当前计数寄存器值为n则表示距上次中断已过期n*10纳秒的时间。
则bspTickHighResolution 函数的实现源码如下:
/*********************************************************************************************************
** 函数名称: bspTickHighResolution
** 功能描述: 修正从最近一次 tick 到当前的精确时间.
** 输 入 : ptv 需要修正的时间
** 输 出 : NONE
*********************************************************************************************************/
VOID bspTickHighResolution (struct timespec *ptv)
{
UINT32 uiCntCur;
uiCntCur = read32(REG_TIMER_COUNT);
ptv->tv_nsec += (LONG)(uiCntCur * 10);
if (ptv->tv_nsec >= 1000000000) {
ptv->tv_nsec -= 1000000000;
ptv->tv_sec++;
}
}
如果是递减计数器,需要做一次差来获取已运行的时钟周期数。再有就配置计数周期寄存器时是否要进行减一。不同的硬件其系统心跳和高精度修正实现是不同的,但绝大部分基本方法都和上面的实现类似。
五、漏洞
高精度时间修正看起来实现比较简单,但操作不严谨的话可能出现时间偏差一个tick的情况,甚至造成时间回退的情况,这在很多情况下是不能容忍的。这个漏洞不但非常危险,而且概率很小不容易发现。
可以用下面程序对系统高精度时间回退问题进行测试。
测试方法一:
一段时间内,tv[0]保持不变,tv[1]不断更新,检查tv[0]是否会出现大于tv[1]的情况。
/*********************************************************************************************************
** 函数名称: tickhTest
** 功能描述: tickh测试
** 输 入 : 无
** 输 出 : NONE
*********************************************************************************************************/
VOID tickhTest (VOID)
{
struct timespec tv[2];
UINT32 n = 0;
printf("tickhighresolutionTest\n");
clock_gettime(CLOCK_MONOTONIC, &tv[0]);
while(1) {
clock_gettime(CLOCK_MONOTONIC, &tv[1]);
if ((tv[0].tv_sec > tv[1].tv_sec) ||
((tv[0].tv_sec == tv[1].tv_sec) && (tv[0].tv_nsec > tv[1].tv_nsec))) {
printf("\n");
printf("time coiling, n = %d\n", n);
printf("tv[0] = %lld.%09lu\n", tv[0].tv_sec, tv[0].tv_nsec);
printf("tv[1] = %lld.%09lu\n", tv[1].tv_sec, tv[1].tv_nsec);
} else if (tv[0].tv_sec < tv[1].tv_sec) {
printf("time = %lld.%09lu\n", tv[1].tv_sec, tv[1].tv_nsec);
tv[0] = tv[1];
}
n++;
}
}
赶快用此测试例程测试一下你的系统吧,可能会有意外发现
[root@sylixos:/root]#
[root@sylixos:/root]# /apps/test/test
tickhighresolutionTest
time coiling, n = 2667
tv[0] = 21.832946929
tv[1] = 21.830001676
time = 22.001293263
time coiling, n = 61819
tv[0] = 22.001293263
tv[1] = 22.000000000
time = 23.001036738
time coiling, n = 422003
tv[0] = 23.001036738
tv[1] = 23.000000000
time = 24.001429043
time coiling, n = 783152
tv[0] = 24.001429043
tv[1] = 24.000000000
time coiling, n = 783153
tv[0] = 24.001429043
tv[1] = 24.001231240
time = 25.001211073
time coiling, n = 1141984
tv[0] = 25.001211073
tv[1] = 25.000000838
[sh]Warning: Program is killed (SIGKILL) by shell.
Restart SylixOS is recommended!
[root@sylixos:/root]#
分析:tv[1]在tv[0]的后面获取,却出现了tv[1]小于tv[0]的情况,这显然是不可接受的。而且可以看到114万次才出现了6次错误情况,说明这个错误的触发概率是很小的,很容易被忽视。
注意:tv[1]是可能等于tv[0]的,尤其是在CPU运算较快,而定时器计数时钟频率较低时,可能连续获取到同一个时间值。
测试方法二:
tv[1]不断更新,检查tv[0]是否会出现大于tv[1]的情况,且每次比较后都将tv[0]更新为tv[1];
/*********************************************************************************************************
** 函数名称: tickhTest2
** 功能描述: tickh测试2
** 输 入 : 无
** 输 出 : NONE
*********************************************************************************************************/
VOID tickhTest2 (VOID)
{
struct timespec tv[2];
UINT32 n = 0;
printf("tickhighresolutionTest2\n");
clock_gettime(CLOCK_MONOTONIC, &tv[0]);
while(1) {
clock_gettime(CLOCK_MONOTONIC, &tv[1]);
if ((tv[0].tv_sec > tv[1].tv_sec) ||
((tv[0].tv_sec == tv[1].tv_sec) && (tv[0].tv_nsec > tv[1].tv_nsec))) {
printf("\n");
printf("time coiling, n = %d\n", n);
printf("tv[0] = %lld.%09lu\n", tv[0].tv_sec, tv[0].tv_nsec);
printf("tv[1] = %lld.%09lu\n", tv[1].tv_sec, tv[1].tv_nsec);
}
tv[0] = tv[1];
n++;
}
}
[root@sylixos:/root]#
[root@sylixos:/root]# /apps/test/test
Hello SylixOS!
tickhighresolutionTest
time coiling, n = 811041
tv[0] = 1206.972100781
tv[1] = 1206.966755938
time coiling, n = 1050209
tv[0] = 1275.211602831
tv[1] = 1275.207336584
实测发现,不同bspTickHighResolution实现方式下,有的只能用测试方法一测出,有的只能用测试方法二测出。所以这两种测试方法都要测试。
六、分析
上面介绍了时间回退漏洞的概念和测试方法,这里介绍时间回退原因和解决方法。
从心跳中断触发,到中断服务响应并对tick进行计数,是需要消耗一段时间的。而且心跳中断可能被屏蔽或被其他高优先级中断抢占,这都使得心跳计数更新比心跳触发滞后,且滞后时间存在一定抖动。不管心跳计数更新滞后多少,硬件计数寄存器都是在正常计数变化的。如果获取时间的时刻正处于心跳中断触发和心跳计数更新之间,心跳计数还是上一个周期的,而心跳内时间是当前周期的,补偿的时间就比实际时间少了约一个tick周期。

如上图,Tn-1,Tn,Tn+1,是定时器中断触发时刻。Tn‘是Tn中断响应中更新心跳计数的时刻。t是实际的读取时间的时刻,此时心跳计数还未更新还是n-1,而补偿时间是个很小的值,最终得到时间是t’,也就是说时间回退了一个心跳周期。
错误概率估算
由上可知,只有在Tn~Tn‘之间获取时间才会错误,那(Tn’-Tn)/T就是错误概率(T为心跳周期)。心跳周期由系统设定决定,而(Tn'-Tn)主要由心跳服务执行速度决定。所以心跳周期越小概率越高,CPU越慢概率越高。假设系统心跳是10毫秒,从中断触发到更新心跳计数需要100条单周期指令,当CPU主频是10MHz时,耗时为10微秒,错误概率为千分之一;当CPU主频是1GHz时,耗时为100纳秒,错误概率为十万分之一。
心跳中断服务中,一般更新心跳计数的操作是比较靠前的,可能用不了100条指令。但因为cache不命中,中断屏蔽或高优先级中断抢占等原因,则此耗时又可能远大于100条指令。
那如何规避该错误呢?
方案一,读取心跳内时间后再读取一下定时器中断标志,如果中断标志置位,说明定时器已经从新计数了,需要在已有补偿的情况下再补偿一个心跳周期。
之前的代码改进如下:
/*********************************************************************************************************
** 函数名称: bspTickHighResolution
** 功能描述: 修正从最近一次 tick 到当前的精确时间.
** 输 入 : ptv 需要修正的时间
** 输 出 : NONE
*********************************************************************************************************/
VOID bspTickHighResolution (struct timespec *ptv)
{
UINT32 uiCntCur;
uiCntCur = read32(REG_TIMER_COUNT);
/*
* 检查是否有 TICK 中断请求
*/
if (read32(REG_TIMER_STATE) & TIMER_INT_PENDING) {
/*
* 这里由于 TICK 没有及时更新, 所以需要加上一个 TICK 的时间
*/
uiCntCur += 100 * 1000;
}
ptv->tv_nsec += (LONG)(uiCntCur * 10);
if (ptv->tv_nsec >= 1000000000) {
ptv->tv_nsec -= 1000000000;
ptv->tv_sec++;
}
}
这个方法能明显减小错误概率,但还是存在问题。不是所有的心跳中断都有中断标志,而且中断服务中会进行清除中断标志操作,这个操作不同实现下,可能在T’前也可能在T’后。在T’前可能读不到中断标志,在T’后则可能多加一个心跳周期。所以这种方法,虽能大幅减小错误概率,但原理上并不能完全避免时间回退问题。


方案二,记录下每次修正的高精度时间,本次时间与上次时间比较,如果本次时间小于上次时间,则本次时间再补偿一个心跳周期。
/*********************************************************************************************************
** 函数名称: bspTickHighResolution
** 功能描述: 修正从最近一次 tick 到当前的精确时间.
** 输 入 : ptv 需要修正的时间
** 输 出 : NONE
*********************************************************************************************************/
VOID bspTickHighResolution (struct timespec *ptv)
{
UINT32 uiCntCur;
static volatile struct timespec tv = {0, 0}; /* 单调递增 */
uiCntCur = read32(REG_TIMER_COUNT);
/*
* 检查是否有 TICK 中断请求
*/
if (read32(REG_TIMER_STATE) & TIMER_INT_PENDING) {
/*
* 这里由于 TICK 没有及时更新, 所以需要加上一个 TICK 的时间
*/
uiCntCur += 100 * 1000;
}
ptv->tv_nsec += (LONG)(uiCntCur * 10);
if (ptv->tv_nsec >= 1000000000) {
ptv->tv_nsec -= 1000000000;
ptv->tv_sec++;
}
if ((tv.tv_sec > ptv->tv_sec) ||
((tv.tv_sec == ptv->tv_sec) && (tv.tv_nsec > ptv->tv_nsec))) { /* 出现时间倒退 */
ptv->tv_nsec += 1000 * 1000;
if (ptv->tv_nsec >= 1000000000) {
ptv->tv_nsec -= 1000000000;
ptv->tv_sec++;
}
}
tv = *ptv;
}
此方案能绝对解决时间回退的问题,但依然存在纰漏。
- 如果上一次记录在一个tick之前,则本次获取时间肯定大于上次时间,得不到额外的补偿,时间还是可能差一个心跳周期。
- 这种访问只能对单调时间有效,对墙上时间会有错误影响。
方案三
在心跳中断服务中,在更新心跳计数前,获取一次单调高精度时间Tmonon,bspTickHighResolution 中获取的时间和Tmonon比较,如果小于Tmonon就补偿一个心跳周期。如果在Tn~Tmonon间获取时间,此时Tmonon还未更新,应该是Tmonon - 1的值,大概率会小于这个值,可以得到补偿;如果在Tmonon~Tn‘间获取时间,此时Tmonon已更新,肯定会小于这个值,可以得到补偿。这个会比方案二效果好一些,但还是存在错误概率,且只对单调时间适用。

方法四
借助另外的64位定时器,它和心跳定时器可以同步的也可以是异步的,这个关系不大,任意时刻读取该定时器值,都可算到系统累计运行时间,这里只利用其计算时间差。x86和高版本的ARM都具备这样的通用64位定时器。
在心跳回调函数中读取一下64位定时器时间,设为T64n,此处紧挨在更新心跳计数时间后。进行高精度补偿时,再读取一下该64位定时器时间,设为T64n’,然后将T64n’ - T64n的差值作为补偿值。



具体又分3种情况
- Tn < T64n’ < Tn’ 时,时间结果为 t = Tick(Tn - 1) + (T64n’ - T64n - 1)
- T64n < T64n’ < Tn+1 时,时间结果为 t = Tick(Tn) + (T64n’ - T64n)
- Tn’ < T64n’ < T64n 时,时间结果为 t = Tick(Tn) + (T64n’ - T64n-1)
其中1,2能得到正确结果,但会和实际值有一个的偏差。大概率下,此偏差值是固定的,等于T64n - Tn,对于使用一般无影响,且可以对此偏差进行补偿。但因为中断关闭、高优先级中断抢占、cache未命中等原因,使得每个心跳周期中的T64位置是有偏移的,如果偏移较大则可能有不良影响。
情况3就有问题了,时间多了一个心跳周期,但Tn’ ~ T64n的间隔极小,出现情况3的概率也极小。
/*********************************************************************************************************
精确时间换算参数
*********************************************************************************************************/
static UINT32 _G_uiFullCnt; /* 一个 Tick 所需的计数周期 */
static UINT64 _G_ullNSecPerCnt7; /* 一个计数值的 ns 数,放大 2^7*/
static UINT64 _G_ullComparatorCur; /* 保存当前产生中断的计数器值 */
/*********************************************************************************************************
** 函数名称: bspTaskIdleHook
** 功能描述: 每个操作系统时钟节拍,系统将调用这个函数
** 输 入 : i64Tick 系统当前时钟
** 输 出 : NONE
** 全局变量:
** 调用模块:
*********************************************************************************************************/
VOID bspTickHook (INT64 i64Tick)
{
_G_ui64ComparatorCur = armGlobalTimerComparatorGet() - _G_uiFullCnt;
}
/*********************************************************************************************************
** 函数名称: bspTickHighResolution
** 功能描述: 修正从最近一次 tick 到当前的精确时间.
** 输 入 : ptv 需要修正的时间
** 输 出 : NONE
** 全局变量:
** 调用模块:
*********************************************************************************************************/
VOID bspTickHighResolution (struct timespec *ptv)
{
UINT64 ui64CntCur = (UINT64)armGlobalTimerCounterGet();
ptv->tv_nsec += ((ui64CntCur - _G_ui64ComparatorCur) * _G_ui64NSecPerCnt7) >> 7;
if (ptv->tv_nsec >= 1000000000) {
ptv->tv_nsec -= 1000000000;
ptv->tv_sec++;
}
}
小结
以上各种方法都不能完美的解决时间回退或计时精度的问题。其根本原因是高精度时间由两部分组成,心跳计数时间和心跳内修正时间,这两部分时间是在不同时刻获取或更新的,这个时间差是关中断、加锁、加标志所无法克服的。获取高精度时间的操作可能是在任意时刻调用的,中断中也是可以的。调用时是无法确定心跳计数是刚更新的还是早已更新的。又由于目前系统框架下,多种时间类型都是通过bspTickHighResolution函数来补偿的,所以方法二三是不可采用的,除非实际项目中只用到了高精度单调时间。
另一个思路就是保证不在Tn~Tn’ 间进行高精度时间校正,自然也就不会出现错误。将更新心跳计数操作放在心跳中断服务开头位置,将心跳中断优先级设为最高,不在中断中使用高精度时间等都可有效减小错误概率。这也是实际项目中不会发现该问题的重要原因之一。但在进行中断服务耗时测量时,一定会在Tn~Tn'间获取一次高精度时间的。
获取时间的操作细节
获取一个高精度时间,需要分两次读取,设tn时刻读取tick计数时间,tn ’时刻读取补偿时间,此其间时间极短,且是被LW_SPIN_KERN_LOCK_QUICK锁保护的,且关闭中断响应。



状态分析:
- tn >= tn ’ 不可能
- Tn >= Tn’ 不可能
- Tn’>=Tn+1 严重错误,因中断屏蔽、高优先级中断抢占、执行超时等原因使得心跳中断延迟超过一个心跳周期,心跳计数会少记一次,系统时间滞后了。时间丢了!
- Tn’ < tn < tn ’ < Tn+1,则读取的时间是正确的,为Time(tn ’)。
- tn < Tn’ 且Tn < tn ’,读取时间少会一个tick周期。又分一下4中情况。
- Tn <tn < tn ’ < Tn’ ,概率最高
- Tn <tn < Tn’ < tn ’ ,概率较小
- tn < Tn <Tn’ < tn ’ ,不太可能
- tn < Tn < tn ’<Tn’ ,概率较小
七、改进
思路
- 要想获取准确的,高精度的系统累计运行时间,必须要有64位硬件定时器,一次性获取完整时间值。
- 可以直接在系统底层增加一个累计运行时间函数bspGetMonoTime,单位纳秒,UINT64类型,保证连续运行100年不溢出。
- 系统实现中,只对系统累计运行时间进行高精度补偿,其他时间类型则基于高精度系统累计运行时间来实现。
- 对于UTC时间和系统累计运行时间,都是单调递增的,只是他们的起点不一样,之间有一个固定的差值,而这个时间差值其实就是系统复位时的UTC时间。当前系统累计运行时间加上系统复位时的UTC时间就可得到当前UTC时间。
- 系统实时时间或墙上时间,是应该和UTC时间一致的,但由于实际误差的存在,会有一些偏差,所以墙上时间需要被设置或校时,时间是可能回退的。和UTC时间一样墙上时间也可以通过系统累计运行时间加上一个差值获取,而校时操作最终都可转化为对此差值的调整。
方法一
有这样一种硬件计数器,它有两个计数寄存器,一个64位,一个32位,他们同时启动且对同一个时钟源进行计数,也就是说这俩计数寄存器是同步的。64位计数器初始值为0,只读,且在读取高32位时会自动锁存低32位值,也就是读取时不会出现高低32位进位误差错误。32位计数器则在计数到都值时可触发中断,并自动归零循环计数,即32位计数器可实现系统心跳中断。
基于此硬件,想要获取系统累计运行时间,直接读取64位计数器即可,且速度快、精度高、无错误、任意时刻可访问,是高精度时间计算机系统的不二之选。
ARM 高端处理器统一具备的ARM generic timer就具备且超越了这样的定时器功能。它甚至可以让操作系统不需要心跳的需求,可以自由精确的获取时间和设置定时中断。
/*********************************************************************************************************
** 函数名称: bspGetMonoTime
** 功能描述: 获取系统累计运行时间
** 输 入 : 无
** 输 出 : 系统累计运行时间,单位纳秒
*********************************************************************************************************/
UINT64 bspGetMonoTime (VOID)
{
UINT64 uiMonoTime;
uiMonoTime = read32(REG_TIMER_COUNT_H32);
uiMonoTime = read32(REG_TIMER_COUNT_L32) | (uiMonoTime << 32);
return (uiMonoTime * 10);//假设计数时钟为100MHz
}
方法二
方法一的硬件要求比较高,大部分CPU不具备。方案二的系统心跳用普通定时器实现即可,高精度时间得有两组可级联的定时器,累计计时要求1年内不溢出,也没有读取时中断锁存的功能。心跳定时器先于级联定时器启动,俩个计时差是固定的,可以补偿同步下。
/*********************************************************************************************************
** 函数名称: bspGetMonoTime
** 功能描述: 获取系统累计运行时间
** 输 入 : 无
** 输 出 : 系统累计运行时间,单位纳秒
*********************************************************************************************************/
UINT64 bspGetMonoTime (VOID)
{
UINT64 uiMonoTime;
UINT32 uiValue[3];
uiValue[0] = read32(REG_TIMER0_COUNT);
while (1) {
uiValue[1] = read32(REG_TIMER1_COUNT);
uiValue[2] = read32(REG_TIMER0_COUNT);
if (uiValue[2] == uiValue[0]) {
break;
} else {
uiValue[0] = uiValue[2]
}
}
uiMonoTime = uiValue[0];
uiMonoTime = uiValue[1] | (uiMonoTime << 32);
return (uiMonoTime * 10 + 123);//假设计数时钟为100MHz,和心跳定时器起始偏差123纳秒
}
方法三
如果实在没有64位的定时器可用,那就只能用心跳定时器来实现了,为了防止时间回绕,每次获取时间要和上次比较,如果小于上次时间那就重新获取一次。
/*********************************************************************************************************
** 函数名称: bspGetMonoTime
** 功能描述: 获取系统累计运行时间
** 输 入 : 无
** 输 出 : 系统累计运行时间,单位纳秒
*********************************************************************************************************/
UINT64 bspGetMonoTime (VOID)
{
UINT32 uiCntCur;
UINT64 uiMonoTime;
static volatile UINT64 ulTimeLast = 0; /* 单调递增 */
uiCntCur = read32(REG_TIMER_COUNT);
while (1) {
uiMonoTime = _K_tvTODMono + uiCntCur * 10;
if (uiMonoTime > ulTimeLast) {
break;
}
}
ulTimeLast = uiMonoTime;
return (uiMonoTime);
}
lib_clock_gettime 和 lib_clock_settime 函数则可以直接基于bspGetMonoTime函数实现。
INT64 _G_iDiffRealtoMonotonic;
/*********************************************************************************************************
** 函数名称: lib_clock_gettime
** 功能描述:
** 输 入 :
** 输 出 :
*********************************************************************************************************/
INT lib_clock_gettime (clockid_t clockid, struct timespec *tv)
{
INTREG iregInterLevel;
PLW_CLASS_TCB ptcbCur;
UINT64 uiMonoTime;
if (tv == LW_NULL) {
_ErrorHandle(EINVAL);
return (PX_ERROR);
}
switch (clockid) {
case CLOCK_REALTIME:
LW_SPIN_KERN_TIME_LOCK_QUICK(&iregInterLevel);
uiMonoTime = bspGetMonoTime() + _G_iDiffRealtoMonotonic;
tv->tv_sec = uiMonoTime / 1000000000;
tv->tv_nsec= uiMonoTime % 1000000000;
LW_SPIN_KERN_TIME_UNLOCK_QUICK(iregInterLevel);
break;
case CLOCK_MONOTONIC:
LW_SPIN_KERN_TIME_LOCK_QUICK(&iregInterLevel);
uiMonoTime = bspGetMonoTime();
tv->tv_sec = uiMonoTime / 1000000000;
tv->tv_nsec= uiMonoTime % 1000000000;
LW_SPIN_KERN_TIME_UNLOCK_QUICK(iregInterLevel);
break;
case CLOCK_PROCESS_CPUTIME_ID:
break;
case CLOCK_THREAD_CPUTIME_ID:
break;
default:
_ErrorHandle(EINVAL);
return (PX_ERROR);
}
return (ERROR_NONE);
}
/*********************************************************************************************************
** 函数名称: lib_clock_settime
** 功能描述:
** 输 入 :
** 输 出 :
*********************************************************************************************************/
INT lib_clock_settime (clockid_t clockid, const struct timespec *tv)
{
INTREG iregInterLevel;
UINT64 uiRealTime;
if (tv == LW_NULL) {
_ErrorHandle(EINVAL);
return (PX_ERROR);
}
if (clockid != CLOCK_REALTIME) { /* CLOCK_REALTIME */
_ErrorHandle(EINVAL);
return (PX_ERROR);
}
uiRealTime = tv->tv_sec * 1000000000 + tv->tv_nsec;
LW_SPIN_KERN_TIME_LOCK_QUICK(&iregInterLevel);
_G_iDiffRealtoMonotonic = uiRealTime - bspGetMonoTime();
LW_SPIN_KERN_TIME_UNLOCK_QUICK(iregInterLevel);
return (ERROR_NONE);
}
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)