uCOS2内核调度原理
作者:JCY
来自09级安徽宿州学院电子创新实验室
此文中对uCOS2内核调度的理解,若有错误之处请指出,不胜感激!
在uCOS2操作系统当中有程序会处于五种状态:运行态、就绪态、挂起态、睡眠态、中断服务态。
运行态:某一个任务正在运行,独占CPU的使用权。
l 就绪态:某一个任务已经有了运行的准备,可以随时被调度。
l 挂起态:某一个任务需要等待某一个事件的发生,例如等待信号量、互斥信号量、时间到等。
l 睡眠状态:任务被任务删除函数后,不能再被操作系统调度。虽然在程序存储区还存在该任务,但是该任务永远都不会执行。
l 中断服务态:某一个中断发生后,就会执行中断服务程序,那么程序就会处于中断服务态。
具体各任务间的关系如下(来自于一本uCOS2方面的书籍):
从图中可以看出各任务之间就是通过调用各种系统函数来实现,这也是我们在编写程序时使用的的函数。
对于操作系统来说,我们只需要它提供的操作函数来操作系统提供的功能。在此基础上我们必须要了解每一个系统函数的函数原型,每一个参数的意义,参数限制等。但是这只是最低级的工作,了解每一个函数如何实现的,深入到操作系统的源码,才可以说对操作系统熟悉了。
操作系统的调度都围绕这一个数据结构体OS_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_FLAG_EN > 0)
OS_EVENT *OSTCBEventPtr; /* Pointer to event control block */
#endif
#if (OS_EVENT_EN) && (OS_EVENT_MULTI_EN > 0)
OS_EVENT **OSTCBEventMultiPtr; /* Pointer to multiple event control blocks */
#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_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 */
INT8U OSTCBStatPend; /* Task PEND status */
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;
任务控制块贯穿了整个uCOS2操作系统,如果不了解这个结构体在操作系统当中的用法,那就不可能对uCOS2深入的理解。
在Ucos_ii.h当中有一个
OS_EXT OS_TCB OSTCBTbl[OS_MAX_TASKS + OS_N_SYS_TASKS]; /* Table of TCBs */
它是定义一个或者外部声明一个OSTCBTbl的数组。这两个作用需要靠OS_GLOBALS是否宏定义来选择。当然在整个程序代码当中可能只有一个C文件中定义了OSTCBTbl变量,其他的C文件中只是对它进行外部变量声明。
在Linux操作系统编写增加链表和删除链表程序时,都会伴随着内存空间的申请和释放。但是在uCOS2操作系统当中没有这么强悍的内存管理。一个任务控制块OS_TCB管理者一个任务,所以OSTCBTbl的元素个数必须大于在应用程序当中建立的任务数。从定义中可以看出元素个数由两个宏定义决定,第一OS_MAX_TASKS:就是在应用程序当中可能建立的最大任务数,第二OS_N_SYS_TASKS:是调用系统函数后有系统建立的任务数。这两个数值是可以改变的,因为在程序当中必须要有一个空闲任务,而计算CPU效率的统计任务是由用户选择的,所以OS_N_SYS_TASKS的值受到OS_TASK_STAT_EN的控制。宏定义OS_TASK_STAT_EN的值决定了,是否使能统计任务。
#if OS_TASK_STAT_EN > 0
#define OS_N_SYS_TASKS 2u /* Number of system tasks */
#else
#define OS_N_SYS_TASKS 1u
#endif
任务控制块主要由两个链表来控制,一个是正在使用的任务控制块链表OSTCBList和空闲任务控制块链表OSTCBFreeList。OSTCBList是一个双向链表,只要知道了链表当中的一个元素就能够找到该链表中所有的元素,即能访问每一个任务。OSTCBFreeList是一个单向链表,存储着还没有使用的任务控制块。需要建立一个任务时,就会从空闲任务控制块OSTCBFreeList中取出一个任务控制块,并将空闲任务控制块指针OSTCBFreeList指向下一个空闲任务控制块。从空闲任务控制链表中取出的任务控制块(暂且把该任务控制块叫做OSTCB1)后,需要将人任务控制块加入到任务控制块双向链表OSTCBList中,该任务控制块指针指向了最后一个加入到任务控制块链表的任务控制块。将OSTCB1当中的任务控制块指针OSTCBNext指向OSTCBList指向的任务控制块,OSTCBList指向的任务控制块当中也有指向任务控制块的变量OSTCBPrev,将OSTCBPrev指向OSTCB1任务控制块。然后再将OSTCBList指向OSTCB1任务控制块,这样就将新的任务控制块加入到了任务控制块链表当中。
关系图如下:
我们来看一下,系统初始化后,两链表OSTCBList,OSTCBFreeList的形式。查看源码文件Os_core.c,在文件中有一个函数OSInit()。这个函数必须被应用程序调用,并且只能调用一次。该函数中调用了OS_InitTCBList。在OS_InitTCBList函数中对与任务控制块相关联的代码进行初始化。源码如下:
static void OS_InitTCBList (void)
{
INT8U i;
OS_TCB *ptcb1;
OS_TCB *ptcb2;
OS_MemClr((INT8U *)&OSTCBTbl[0], sizeof(OSTCBTbl)); /* Clear all the TCBs */
OS_MemClr((INT8U *)&OSTCBPrioTbl[0], sizeof(OSTCBPrioTbl)); /* Clear the priority table */
ptcb1 = &OSTCBTbl[0];
ptcb2 = &OSTCBTbl[1];
for (i = 0; i < (OS_MAX_TASKS + OS_N_SYS_TASKS - 1); i++) { /* Init. list of free TCBs */
ptcb1->OSTCBNext = ptcb2;
#if OS_TASK_NAME_SIZE > 1
ptcb1->OSTCBTaskName[0] = '?'; /* Unknown name */
ptcb1->OSTCBTaskName[1] = OS_ASCII_NUL;
#endif
ptcb1++;
ptcb2++;
}
ptcb1->OSTCBNext = (OS_TCB *)0; /* Last OS_TCB */
#if OS_TASK_NAME_SIZE > 1
ptcb1->OSTCBTaskName[0] = '?'; /* Unknown name */
ptcb1->OSTCBTaskName[1] = OS_ASCII_NUL;
#endif
OSTCBList = (OS_TCB *)0; /* TCB lists initializations */
OSTCBFreeList = &OSTCBTbl[0];
}
函数OS_MemClr可以对参数中所指示的空间进行清零。第一个参数是空间对象的首地址,第二个参数是需要清零的空间大小(以字节为单位)。对于具体的源码可以自行分析,在此不再叙述。
在程序当中蓝色的代码将OSTCBTbl中的所有的元素组成一个单向的链表,最有一个元素中的任务控制块指针OSTCBNext 指向空。
形式如下(摘自某书籍):
初始化的时候并没有任何的任务被创建,所以在程序当中OSTCBList指向了空。
在OSInit()当中调用OS_InitTaskIdle,OS_InitTaskIdle调用OSTaskCreateExt创建了一个空闲任务。在该函数中涉及到知识较多,先不表述。必要的基础部分写出后,在写这块。
uCOS2操作系统怎样标记某一个任务进入就绪状态呐!
先把涉及到的数据变量列出来,以供讲解。
OS_EXT INT8U OSRdyGrp; /* Ready list group */
OS_EXT INT8U OSRdyTbl[OS_RDY_TBL_SIZE]; /* Table of tasks which are ready to run */
把一本书上总结出来的图列出:
8*8的方框就是就绪表,在表中每一位代表一个优先级。例如OSRdyTbl[0].0代表第0优先级,即优先级最高。OSRdyTbl[8].7代表第63个优先级,即优先级最低。若某一个优先级处于就绪状态那么就将就绪表OSRdyTbl中的相应的位置零。OSRdyGrp也是一个8位的变量,它指示那个组当中有就绪的任务。OSRdyTbl每一个元素被称为一个组,若在组中有任务处于就绪状态那么就将OSRdyGrp中的相应的位置1,否则清零。例如优先级4处于就绪状态了,那么就将该OSRdyGrp的第零位置1,因为第四个优先级被划归第零组。其实OSRdyGrp的位号与组号相对应,而组号又和OSRdyTbl的元素下标号相同,所以这种关系就清楚了。
uCOS2是基于优先级调度算法的,怎样从OSRdyGrp和OSRdyTbl找到处于最高的就绪任务呐!
当然算法有很多,不使用OSRdyGrp也是可以找到的,但是uCOS2出于对调度时间的考虑,它使用了OSRdyGrp变量。在算法中加入了一些查找算法,是程序运行更快。
算法中使用的查找表有一个OSUnMapTbl,2.86版本的uCOS2还使用了OSMapTbl。
OSMapTbl表的功能也可以使用1<
INT8U const OSUnMapTbl[256] = {
0, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 0x00 to 0x0F */
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 0x10 to 0x1F */
5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 0x20 to 0x2F */
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 0x30 to 0x3F */
6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 0x40 to 0x4F */
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 0x50 to 0x5F */
5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 0x60 to 0x6F */
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 0x70 to 0x7F */
7, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 0x80 to 0x8F */
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 0x90 to 0x9F */
5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 0xA0 to 0xAF */
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 0xB0 to 0xBF */
6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 0xC0 to 0xCF */
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 0xD0 to 0xDF */
5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0, /* 0xE0 to 0xEF */
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0 /* 0xF0 to 0xFF */
};
这个表的意义很简单,就是给出一个8位的数字,然后找出其二进制数中1所占的最低位的位号。
例如
0x38 = (0011_1000)2经查表OSUnMapTbl[0x38]的值为4。
0x56 = (0101_1100)2经查表OSUnMapTbl[0x56]的值为2。
现在把查找优先级的代码列出如下:
INT8U y;
y = OSUnMapTbl[OSRdyGrp];
OSPrioHighRdy = (INT8U)((y << 3) + OSUnMapTbl[OSRdyTbl[y]]);
假设OSRdyGrp = 0x82 = (1000_0010);从OSRdyGrp 的值可以知道在所有就绪的优先级当中, 最高的那个优先级属于第一组。第一组有8个优先级,还要查看OSRdyTbl[1]中最高优先级处在哪个位置,进而可以确定最高优先级的优先级号。例如如果OSRdyTbl[1] = 0x55 = (0101_0101)2。那么查表后OSUnMapTbl[0x55] = 0,所以可以确定优先级为1<<3 + 0 = 8。
找到了最高的优先级了,那么怎样通过这个最高优先级号找到所对应的任务控制块呐?这是通过任务控制块优先级表OSTCBPrioTbl,这里面最放着任务控制块指针,数组的元素数等于最低优先级的加1,
并且会大于等于在应用程序建立的任务数和系统建立的任务数之和。若某一个任务的优先级为10,那么OSTCBPrioTbl[10]就只向优先级为10所对应的任务控制块。
OSTCBPrioTbl在Ucos_ii.h当中定义,定义的原型如下:
OS_EXT OS_TCB *OSTCBPrioTbl[OS_LOWEST_PRIO + 1];/* Table of pointers to created TCBs */
以便于下面的讲解,要说明的是,对于每一个任务都有自己的堆栈,并且任务的堆栈是由用户建立的。在任务控制块中含有指向堆栈的指针,在堆栈中存放着进行任务调度时需要保存的与本任务相关CPU寄存器值,和任务调用用函数时存放的一些中间值。
看看创建任务都做了哪些事情?以空闲任务的建立为例来说明。
(void)OSTaskCreateExt(OS_TaskIdle,
(void *)0, /* No arguments passed to OS_TaskIdle() */
&OSTaskIdleStk[OS_TASK_IDLE_STK_SIZE - 1], /* Set Top-Of-Stack */
OS_TASK_IDLE_PRIO, /* Lowest priority level */
OS_TASK_IDLE_ID,
&OSTaskIdleStk[0], /* Set Bottom-Of-Stack */
OS_TASK_IDLE_STK_SIZE,
(void *)0, /* No TCB extension */
OS_TASK_OPT_STK_CHK | OS_TASK_OPT_STK_CLR);/* Enable stack checking + clear stack */
参数说明:
OS_TaskIdle:空闲任务的任务函数,通常所说的任务的执行就是执行执行这个函数,任务的切换也是中断此函数或者从中断处继续执行此函数。
(void *)0:是传给任务函数的参数,对于每一个任务函数都会有一个参数,用来从外界传递重要的数据。
&OSTaskIdleStk[OS_TASK_IDLE_STK_SIZE - 1]:指向栈顶定的指针。
OS_TASK_IDLE_PRIO:此是优先级号,任务的每个优先级是不同的。
OS_TASK_IDLE_ID:是任务的ID,每个任务的ID是不同的,但可以和优先级号相同。
&OSTaskIdleStk[0]:指向栈底的指针,由此可知栈的增长方向是向下增长的。
OS_TASK_IDLE_STK_SIZE:空闲任务的栈空间大小。当然这个可以由栈顶和栈底的地址得到。
(void *)0:对任务控制块的数据进行扩展,NULL代表不对任务控制块扩展
OS_TASK_OPT_STK_CHK | OS_TASK_OPT_STK_CLR:这是任务的属性。使能任务的核查,和在没有使用任务栈时,对该任务的整个栈空间进行初始化。
传递的参数是不是非常明了呀,那我们就看看OSTaskCreateExt的实现吧!该函数中主要的代码如下:
OS_ENTER_CRITICAL();
if (OSIntNesting > 0) { /* Make sure we don't create the task from within an ISR */
OS_EXIT_CRITICAL();
return (OS_ERR_TASK_CREATE_ISR);
}
if (OSTCBPrioTbl[prio] == (OS_TCB *)0) { /* Make sure task doesn't already exist at this priority */
OSTCBPrioTbl[prio] = OS_TCB_RESERVED;/* Reserve the priority to prevent others from doing ... */
/* ... the same thing until task is created. */
OS_EXIT_CRITICAL();
#if (OS_TASK_STAT_STK_CHK_EN > 0)
OS_TaskStkClr(pbos, stk_size, opt); /* Clear the task stack (if needed) */
#endif
psp = OSTaskStkInit(task, p_arg, ptos, opt); /* Initialize the task's stack */
err = OS_TCBInit(prio, psp, pbos, id, stk_size, pext, opt);
if (err == OS_ERR_NONE) {
if (OSRunning == OS_TRUE) { /* Find HPT if multitasking has started */
OS_Sched();
}
} else {
OS_ENTER_CRITICAL();
OSTCBPrioTbl[prio] = (OS_TCB *)0; /* Make this priority avail. to others */
OS_EXIT_CRITICAL();
}
return (err);
}
OS_EXIT_CRITICAL();
return (OS_ERR_PRIO_EXIST);
由红色的代码可知,不能再中断函数中调用此函数,否则会产生错误返回。蓝色的代码指示先把OSTCBPrioTbl的优先级占用,防止在执行非临界代码时发生任务的切换,而在将要正在执行的任务当中也执行任务创造函数,并且两者的优先级相同。
这一部分代码就是对任务的栈空间进行清零,具体的源码如下:
在此函数中的核心代码如下:
然后就开始执行任务堆栈初始化函数OSTaskStkInit()。此函数就是初始化栈顶的一部分空间,这部分栈内容存放着该任务在CPU当中的状态。需要执行该任务时只需要把该空间的值存放在CPU寄存器当中就可以了。当时这只使用于第一次执行该任务的时候。若执行该任务超过了两个或者两次以上,需要存放到CPU寄存器在栈中的位置就不确定了。任务堆栈初始化函数如下:
图中的p_arg就是在第一次调用该任务时所需要的参数,该参数是从任务创造函数中依次传递过来的。这里面的值有些是可以改动的。如R0-R11的值,R0和PC是不能改动的。
因为这是第一次调用任务创造函数,在调用之前OSRunning被赋值为假了。该语句是在OS_InitMisc函数中定义的,代码如下图:
创建任务就介绍完了。 假设建立了两个任务,然后开始执行多任务。这是通过在主函数中调用 OSStart来实现的。代码如下图:
该函数首先就是调用OS_SchedNew函数找到最高优先级的优先级号,然后将任务优先级号放在OSPrioHighRdy当中。代码图如下:
这部分的代码看起来虽然长,但是不难。如果最低优先级为63,那么只会编译最上面的三行代码。如果你的最低优先级超过了63,那么就是只会编译下面的代码。所以我们可以知道uCOS2的2.86版本支持256的优先级。对于当256个优先级的就续表查找方法和最低优先级为63个的查找方法是相同的。在这里就不在赘述了。
然后继续执行OSStart函数。接下来就是对当前优先级OSPrioCur、就绪态最高优先级的任务控制块指针OSTCBHighRdy、当前要执行的任务控制块指针OSTCBCur进行赋值。这三个变量都是全局变量,存储正在执行的任务信息。执行每一个任务的时候都会更新。OSTCBCur和OSTCBHighRdy指向的任务控制块是相同的。
然后开始执行函数OSStartHighRdy。在C语言文件当中你是查不到这个函数的,即使找也只是找到它的定义。对于函数的实现是在汇编代码当中。那么就进入这个汇编文件OS_CPU_ASM.asm吧!
OSStartHighRdy函数的代码如下:
OSStartHighRdy
LDR R0, =NVIC_SYSPRI14 ; Set the PendSV exception priority
LDR R1, =NVIC_PENDSV_PRI
STRB R1, [R0]
MOVS R0, #0 ; Set the PSP to 0 for initial context switch call
MSR PSP, R0
LDR R0, =OSRunning ; OSRunning = TRUE
MOVS R1, #1
STRB R1, [R0]
LDR R0, =NVIC_INT_CTRL ; Trigger the PendSV exception (causes context switch)
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
CPSIE I ; Enable interrupts at processor level
OSStartHang
B OSStartHang ; Should never get here
在该程序中的最后一个语句,就是一个while(1);说明程序调用OSStartHighRdy函数后永远不会退出。
在代码中红色的部分是为了设置PendSV的优先级,设置为最低0xff;NVIC_SYSPRI14 是系统处理器优先级寄存器的地址。具体的解释是在CM3技术参考手册里面,截图如下:
蓝色部分是将CPU的堆栈指针设置为0,而后再对OSRunning全局变量设置为1,这个变量指示了操作系统处在运行状态。
该函数代码是为了使用软件的方式触发PendSV中断。接着程序一直在处于死循环当中。开始执行PendSV 中断服务程序。中断服务程序的代码如下:
代码图片不太清楚,放大之后还是蛮清楚的。即使不清楚还是用图,是因为可以看到后面的注释。
第一个执行的语句是 CPSID I
这个语句是为了禁止中断,此中断服务程序就是为了做任务之间的切换,在任务切换期间允许有被打断,因为设置的中断优先级最低,所以禁止中断时非常有必要的。
第二个语句就是为了获得CPU堆栈中的值给R0,我们知道此中断第一次执行时栈指针寄存器PSP的值为0(调用OSStartHighRdy函数时)。
CBZ 是一个跳转指令,判断R0的值是否为0,为0则跳转。
下面的代码只有在第一次执行中断服务程序时才不会执行,在以后的再进入该中断服务程序还会都会执行此代码。
SUBS R0, R0, #0x20 ; Save remaining regs r4-11 on process stack
STM R0, {R4-R11};将当前处理器的R4-R11的值存储到任务控制块的堆栈当中
LDR R1, =OSTCBCur ; OSTCBCur->OSTCBStkPtr = SP;
LDR R1, [R1]
STR R0, [R1]; R0 is SP of process being switched out将当前的堆栈指针保存到任务控制块的第一个栈指针域
之后每次进入OS_CPU_PendSVHandler函数都会把每一行语句执行了。
需要指出的是当产生中断后,当前程序的CPU寄存器xPSR,PC,R14,R12,R3,R2, R1, R0值为自动被处理器保存,并且保存到该任务控制块的栈中,所以这几个寄存器的值不需要再保存到栈了,而只需要把寄存器R11-R4的值保存到栈中就可以了。用文字难以表述,先列出流程图。