基本原理
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); /* 退出内核同时打开中断 */ }