这一节我们来看任务结构和任务管理
任务控制块
我们知道,在工作中,管理很多东西都需要一张记录表,包括机器的管理,我们可能会记录其信息,如工作时间,何时启动,运行了多久,当前工作电压电流,温度等等,通过这些信息我们能实时监控和管理好机器的运转。而任务也需要类似的管理,也需要类似的“表”,那么我把任务控制块理解为这样的一张表,可能不够恰当,但是便于理解。在uC/OS-III中,每个任务控制块都是一个在os.h中定义的os_tcb结构体对象:
struct os_tcb {
CPU_STK *StkPtr; /* Pointer to current top of stack */
void *ExtPtr; /* Pointer to user definable data for TCB extension */
CPU_STK *StkLimitPtr; /* Pointer used to set stack 'watermark' limit */
OS_TCB *NextPtr; /* Pointer to next TCB in the TCB list */
OS_TCB *PrevPtr; /* Pointer to previous TCB in the TCB list */
OS_TCB *TickNextPtr;
OS_TCB *TickPrevPtr;
OS_TICK_SPOKE *TickSpokePtr; /* Pointer to tick spoke if task is in the tick list */
CPU_CHAR *NamePtr;
//...(由于篇幅问题此处省略很多)
#if OS_CFG_DBG_EN > 0u
OS_TCB *DbgPrevPtr;
OS_TCB *DbgNextPtr;
CPU_CHAR *DbgNamePtr;
#endif
};
这个结构体很长,我在贴出来的代码中省略了很多。可以把它想想成一个列表,现在大家先不用去想那些繁多的变量都代表什么,因为我也说了,之前我是想解析PendSV异常代码所以想先说明下任务管理,所以在这里,我们先只关注结构体中第一个元素:
CPU_STK *StkPtr;
即可,其他元素的意义,后续讲解中会涉及到。
这个StkPtr元素是任务堆栈指针,其意义是指向任务本身的堆栈栈顶,因为我们知道在处理任务的时候,其“工作场所”就是该任务的堆栈,所以我总要知道“工作场所”在哪以及其使用情况。当一个任务切换到另一个任务的时候,因为两个任务都有独自的“工作场所”,那么切换过去的时候肯定要知道这个“工作场所”在哪。所以StkPtr就是这个作用的。
话说,堆栈了不了解?这里更准确地说应该叫做“栈”,是一段连续的存储空间,在任务创建的时候需要去设置栈,包括其大小,位置,实际上就是unsigned int类型的一个数组,这个不赘述。
任务管理
在日常生活中,我们做事情也会分轻重缓急,而在uC/OS-III中,任务也是有优先级之分,这个决定了同样是两个就绪但优先级不同的任务哪一个会先得到执行,在uC/OS-III中,优先级梯数可以由用户决定:
#define OS_CFG_PRIO_MAX 64u /* Defines the maximum number of task priorities (see OS_PRIO data type) */
优先级为0表示优先级别最高,以此类推。每个任务都有自己的优先级,其在每个任务的任务控制块os_tcb中有表示:
OS_PRIO Prio; /* Task priority (0 == highest) */
uC/OS-III允许有多个优先级别相同的任务存在,这些任务通过链表管理。当执行任务切换时,系统会在找到就绪的任务中优先级别最高的任务,然后执行它,如果该最高优先级别下有多个任务,那么最先被执行的是这多个相同优先级别任务组成的链表的表头。所以,系统要去切换到优先级别最高的就绪任务,步骤是这样的:找到就绪任务中最高优先级是哪一级,通过找到的该优先级再进一步找到属于该优先级别任务链表的表头,从而就确定了要切换的那个就绪任务的任务控制块所在,进而就可以得到该任务堆栈所在,最后可以实现任务切换。所以,任务就绪管理就分为两个部分:
优先级位映射表OSPrioTbl[]:用来记录哪个优先级下有任务就绪。
就绪任务列表OSRdyList[]:用来记录每一个优先级下所有就绪的任务。
先看优先级位映射表OSPrioTbl[]
该表的定义代码为:
CPU_DATA OSPrioTbl[OS_PRIO_TBL_SIZE];
实际上就是一个unsigned int类型的数组,数组长度是:OS_PRIO_TBL_SIZE,通过定义往上找,可以知道这个值为2
#define OS_PRIO_TBL_SIZE (((OS_CFG_PRIO_MAX - 1u) / DEF_INT_CPU_NBR_BITS) + 1u) //((64-1)/(4*8))+1=2
//也即是表明总共有2个32位来表示优先级,即优先级有64个,但是ucosiii支持每个优先级下有无数个相同优先级任务
当然,你也可以定义得更多,改动OS_CFG_PRIO_MAX宏定义的值即可。那么其本质定义就是:unsigned int OSPrioTbl[2]
所以数组每一个元素都是一个32位无符号值,其每一位代表一个优先级,那么一个元素就可以代表32个优先级,如OSPrioTbl[0]就可以代表0–31这32个优先级。以此类推。
接下来是就绪任务列表OSRdyList[]
定义如下:
OS_RDY_LIST OSRdyList[OS_CFG_PRIO_MAX]; /* Table of tasks ready to run */
typedef struct os_rdy_list OS_RDY_LIST;
所以它是一个os_rdy_list类型的数组,数组长度为64,代表原uC/OS-III定义的优先级别级数,而os_rdy_list是一个结构体,定义如下:
struct os_rdy_list {
OS_TCB *HeadPtr; /* 链头 */
OS_TCB *TailPtr; /* 链尾 */
OS_OBJ_QTY NbrEntries; /*数量 */
};
可以看到它有三个元素,两个任务控制块指针和一个16位无符号数据类型变量。HeadPtr指向链表头,TailPtr指向链表尾。如果一个优先级下有多个任务组成链表,其图示如下:
可以看到在任务控制块中的PrevPtr和NextPtr变量可以使得多个任务控制块组成双向链表。所以OSRdyList[]数组每个元素管理着对应优先级别下的任务,如上图中是OSRdyList[4],那么该链表中的任务都是优先级别为4的任务。为了辅助理解,我想到了一个比喻:
有6个水果盘排成2行,一行3个,每个水果盘根据你喜欢的水果的喜欢程度来盛放不同的水果。当然你在某个时候不会每个盘都装了水果,有可能一些因为没有货了买不到。那么当你想吃水果的时候,那么你就会从你最爱的第一行第一列那个开始找,当你发现该水果盘上有水果存在(优先级表某个位置位),紧接着,你会从那个盘里(该优先级数)找到相同水果中最大那个(根据优先级得到该优先级下的链表头)。
有了这个比喻,就可以讲解uC/OS-III中怎样找到最高优先级就绪任务的代码了!
首先,定位到os_prio.c文件中的OS_PrioGetHighest函数,该函数的作用就是找到优先级位映射表OSPrioTbl[]中就绪的最高优先级级数:
OS_PRIO OS_PrioGetHighest (void)
{
CPU_DATA *p_tbl; //unsigned int指针
OS_PRIO prio;
prio = (OS_PRIO)0;
p_tbl = &OSPrioTbl[0];//数组头取地址
while (*p_tbl == (CPU_DATA)0)
{ //遍历数组,从[0]开始,如果该元素为0,则继续从[1]寻找,以此类推
prio += DEF_INT_CPU_NBR_BITS; // DEF_INT_CPU_NBR_BITS是32,即一个数组元素是32位的,代表32个优先级,如果上一个数组元素为0,下一个数组元素代表的优先级要低32个级别
p_tbl++;//指向数组下一个元素
}
prio += (OS_PRIO)CPU_CntLeadZeros(*p_tbl); /* 汇编实现,计算最高符号位与第一个1之间的0的个数,例如00010000,那么1左边有3个0,那么就是返回3 */
//这一句就找到了就绪的最高优先级级数。
return (prio);//返回这个级数
}
该代码有注释解析,过程比较简单,就是while循环从高优先级开始往下遍历数组元素的值是否为0,不为0说明有被置位,即有该位代表的优先级别的任务就绪,然后算出这个优先级数,再返回。好了,那我们得到了这个优先级数,进一步我们就可以得到这个优先级数下的任务的链表头了,这个链表头指向最高优先级就绪任务控制块。代码可以看os_core.c中的任务调度函数OSSched (void)中的这两句:
OSPrioHighRdy = OS_PrioGetHighest(); /* 找到最高优先级 */
OSTCBHighRdyPtr = OSRdyList[OSPrioHighRdy].HeadPtr; /*得到该优先级下的任务控制块列表中的第一个任务控制块指针*/
好了至此就讲明白了系统如何找寻将要切换的就绪任务,实际上就是得到该任务控制块的指针,进而就可以实现任务切换,这个在下一节讲解PendSV异常代码的时候再解析其具体实现过程。另外就是,反过来,当任务需要就绪的时候,这些优先级表,就绪表等怎么设置的,这些都等到以后章节进行详细解析。所以现在要知道的知识点就是:
任务控制块,任务堆栈,就绪任务的寻找
重点就是要找到任务控制块,因为这个任务控制块管理者任务的一切。
如果这些明白了,接下来的一节,将进入PendSV异常代码的解析!