一、概念

操作系统时间主要是基于系统心跳(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, &timestart);
        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, &timestart);
        printf("Hello SylixOS! time = %lld.%09lu\n", timestart.tv_sec, timestart.tv_nsec);

        lib_clock_gettime(CLOCK_MONOTONIC, &timeend);
        __timespecSub2(&temp, &timeend, &timestart);
        __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;
}

此方案能绝对解决时间回退的问题,但依然存在纰漏。

  1. 如果上一次记录在一个tick之前,则本次获取时间肯定大于上次时间,得不到额外的补偿,时间还是可能差一个心跳周期。
  2. 这种访问只能对单调时间有效,对墙上时间会有错误影响。

方案三
在心跳中断服务中,在更新心跳计数前,获取一次单调高精度时间Tmonon,bspTickHighResolution 中获取的时间和Tmonon比较,如果小于Tmonon就补偿一个心跳周期。如果在Tn~Tmonon间获取时间,此时Tmonon还未更新,应该是Tmonon - 1的值,大概率会小于这个值,可以得到补偿;如果在Tmonon~Tn‘间获取时间,此时Tmonon已更新,肯定会小于这个值,可以得到补偿。这个会比方案二效果好一些,但还是存在错误概率,且只对单调时间适用。
在这里插入图片描述
方法四
借助另外的64位定时器,它和心跳定时器可以同步的也可以是异步的,这个关系不大,任意时刻读取该定时器值,都可算到系统累计运行时间,这里只利用其计算时间差。x86和高版本的ARM都具备这样的通用64位定时器。
在心跳回调函数中读取一下64位定时器时间,设为T64n,此处紧挨在更新心跳计数时间后。进行高精度补偿时,再读取一下该64位定时器时间,设为T64n’,然后将T64n’ - T64n的差值作为补偿值。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
具体又分3种情况

  1. Tn < T64n’ < Tn’ 时,时间结果为 t = Tick(Tn - 1) + (T64n’ - T64n - 1)
  2. T64n < T64n’ < Tn+1 时,时间结果为 t = Tick(Tn) + (T64n’ - T64n)
  3. 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);
}
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐