μcosIII任务调度原理解析
回过头来翻了翻自己在CSDN写的第一篇博客,感觉几年前写的有些不清晰的地方,虽然这块工作内容已经不太涉及了,而且,现在单片机操作系统用的更多的freertos这种开源免费或者rthread、LiteOS这种国产的。不过还是有始有终吧。希望能给读者一些帮助。
前言
μcosIII是一个源码公开的的商业嵌入式实时操作系统内核,在公司的前人留下的几个项目都运行在该内核上,在一次添加功能发现软delay时间到后该任务无法正常唤醒,即使该任务优先级最高,研究了一下μcosIII的源代码才发现是前人不知道由于什么原因将TICK任务置于较低的优先级,该任务的功能是跟踪延时或者等待超时之类操作的任务,正常来说该任务应该是较高的优先级。该文用于记录自己对排查bug过程中对μcosIII任务调度的一些思考,错误之处还望指正。
μcosIII任务调度相关的数据结构
任务控制块 OS_TCB
该结构体成员较多,与本文相关的部分的数据成员与注释如下
OS_TCB *NextPtr; //用于任务就绪表中,链表方便删插
OS_TCB *PrevPtr;
OS_TCB *TickNextPtr; //时钟节拍轮上延时或者等待事件超时等任务组成双向链表
OS_TCB *TickPrevPtr;
OS_TICK_SPOKE *TickSpokePtr; /*在哪时钟节拍轮辐条上 */
OS_PEND_DATA *PendDataTblPtr; /*该任务的等待事件表(一维数组)*/
OS_OBJ_QTY PendDataTblEntries; /* 该任务的等待事件数量*/
OS_STATE PendOn; /* 标志是否正在等待*/
OS_STATUS PendStatus; /*等待结果*/
OS_STATE TaskState; /*任务状态*/
OS_PRIO Prio; /*优先级*/
/*以下三个用于记录延时或超时 */
OS_TICK TickCtrPrev;
OS_TICK TickCtrMatch;
OS_TICK TickRemain;
/*以下两个 时间片轮转相关*/
OS_TICK TimeQuanta;
OS_TICK TimeQuantaCtr;
就绪任务表结构
μcosIII的任务表简单来说是一个双向链表结构体数组,存储当前就绪任务,大小由OS_CFG_PRIO_MAX(OS_CFG.H)该宏定义决定,数组下标为优先级,越小优先级越高,链表通过 OS_TCB的NextPtr和PrevPtr连接,结构体如下
struct os_rdy_list {
OS_TCB *HeadPtr; /*双向任务链表的头 */
OS_TCB *TailPtr; /* 双向任务链表的尾 */
OS_OBJ_QTY NbrEntries; /* 该优先级下的就绪任务数量 */
};
当然以上设计其实是为了μcosIII加入的新特性——“时间片轮转调度”,链表方便快速插入删除任务,ucosii中就绪表为简单数组,且不允许相同优先级任务,对于时间片轮转本文不细讲。
就绪任务表定义(os.h):
/* READY LIST --------------------------------- */
OS_EXT OS_RDY_LIST OSRdyList[OS_CFG_PRIO_MAX]; /* Table of tasks ready to run */
μcosIII时间节拍轮
struct os_tick_spoke {
OS_TCB *FirstPtr; /* 辐条中延时或者超时的最近结束的任务节点,并按结束时间远近依次用TickNextPtr,TickPrevPtr连接 */
OS_OBJ_QTY NbrEntries; /* 当前辐条中的任务数量*/
OS_OBJ_QTY NbrEntriesMax; /* 辐条允许最大数量*/
};
μcosIII系统节拍一般是硬件平台定时产生的周期性信号,如stm32中一般是SysTick中断作为信号源,而节拍轮则是一个看作大小为OS_CFG_TICK_WHEEL_SIZE(OS_CFG_APP.H)的环形结构,将delay任务或者等待超时任务按结束tick取余分开管理,而环形中的一个节点称为SPOKE(辐条),辐条主要结构是等待任务的OS_TCB中的TickNextPtr,TickPrevPtr链接而成的双向链表,任务等待结束后移出辐条中的链表,并插入就绪任务表。
节拍轮也是一个很巧妙的设计,在一个操作系统中有可能有非常多的任务在等待中,而环状设计可以快速获取哪些任务已经等待结束可以进入就绪态。
时钟节拍轮定义
os_cfg_app.c
OS_TICK_SPOKE OSCfg_TickWheel [OS_CFG_TICK_WHEEL_SIZE];
任务阻塞表
该表用于记录任务等待各种信号量,消息队列和事件标志组,跟前面两个表不同,任务阻塞 表存储的链表节点并不是任务控制块OS_TCB,而是OS_PEND_DATA,这是因为任务可以同时等待多个内核对象,即PendMulti。
从以下代码可以看出OS_SEM(os_mutex,os_q,os_flag_GRP等同)包含OS_PEND_OBJ(指前几个成员变量相同,在OS_SEM等处理的时候都会强转成OS_PEND_OBJ指针处理),而OS_PEND_OBJ又包含OS_PEND_LIST,OS_PEND_LIST是一个由OS_PEND_DATA组成的链表,每个OS_PEND_DATA节点都可根据TCBPtr找到相应的任务,OS_TCB也可根据指针找到对应的OS_PEND_DATA。
struct os_pend_data { //OS_tcb拥有的等待事件
OS_PEND_DATA *PrevPtr;
OS_PEND_DATA *NextPtr;
OS_TCB *TCBPtr;
OS_PEND_OBJ *PendObjPtr;
OS_PEND_OBJ *RdyObjPtr;
void *RdyMsgPtr;
OS_MSG_SIZE RdyMsgSize;
CPU_TS RdyTS;
};
struct os_pend_list { //信号量对象拥有的任务阻塞表
OS_PEND_DATA *HeadPtr; /*表头,顺序由等待队列中优先级最高排在前面*/
OS_PEND_DATA *TailPtr; /*表尾*/
OS_OBJ_QTY NbrEntries; /* 数量*/
};
struct os_pend_obj { //信号量对象公共成员
OS_OBJ_TYPE Type;
CPU_CHAR *NamePtr;
OS_PEND_LIST PendList; /* List of tasks pending on object */
#if OS_CFG_DBG_EN > 0u
void *DbgPrevPtr;
void *DbgNextPtr;
CPU_CHAR *DbgNamePtr;
#endif
};
struct os_sem { //信号量对象
OS_OBJ_TYPE Type; /* Semaphore */
CPU_CHAR *NamePtr; /* Should be set to OS_OBJ_TYPE_SEM */
OS_PEND_LIST PendList; /* Pointer to Semaphore Name (NUL terminated ASCII) */
#if OS_CFG_DBG_EN > 0u
OS_SEM *DbgPrevPtr;
OS_SEM *DbgNextPtr;
CPU_CHAR *DbgNamePtr;
#endif
OS_SEM_CTR Ctr; /* List of tasks waiting on event flag group */
CPU_TS TS;
};
值得一提的是除了以上几个任务调度中较为重要的数据结构,μcos同时也维护着OSPrioTbl表,该表是一个u32的数组,ucos比较巧妙的通过位运算从该表可以快速获取就绪任务的最大优先级。
任务调度实现细节
任务调度在μcosiii通过OSSched()和OSIntExit() 执行。OSSched()是任务级调度,而OSIntExit()在中断服务函数中执行(通知系统出中断执行调度)。
两个函数源代码代码实现如下,基本大同小异,逻辑基本都是检查状态是否正常且可以调度,是则关中断找到最高优先级获取任务控制块,触发pendSV以上下文切换,最后开中断。
void OSSched (void)
{
CPU_SR_ALLOC();
if (OSIntNestingCtr > (OS_NESTING_CTR)0) { /* ISRs still nested? */
return; /* Yes ... only schedule when no nested ISRs */
}
if (OSSchedLockNestingCtr > (OS_NESTING_CTR)0) { /* Scheduler locked? */
return; /* Yes */
}
CPU_INT_DIS();
OSPrioHighRdy = OS_PrioGetHighest(); /* Find the highest priority ready */
OSTCBHighRdyPtr = OSRdyList[OSPrioHighRdy].HeadPtr;
if (OSTCBHighRdyPtr == OSTCBCurPtr) { /* Current task is still highest priority task? */
CPU_INT_EN(); /* Yes ... no need to context switch */
return;
}
#if OS_CFG_TASK_PROFILE_EN > 0u
OSTCBHighRdyPtr->CtxSwCtr++; /* Inc. # of context switches to this task */
#endif
OSTaskCtxSwCtr++; /* Increment context switch counter */
OS_TASK_SW(); /* Perform a task level context switch */
CPU_INT_EN();
}
void OSIntExit (void)
{
CPU_SR_ALLOC();
if (OSRunning != OS_STATE_OS_RUNNING) { /* Has the OS started? */
return; /* No */
}
CPU_INT_DIS();
if (OSIntNestingCtr == (OS_NESTING_CTR)0) { /* Prevent OSIntNestingCtr from wrapping */
CPU_INT_EN();
return;
}
OSIntNestingCtr--;
if (OSIntNestingCtr > (OS_NESTING_CTR)0) { /* ISRs still nested? */
CPU_INT_EN(); /* Yes */
return;
}
if (OSSchedLockNestingCtr > (OS_NESTING_CTR)0) { /* Scheduler still locked? */
CPU_INT_EN(); /* Yes */
return;
}
OSPrioHighRdy = OS_PrioGetHighest(); /* Find highest priority */
OSTCBHighRdyPtr = OSRdyList[OSPrioHighRdy].HeadPtr; /* Get highest priority task ready-to-run */
if (OSTCBHighRdyPtr == OSTCBCurPtr) { /* Current task still the highest priority? */
CPU_INT_EN(); /* Yes */
return;
}
#if OS_CFG_TASK_PROFILE_EN > 0u
OSTCBHighRdyPtr->CtxSwCtr++; /* Inc. # of context switches for this new task */
#endif
OSTaskCtxSwCtr++; /* Keep track of the total number of ctx switches */
OSIntCtxSw(); /* Perform interrupt level ctx switch */
CPU_INT_EN();
}
任务调度点
- 释放事件,即执行OS???Post(), 执行完会调度,当然也可手动配置参数不调度
- 延时函数,即OSTimeDlyHMSM()等 ,当前任务插入时间节拍轮,移出就绪任务表,并执行调度;
- 等待事件,即执行OS???Pend(),任务插入任务阻塞表,若指定超时还将插入时间节拍轮,而后任务调度;
- 取消等待,即OS???PendAbort();
- 创建或删除任务,OSTaskCreate()与OSTaskDel();
- 删除内核对象(信号量,消息队列等),等待该对象的阻塞任务会进入就绪状态并调度;
- 改变任务优先级;
- 任务挂起和取消挂起,即OSTaskSuspend()与OSTaskResume();
- 退出所有的嵌套中断(我的遇到问题是该情况下导致的);
- 任务调度器解锁,即OSSchedUnlock(),会立即执行一次调度;
- 用户主动调用OSSched();
- 时间片轮转中,任务调用OSSchedRoundRobinYield()主动放弃其执行时间片(略)。
退出所有的嵌套中断可以理解成从硬件中断退到系统内核,因发生硬件中断可能让等待的高优先级任务恢复就绪态,系统也应当执行一次任务调度以保证实时性,通过OSIntExit()。
个人认为调度点大致可以分为以下三类,时钟节拍轮相关调度,任务阻塞表相关调度,就绪任务表相关调度(用户主动修改就绪任务表)。
时钟节拍轮相关调度
一般来说在操作系统中最频繁的硬件中断是系统时钟源中断——STM32中的SysTick,该中断联合OS_TickTask任务以处理系统中的延时及Pend超时。逻辑如下
OS_TickTask该任务应当在一个较高的优先级( 建议设1),以在时钟源中断后很快就能获取到信号量将延时结束的任务恢复就绪状态,用于下一次调度。本人程序之前将该任务优先级设置为倒数第三低,导致迟迟无法将延时结束的任务插入就绪队列中,导致延时任务无法继续执行。
void SysTick_Handler(void)
{
CPU_SR_ALLOC();
CPU_CRITICAL_ENTER();
OSIntNestingCtr++; /* Tell uC/OS-III that we are starting an ISR */
CPU_CRITICAL_EXIT();
OSTimeTick(); /* 释放信号量,在出中断后唤醒OS_TickTask*/
OSIntExit(); /* Tell uC/OS-III that we are leaving the ISR*/
}
void OS_TickTask (void *p_arg)
{
OS_ERR err;
CPU_TS ts;
p_arg = p_arg; /* Prevent compiler warning */
while (DEF_ON) {
(void)OSTaskSemPend((OS_TICK )0,
(OS_OPT )OS_OPT_PEND_BLOCKING,
(CPU_TS *)&ts,
(OS_ERR *)&err); /* Wait for signal from tick interrupt */
if (err == OS_ERR_NONE) {
if (OSRunning == OS_STATE_OS_RUNNING) {
OS_TickListUpdate(); /* 处理节拍轮 */
}
}
}
}
根据以上分析,也可以得知,SysTick频率越高,调度点越多,实时性越强,软件delay也越准,但处理器负担也越重,一般给该频率依照处理器性能一般在10~1000HZ之间。
任务阻塞表相关调度
主要产生在于任务各类Pend,Post及PendAbort()操作,Pend若能立刻获取资源,则不会调度,继续执行该任务;若无资源则阻塞该任务(设置超时选项还将插入时间节拍轮,当然也可设置不阻塞选项),将该任务的OS_PEND_DATA设置进资源的任务阻塞表(OS_PEND_LIST),移出就绪任务表,再任务调度,直到等待的资源Post,可以单独通知(也可根据选项OS_OPT_POST_ALL设置为广播模式)该事件的pendlist 的表头(等待任务中优先级最高的任务), 唤醒等待该事件的任务。
就绪任务表相关调度
该类指在用户主动修改就绪表操作下的产生调度,如创删任务,挂起,取消挂起,修改任务优先级。
其他
看到这里,可能有人发现,上面的分类没有把调度点最后三点没有囊括到,个人认为这三点应当属于用户让系统强加的调度点,并不属于系统本身的调度。
- 任务调度器解锁,即OSSchedUnlock(),会立即执行一次调度;
- 用户主动调用OSSched();
- 时间片轮转中,任务调用OSSchedRoundRobinYield()主动放弃其执行时间片(略)