uCos的多任务实现
uCos的多任务实现
作为操作系统(OS),最基本的一项服务就是提供多线程,在实时操作系统uCos里,多线程被称为多任务(Task)。多任务并不是CPU能真正同时运行多个程序,实际是靠CPU在多个任务之间转换切换实现的,CPU轮番的服务于一系列的任务,这样CPU在宏观上好像在同时执行多个任务,实际在微观上CPU绝对是“单任务”的。这里要注意区别多线程和多核,如果系统里是有多个CPU,则可以实现真正的多线程了。
按照上面的思路,多任务的实现,就是要实现CPU在不同的任务之间切换。按照uCos作者的话说:“就是不断的保存,恢复CPU的那些寄存器”。
我们知道uCos的多任务(这里以两个任务为例)的程序模型如下:
void Task_A(void *p_arg)
{
while(1)
{
OSTimeDly(10); //任务里必须有类似的主动释放CPU的函数
......
}
}
void Task_B(void *p_arg)
{
while(1)
{
OSTimeDly(10);
......
}
}
使用uCos,程序将在这两个任务之间轮换,这两个while(1)里的程序都可以得到执行。
我们知道,在裸机编程里,如果出现while(1)这样的语句,那么程序将永远在这个循环里执行(当然是要程序主动调用break除外),他是不会放弃CPU的,那为什么加了操作系统后,程序能在这两个while(1)之间轮换?
操作系统都需要使用“时钟节拍”技术来实现对任务的监控,并能主动调度和切换任务的执行。uCos也同样是使用“时钟节拍”来实现任务的监管的,以STM32单片机为例,一般用SysTick这个系统时钟定时器来实现,比如我们设置这个定时器10ms的定时间隔,那么每隔10ms都会调用下面的中断服务(所以移植的时候,需要实现这个函数):
void SysTickHandler(void)
{
OSIntEnter();
OSTimeTick();
OSIntExit();
}
假设现在程序在Task_A的while(1)里,然后SysTick中断来了,SysTickHandler服务程序开始执行,先执行OSIntEnter():
void OSIntEnter (void)
{
if (OSRunning == OS_TRUE) {
if (OSIntNesting < 255u) {
OSIntNesting++;
}
}
}
这个函数很简单,他是uCos的内核函数,主要是给中断嵌套计数器OSIntNesting加一,因为uCos内核需要实时的判断程序的当前执行是不是在中断里,要知道,大部分的处理器是可以中断嵌套的,这里我们先不管判断程序是不是在中断里有什么用,后面马上会说到。
然后开始执行OSTimeTick(),这个函数我们只分析关键代码,也就是跟任务管理有关的代码:
void OSTimeTick (void)
{
OS_TCB *ptcb;
#if OS_TICK_STEP_EN > 0
BOOLEAN step;
#endif
#if OS_CRITICAL_METHOD == 3 /* Allocate storage for CPU status register */
OS_CPU_SR cpu_sr = 0;
#endif
#if OS_TIME_TICK_HOOK_EN > 0
OSTimeTickHook(); /* Call user definable hook */
#endif
#if OS_TIME_GET_SET_EN > 0
OS_ENTER_CRITICAL(); /* Update the 32-bit tick counter */
OSTime++;
OS_EXIT_CRITICAL();
#endif
if (OSRunning == OS_TRUE) {
#if OS_TICK_STEP_EN > 0
switch (OSTickStepState) { /* Determine whether we need to process a tick */
case OS_TICK_STEP_DIS: /* Yes, stepping is disabled */
step = OS_TRUE;
break;
case OS_TICK_STEP_WAIT: /* No, waiting for uC/OS-View to set ... */
step = OS_FALSE; /* .. OSTickStepState to OS_TICK_STEP_ONCE */
break;
case OS_TICK_STEP_ONCE: /* Yes, process tick once and wait for next ... */
step = OS_TRUE; /* ... step command from uC/OS-View */
OSTickStepState = OS_TICK_STEP_WAIT;
break;
default: /* Invalid case, correct situation */
step = OS_TRUE;
OSTickStepState = OS_TICK_STEP_DIS;
break;
}
if (step == OS_FALSE) { /* Return if waiting for step command */
return;
}
#endif
ptcb = OSTCBList; /* Point at first TCB in TCB list */
while (ptcb->OSTCBPrio != OS_TASK_IDLE_PRIO) { /* Go through all TCBs in TCB list */
OS_ENTER_CRITICAL();
if (ptcb->OSTCBDly != 0) { /* No, Delayed or waiting for event with TO */
if (--ptcb->OSTCBDly == 0) { /* Decrement nbr of ticks to end of delay */
/* Check for timeout */
if ((ptcb->OSTCBStat & OS_STAT_PEND_ANY) != OS_STAT_RDY) {
ptcb->OSTCBStat &= ~OS_STAT_PEND_ANY; /* Yes, Clear status flag */
ptcb->OSTCBPendTO = OS_TRUE; /* Indicate PEND timeout */
} else {
ptcb->OSTCBPendTO = OS_FALSE;
}
if ((ptcb->OSTCBStat & OS_STAT_SUSPEND) == OS_STAT_RDY) { /* Is task suspended? */
OSRdyGrp |= ptcb->OSTCBBitY; /* No, Make ready */
OSRdyTbl[ptcb->OSTCBY] |= ptcb->OSTCBBitX;
}
}
}
ptcb = ptcb->OSTCBNext; /* Point at next TCB in TCB list */
OS_EXIT_CRITICAL();
}
}
}
在这个函数里,出现了一个全局变量OSTCBList,这个变量是uCos内核里任务控制块(TCB)链表的表头指针,问题又来了,TCB是什么,TCB链表又是什么?
原来uCos的每个任务都需要有一个TCB来管理,这个TCB记录了该任务的所有信息,同时uCos用链表来管理所有的这些TCB,TCB的结构如下:
typedef struct os_tcb {
OS_STK *OSTCBStkPtr; /* Pointer to current top of stack */
#if OS_TASK_CREATE_EXT_EN > 0
void *OSTCBExtPtr; /* Pointer to user definable data for TCB extension */
OS_STK *OSTCBStkBottom; /* Pointer to bottom of stack */
INT32U OSTCBStkSize; /* Size of task stack (in number of stack elements) */
INT16U OSTCBOpt; /* Task options as passed by OSTaskCreateExt() */
INT16U OSTCBId; /* Task ID (0..65535) */
#endif
struct os_tcb *OSTCBNext; /* Pointer to next TCB in the TCB list */
struct os_tcb *OSTCBPrev; /* Pointer to previous TCB in the TCB list */
#if OS_EVENT_EN
OS_EVENT *OSTCBEventPtr; /* Pointer to event control block */
#endif
#if ((OS_Q_EN > 0) && (OS_MAX_QS > 0)) || (OS_MBOX_EN > 0)
void *OSTCBMsg; /* Message received from OSMboxPost() or OSQPost() */
#endif
#if (OS_VERSION >= 251) && (OS_FLAG_EN > 0) && (OS_MAX_FLAGS > 0)
#if OS_TASK_DEL_EN > 0
OS_FLAG_NODE *OSTCBFlagNode; /* Pointer to event flag node */
#endif
OS_FLAGS OSTCBFlagsRdy; /* Event flags that made task ready to run */
#endif
INT16U OSTCBDly; /* Nbr ticks to delay task or, timeout waiting for event */
INT8U OSTCBStat; /* Task status */
BOOLEAN OSTCBPendTO; /* Flag indicating PEND timed out (OS_TRUE == timed out) */
INT8U OSTCBPrio; /* Task priority (0 == highest) */
INT8U OSTCBX; /* Bit position in group corresponding to task priority */
INT8U OSTCBY; /* Index into ready table corresponding to task priority */
#if OS_LOWEST_PRIO <= 63
INT8U OSTCBBitX; /* Bit mask to access bit position in ready table */
INT8U OSTCBBitY; /* Bit mask to access bit position in ready group */
#else
INT16U OSTCBBitX; /* Bit mask to access bit position in ready table */
INT16U OSTCBBitY; /* Bit mask to access bit position in ready group */
#endif
#if OS_TASK_DEL_EN > 0
INT8U OSTCBDelReq; /* Indicates whether a task needs to delete itself */
#endif
#if OS_TASK_PROFILE_EN > 0
INT32U OSTCBCtxSwCtr; /* Number of time the task was switched in */
INT32U OSTCBCyclesTot; /* Total number of clock cycles the task has been running */
INT32U OSTCBCyclesStart; /* Snapshot of cycle counter at start of task resumption */
OS_STK *OSTCBStkBase; /* Pointer to the beginning of the task stack */
INT32U OSTCBStkUsed; /* Number of bytes used from the stack */
#endif
#if OS_TASK_NAME_SIZE > 1
INT8U OSTCBTaskName[OS_TASK_NAME_SIZE];
#endif
} OS_TCB;
这结构体稍微有点大,这里我们先知需要关心OSTCBDly这个成员,这个成员记录该任务的延时值,当我们调用uCos的系统函数OSTimeDly(),OSQPend()等这些阻塞类型的函数时,该任务的TCB成员OSTCBDly就会被赋予相应值。
我们继续分析OSTimeTick(),这个函数,在这个函数里,主要是给每个TCB的OSTCBDly的计数值减一,如果OSTCBDly为零了,则在任务就绪表里标记该任务,这里又引出了一问题,什么是就绪表,uCos的就绪表的实现用了一个比较巧妙的算法,这里先不仔细去分析,只有知道就绪表里记录了当前系统中,哪些任务是处于就绪态,也就是可被CPU执行的状态。
好,到这里OSTimeTick()分析完了,他完成了对每个任务的延时值减一操作,并更新了就绪表。下面开始执行OSIntExit(),这个函数是个很关键的函数,她将实现任务的上下文切换。
OSIntExit函数的源码如下:
void OSIntExit (void)
{
#if OS_CRITICAL_METHOD == 3
OS_CPU_SR cpu_sr = 0;
#endif
if (OSRunning == OS_TRUE) { /* 如果uCos还没有启动运行,则不进行任务调度,因为这时候uCos的初始化还没完成 */
OS_ENTER_CRITICAL();
if (OSIntNesting > 0) { /* 将中断嵌套计数器减一 */
OSIntNesting--;
}
if (OSIntNesting == 0) { /* 只有在中断嵌套为0,也就是即将退出中断时才进行任务调度 */
if (OSLockNesting == 0) {
OS_SchedNew();/* 找出当前就绪表中的最高优先级任务,并更新全局变量OSPrioHighRdy */
if (OSPrioHighRdy != OSPrioCur) {/*如果发现当前最高优先级任务不是正在运行的任务,就要进行任务切换*/
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
#if OS_TASK_PROFILE_EN > 0
OSTCBHighRdy->OSTCBCtxSwCtr++;
#endif
OSCtxSwCtr++;
OSIntCtxSw(); /*调用专门用于中断服务里的上下文切换函数,进行山下文的切换 */
}
}
}
OS_EXIT_CRITICAL();
}
}
从上面的源码和代码注释可以总结出该函数完成的功能为:找出就绪表里的最高优先级任务,并执行OSIntCtxSw函数来进行任务切换。这里要注意,虽然执行了任务切换,但不会立刻进行上下文的切换,这个实现过程跟CPU的硬件有关,在STM32中,上下文的切换是用的“悬起中断”,该中断只有当CPU的所有中断完成了,也就是退出了所有的中断嵌套后,才会执行。OSIntCtxSw函数是移植uCos的一个非常关键的函数,他负责恢复运行任务的上次中断现场,这个函数跟CPU体系有紧密的联系,这里先不仔细分析。
到这里我们基本可以看到uCos利用系统时钟滴答实现多任务的大概流程如下:
uCos的任务切换过程就分析跟踪完了,这里要注意几点,任务的切换并不只会发生在系统时钟滴答的中断服务里,调用发送信号量,发送消息等这些系统函数时也会引起任务切换,但大致的原理都差不多,这里我们只对程序的原理和框架做了说明,实际还是有些细节需要处理的。