锐单电子商城 , 一站式电子元器件采购平台!
  • 电话:400-990-0325

SylixOS里的时间【7】--- 延时功能如何实现

时间:2022-08-20 04:30:01 24ppcb板公整套连接器

基本原理

SylixOS延迟接口主要依赖于延迟接口TOD实现线程控制接口和信号控制接口,等待唤醒链表接口。

基本原理是计算延迟接口函数中的延迟时间,从就绪表中删除当前线程,将线程添加到延迟等待唤醒链表(_K_wuDelay)退出关中断并尝试调度,此时当前线程将被放弃CPU进入休眠状态。系统心跳中断将继续检查延迟等待唤醒链表。对于计时到达的项目,将相应的线程添加到就绪表中,中断退出时调度线程。此时,延迟到达的线程将被重新调度。

源码分析

SylixOS主要位于延迟功能实现源libsylixos\SylixOS\kernel\interface\TimeSleep.c文件中。
为了容易理解,将对源代码进行一些调整,包括删除一些条件编译和条件执行代码,删除一些重新访问保护代码,删除一些异常和错误处理,删除一些条件检查,启动一些关键宏操作,添加更多注释等。

虽然SylixOS里延迟界面很多,但只要你理解API_TimeSleepEx 和nanosleep这两个函数可以充分理解SylixOS延迟实现机制,其他界面都是基于这两个函数,更容易理解。

API_TimeSleepEx 函数

在SylixOS在延时接口中,API_TimeSSleep、API_TimeMSleep函数是借助API_TimeSleep实现的,而API_TimeSleep其实就是API_TimeSleepEx信号唤醒时不允许特例,因此这三个接口在休眠时不能被信号唤醒。API_TimeSleepUntil是借助API_TimeSleepEx实现,通过bSigRet是否允许信号唤醒可以设置参数。

所以搞懂API_TimeSleepEx函数也能理解一切SylixOS延迟接口函数。

/********************************************************************************************************* ** 函数名称: API_TimeSleepEx ** 功能描述: 线程睡眠函数 (精度为 TICK HZ),中断中不得调用 ** 输 入 : ulTick 睡眠的时间 ** bSigRet 是否允许信号唤醒 ** 输 出 : ERROR_NONE or EINTR *********************************************************************************************************/ LW_API  ULONG  API_TimeSleepEx (ULONG  ulTick, BOOL  bSigRet) { 
             INTREG                iregInterLevel;     PLW_CLASS_TCB         ptcbCur;     PLW_CLASS_PCB         ppcb;     ULONG                 ulKernelTime;     INT                   iRetVal;          ///获得当前线程控制块     LW_TCB_GET_CUR_SAFE(ptcbCur);                                       /* 当前任务控制块 */      __wait_again:     ///延迟可执行多个循环,直到延迟时间为0,正常退出     if (!ulTick) { 
                                                              /* 不进行延迟 */         return;     }      ///进入内核状态,关闭中断     iregInterLevel = __KERNEL_ENTER_IRQ();                              /* 进入内核 */      ///注意线程优先级可能在循环过程中发生变化     ppcb = _GetPcb(ptcbCur);                                            /* 获取优先控制块 */     __DEL_FROM_READY_RING(ptcbCur, ppcb);                               /* 删除就绪表 */          ///将当前线程添加到延迟等待唤醒链表中     ptcbCur->TCB_ulDelay = ulTick;     ptcbCur->TCB_usStatus |= LW_THREAD_STATUS_DELAY;     _WakeupAdd(&_K_wuDelay, &ptcbCur->TCB_wunDelay, LW_FALSE
       
        )
        ; 
        //记录当前心跳计数,用于唤醒后判断延时时间是否已到 
        __KERNEL_TIME_GET_NO_SPINLOCK
        (ulKernelTime
        , ULONG
        )
        ; 
        /* 记录系统时间 */ 
        //退出内核状态同时打开中断,并尝试调度 
        //当前线程就是在此处阻塞的,就是在尝试调度调度后被让出CPU进行休眠,休眠时间到后,继续运行此函数返回 iRetVal 
        = 
        __KERNEL_EXIT_IRQ
        (iregInterLevel
        )
        ; 
        if 
        (iRetVal
        ) 
        { 
          
        /* 被信号激活 */ 
        //允许信号唤醒的话,此处就直接退出,并返回错误号。否则继续延时直到延时时间到 
        if 
        (bSigRet
        ) 
        { 
          
        _ErrorHandle
        (EINTR
        )
        ; 
        return 
        (EINTR
        )
        ; 
        } ulTick 
        = 
        _sigTimeoutRecalc
        (ulKernelTime
        , ulTick
        )
        ; 
        /* 重新计算等待时间 */ 
        goto __wait_again
        ; 
        /* 继续等待 */ 
        } 
        } 
       

nanosleep函数

POSIX提供一些标准通用的延时函数,时间精度最高是纳秒,休眠时能被信号唤醒。需要注意的一点是对于不足一个tick的部分,这些函数是通过轮询延时的,会一直占用CPU。
其中usleep和clock_nanosleep都基于nanosleep函数实现,sleep函数基于API_TimeSleepEx实现。

相比于API_TimeSleepEx 函数nanosleep在使能精密延时时,对不足一个tick的部分是通过轮询方式延时的,而不使能精密延时时精度也是一个tick。

下面代码只考虑使能精密延时时的情况。

/********************************************************************************************************* ** 函数名称: nanosleep ** 功能描述: 使调用此函数的线程睡眠一个指定的时间, 睡眠过程中可能被信号唤醒. ** 输 入 : rqtp 睡眠的时间 ** rmtp 保存剩余时间的结构. ** 输 出 : ERROR_NONE or PX_ERROR error == EINTR 表示被信号激活. *********************************************************************************************************/
LW_API  INT  nanosleep (const struct timespec  *rqtp, struct timespec  *rmtp)
{ 
        
    INTREG             iregInterLevel;
    PLW_CLASS_TCB      ptcbCur;
    PLW_CLASS_PCB      ppcb;
    ULONG              ulKernelTime;
    INT                iRetVal;
    INT                iSchedRet;
    ULONG              ulError;
    ULONG              ulTick;
    
    struct timespec    tvStart;
    struct timespec    tvTemp;
    
    //将纳秒精度时间转为tick数,舍去不足1tick部分
    ulTick = __timespecToTickNoPartial(rqtp);
    if (!ulTick) { 
                                                          /* 不到一个 tick */
        __timePassSpec(rqtp);                                       /* 平静度过 */
        if (rmtp) { 
        
            rmtp->tv_sec  = 0;                                      /* 不存在时间差别 */
            rmtp->tv_nsec = 0;
        }
        return  (ERROR_NONE);
    }
    
    //获取当前高精度的_K_tvTODMono时间
    __timeGetHighResolution(&tvStart);                                  /* 记录开始的时间 */
    //获取当前线程控制块
    LW_TCB_GET_CUR_SAFE(ptcbCur);                                       /* 当前任务控制块 */
    
    //延时主循环,这部分和API_TimeSleepEx中的基本一样
__wait_again:
    //进入内核状态并关闭中断
    iregInterLevel = __KERNEL_ENTER_IRQ();                              /* 进入内核 */
    //注意循环过程中,线程优先级可能会被改变
    ppcb = _GetPcb(ptcbCur);
    __DEL_FROM_READY_RING(ptcbCur, ppcb);                               /* 从就绪表中删除 */
    
    //将当前线程加入延时等待唤醒链表中
    ptcbCur->TCB_ulDelay = ulTick;
    ptcbCur->TCB_usStatus |= LW_THREAD_STATUS_DELAY;
    _WakeupAdd(&_K_wuDelay, &ptcbCur->TCB_wunDelay, LW_FALSE);
    //记录当前心跳计数,用于唤醒后判断延时时间是否已到
    __KERNEL_TIME_GET_NO_SPINLOCK(ulKernelTime, ULONG);                 /* 记录系统时间 */
    
    //退出内核状态同时打开中断,并尝试调度
    //当前线程就是在此处阻塞的,就是在尝试调度调度后被让出CPU进行休眠,休眠时间到后,继续运行此函数返回
    iSchedRet = __KERNEL_EXIT_IRQ(iregInterLevel);                      /* 调度器解 */
    
    //被唤醒,根据唤醒原因不同,分别进行处理
    if (iSchedRet == LW_SIGNAL_EINTR) { 
        
        iRetVal = PX_ERROR;                                             /* 被信号激活 */
        ulError = EINTR;
    
    } else { 
        
        if (iSchedRet == LW_SIGNAL_RESTART) { 
                                   /* 信号要求重启等待 */
            ulTick = _sigTimeoutRecalc(ulKernelTime, ulTick);           /* 重新计算等待时间 */
            if (ulTick != 0ul) { 
        
                goto    __wait_again;                                   /* 重新等待剩余的 tick */
            }
        }
        iRetVal = ERROR_NONE;                                           /* 自然唤醒 */
        ulError = ERROR_NONE;
    }
    
    //正常退出,返回延时剩余时间为0
    if (ulError ==  ERROR_NONE) { 
                                               /* tick 已经延迟结束 */
        //如果还有不足一个tick的时间需要延时,则通过轮询方式等待延时完成
        __timeGetHighResolution(&tvTemp);
        __timespecSub(&tvTemp, &tvStart);                               /* 计算已经延迟的时间 */
        if (__timespecLeftTime(&tvTemp, rqtp)) { 
                                /* 还有剩余时间需要延迟 */
            struct timespec  tvNeed;
            __timespecSub2(&tvNeed, rqtp, &tvTemp);
            __timePassSpec(&tvNeed);                                    /* 平静度过 */
        }
        
        if (rmtp) { 
        
            rmtp->tv_sec  = 0;                                          /* 不存在时间差别 */
            rmtp->tv_nsec = 0;
        }
        return  (iRetVal);
    }
    
    //超前退出,返回延时剩余时间
    if (rmtp) { 
        
        *rmtp = *rqtp;
        __timeGetHighResolution(&tvTemp);
        __timespecSub(&tvTemp, &tvStart);                           /* 计算已经延迟的时间 */
        if (__timespecLeftTime(&tvTemp, rmtp)) { 
                            /* 没有延迟够 */
            __timespecSub(rmtp, &tvTemp);                           /* 计算没有延迟够的时间 */
        }
    }

    return  (iRetVal);
}

nanosleep函数中用到了一个__timePassSpec函数,它能以轮询的方式延时一段时间,保证了一个心跳以内的延时精度。

/********************************************************************************************************* ** 函数名称: __timePassSpec ** 功能描述: 平静等待指定的短时间 ** 输 入 : ptv 短时间 ** 全局变量: NONE *********************************************************************************************************/
static VOID  __timePassSpec (const struct timespec  *ptv)
{ 
        
    struct timespec  tvEnd, tvNow;
    
    __timeGetHighResolution(&tvEnd);
    __timespecAdd(&tvEnd, ptv);
    do { 
        
        __timeGetHighResolution(&tvNow);
    } while (__timespecLeftTime(&tvNow, &tvEnd));
}

__timeGetHighResolution 函数能获取当前的高精度_K_tvTODMono时间,需要bsp提供bspTickHighResolution函数实现支持才行。

/********************************************************************************************************* ** 函数名称: __timeGetHighResolution ** 功能描述: 获得当前高精度时间 (使用 CLOCK_MONOTONIC) ** 输 入 : ptv 高精度时间 ** 全局变量: NONE *********************************************************************************************************/
static VOID  __timeGetHighResolution (struct timespec  *ptv)
{ 
        
    INTREG  iregInterLevel;
    
    LW_SPIN_KERN_LOCK_QUICK(&iregInterLevel);
    *ptv = _K_tvTODMono;
    bspTickHighResolution(ptv);                                         /* 高精度时间分辨率计算 */                                                                /* LW_CFG_TIME_HIGH_RESOLUT... */
    LW_SPIN_KERN_UNLOCK_QUICK(iregInterLevel);
}

心跳处理

系统心跳中断会对等待唤醒链表进行检查和处理,因为链表是递增的,只需检查首项就行,如果首项时间未到则都未到,如果首项时间到,则删除该节点并处理,然后还需要检查新的首项,所以中断里的操作是O(1)算法。

/********************************************************************************************************* ** 函数名称: _ThreadTick ** 功能描述: 扫描等待唤醒链表, tick 处理函数 ** 输 入 : NONE ** 输 出 : NONE *********************************************************************************************************/
VOID  _ThreadTick (VOID)
{ 
        
    INTREG                 iregInterLevel;
    PLW_CLASS_TCB          ptcb;
    PLW_CLASS_PCB          ppcb;
    PLW_CLASS_WAKEUP_NODE  pwun;
    ULONG                  ulCounter = 1;
    LW_CLASS_WAKEUP        pwu = &_K_wuDelay;
    
    iregInterLevel = __KERNEL_ENTER_IRQ();      /* 进入内核同时关闭中断 */
    
    //获取队列首项,循环执行,直到队列为空
    pwu->WU_plineOp = pwu->WU_plineHeader;
    while (pwu->WU_plineOp) { 
            
        pwun = _LIST_ENTRY(pwu->WU_plineOp, LW_CLASS_WAKEUP_NODE, WUN_lineManage);  
        //检查首项等待时间是否已到,未到则更新等待时间,并退出循环检查
        if (pwun->WUN_ulCounter > ulCounter) { 
            
            pwun->WUN_ulCounter -= ulCounter; 
            break;  
        }
        //等待时间已到
        //更新等待时间
        ulCounter -= pwun->WUN_ulCounter; 
        pwun->WUN_ulCounter = 0;
        //获取线程控制块
        ptcb = _LIST_ENTRY(pwun, LW_CLASS_TCB, TCB_wunDelay);
        //将线程从等待链中删除
        ptcb->TCB_usStatus &= ~LW_THREAD_STATUS_DELAY;
        _WakeupDel(pwu, &ptcb->TCB_wunDelay, LW_FALSE);
        //检查是否在等待事件
        if (ptcb->TCB_usStatus & LW_THREAD_STATUS_PEND_ANY) { 
                       /* 检查是否在等待事件 */
            ptcb->TCB_usStatus &= (~LW_THREAD_STATUS_PEND_ANY);             /* 等待超时清除事件等待位 */
            ptcb->TCB_ucWaitTimeout = LW_WAIT_TIME_OUT;                     /* 等待超时 */
            //将一个线程从事件等待队列中解锁
            if (ptcb->TCB_peventPtr) { 
        
                _EventUnQueue(ptcb);
            }  
        } else { 
        
            ptcb->TCB_ucWaitTimeout = LW_WAIT_TIME_CLEAR;                   /* 没有等待事件 */
        }
        //检查线程是否就绪,如果就绪则加入就绪表
        if (__LW_THREAD_IS_READY(ptcb)) { 
                                           /* 检查是否就绪 */
            ptcb->TCB_ucSchedActivate = LW_SCHED_ACT_OTHER;
            ppcb = _GetPcb(ptcb);                                           /* 获得优先级控制块 */
            __ADD_TO_READY_RING(ptcb, ppcb);                                /* 加入就绪环 */
        }
    
        //处理过程中是关中断的,但可能会循环多次,为了避免长时间影响高优先级中断响应,这里要开关一下中断
        KN_INT_ENABLE(iregInterLevel);                                      /* 这里允许响应中断 */
        iregInterLevel = KN_INT_DISABLE();
        //获取新的队列首项,循环执行,直到队列为空
        pwu->WU_plineOp = _list_line_get_next(pwu->WU_plineOp);
    }
    
    __KERNEL_EXIT_IRQ(iregInterLevel);                                  /* 退出内核同时打开中断 */
}
锐单商城拥有海量元器件数据手册IC替代型号,打造电子元器件IC百科大全!

相关文章