前言
大概花了四天时间将将uCOS的任务看了一下,因为之前学过一点操作系统的知识,所以看得不是特别费劲,下面具体来谈谈uCOS-II的任务是什么样的吧
什么是任务
在实际生活中,处理一个大问题的时候通常会将其分解为若干个小问题,当这些小问题解决了,大问题自然而然就解决了,这是典型的“分而治之”的思想。在程序设计方面,如果要做一个比较复杂庞大的工程,通常会将这个工程划分为若干个函数,每个函数都可以执行特定的功能,通过设计这些小函数来达到完成这个工程的目的。
这里的“小问题”、“小函数”在uCOS–II中对应的程序实体就叫做:**任务。**uCOS-II是一个操作系统,操作系统是对各种硬件进行管理的,其中CPU和内存的管理是操作系统的核心内容。对CPU的管理说到底就是对任务的管理。任务管理好了,CPU自然也就管理好了。
对CPU管理一个很重要的概念就是:**并发。**同样,在uCOS-II也存在这方面的内容。并发的实质就是尽可能让cpu“同时”运行多个任务,具体的可以参见一下我前面的博客。在嵌入式方面,cpu通常只有一个,因此任务的并发在微观上看还是“串行”执行的;但是在某段时间看来,这些任务是同时执行的。
从代码的层面看,uCOS-II的任务就是一个个c语言函数。uCOS-II要做的就是管理好这些函数的执行,那么如何管理呢?从操作系统中的学习中知道,cpu是通过一个叫进程控制块(即TCB)的东西来管理各个进程的,同样uCOS-II的任务也存在这样的一个东西,叫做任务控制块。任务控制块存在的意义就是让操作系统更方便的管理任务。
任务的管理
uCOS-II中的任务分为两种:系统任务和用户任务;系统任务是指由系统提供并为系统管理服务的任务;我们一般可以不用管它。用户任务是指为了解决应用问题由用户编写的任务。uCOS-II最多可以有64个任务(系统任务和用户任务一起)。
前面说过任务控制块就是为了方便用户管理而设计的,那么它里面包含什么东西呢?最基本的肯定要有一个指向任务的具体代码的指针,这是肯定的;除此之外还有什么?为了提供cpu的使用效率需要cpu并发执行多个任务,也就是“同时”执行多个任务,既然要做到并发,那么肯定有任务的切换,任务的切换就涉及到保留现场(当前程序运行的位置、各个寄存器的值等等),那么每个任务就都要有一个私有堆栈,即任务堆栈。任务堆栈也是通过指针指向的。除此之外,任务控制块还有一些标志位和优先级等等。
前面说了uCOS最多存在64个任务,任务控制块的数量也不少,因此任务控制块也需要进行管理;在uCOS-II的使用的办法就是用链表将所有的任务控制块链接了起来。这个链表也叫任务注册表,把一个任务控制块加进来也叫做任务的注册。
小结一下,为了方便任务的管理提出了任务控制块的概念,任务控制块里面有:指向任务代码的指针、指向任务堆栈的指针、任务优先级和一些标志位等等。使用链表将所有任务控制块链接在一起,形成任务注册表。
在操作系统里面存在进程和线程的概念,一个很重要的区别就是有无独立的内存运行空间,也就是内存映射是不是一样的。在uCOS-II中,多个运行程序实体共同使用同一套MMU,因此uCOS-II里面的任务不能叫进程,只能叫线程。
任务的状态
uCOS-II是按照一个cpu来进行设计的,同一时刻真正只有一个任务在运行状态,其他任务只能处于其他状态;在uCOS-II中,一共存在五种状态,如下:
-
睡眠状态:
任务只是以代码的形式驻留在程序空间(ROM或RAM),还没有交给操作系统管理时的情况叫做睡眠状态。简单地说,任务在没有被配备任务控制块或被剥夺了任务控制块时的状态叫做任务的睡眠状态。 -
就绪状态:
如果系统为任务配备了务控制块且在任务就绪表中进行了就绪登记,则任务就具备运行的充分条件,这时任务的状态叫做就绪状态。 -
运行状态:
处于就绪状态的任务如果经调度器判断获得了CPU的使用权,则任务就进人运行状态,任何时刻只能有一个任务处于运行状态,就绪的任务只有当所有优光级高于本任务的任务都转为等待状态时,才能进入运行状态。 -
等待状态:
正在运行的任务,需要等待一段时间或需要等待一个事件发生再运行时,该任务就会把CPU的使用权让给其他任务而使任务进入等待状态。 -
中断服务状态:
一个正在运行的任务一旦响应中断申请就会中止运行而去执行中断服务程序,这时任务的状态叫做中断服务状态。
系统任务
前面说过,系统任务是操作系统所需要的。uCOS-II预定义了两个系统任务:空闲任务和统计任务。
空闲任务是每个应用程序必须要有的,计算机硬件是不能停下来的(除非掉电或者时间脉冲丢失),空闲任务的意义就是让cpu没事的时候有事干。因为是没事的时候做的,所以有事的时候肯定不能干,因此它的优先级肯定要最低。
void OSTaskIdle(void * pdata)
{
#if OS_CRITICAL_METHOD == 3
OS_CPU_SR cpu_sr;
# endif
pdata = pdata; // 使用一下参数,防止编译器报错
for (;;)
{
OS_ENTER_CRITICAL(); // 关中断,进临界区
OSdleCtr++;
OS_EXIT_CRITICAL(); // 开中断,出临界区
}
}
临界区指的是不能被中断的区域。从上面的代码也能看到,OSTaskIdle仅仅是计数器加一,几乎什么事都没干。
统计任务不是每个应用程序所必须的。函数名是OSTaskStat()。这个任务每秒执行计算一次cpu在单位时间内被使用的时间,并把计算结果以百分比的形式存放在变量OSCPUsage中,以便其他应用程序来了解cpu的使用情况。
任务的优先权以及优先级别
如何从就绪队列里面找到一个任务运行,这就是调度。在uCOS-II里面给每个任务都设置了优先级,每次都是拿优先级高的任务执行就行了。uCOS-II里面优先级默认为64级,即0~63。数字越小,优先级越高。
由于大多数程序的任务数都小于64,所以用户可以通过OS_CFG.H里面的OS_LOWEST_PRIO变量来设置最低优先级,如果为其赋值了,那么相应的优先级的级别就为:0~OS_LOWEST_PRIO,任务数也不能超过OS_LOWEST_PRIO+1个。
一般情况下,最低优先级OS_LOWEST_PRIO自动赋给空闲任务,OS_LOWEST_PRIO自动赋给统计任务。
任务的优先级需要在定义时显示定义。
任务堆栈
堆栈就是存储器中按照“后进先出”的原则组织的连续存储空间。uCOS-II每个任务控制块都有一个指向该任务堆栈的指针。
任务堆栈的创建
在文件OS_CPU.H中专门定义了一个数据类型OS_STK:
typedef unsigned int OS_STK; // 16位
这样定义堆栈的时候只要定义一个这样的数组就行了。如:
# define TASK_STK_SIZE 512 // 1024个字节
OS_STK TaskStk[TASK_STK_SIZE]; // 定义一个数组来作为任务堆栈
可以使用OSTaskCreate()来创建一个任务,
INT8U OSTaskCreate(
void (*) task(void * pd), // 指向任务的指针
void * pdata, // 传递给任务的参数
OS_STK * pots, // 指定任务堆栈栈顶指针
INT8U prio // 指定任务优先级别的参数
)
这里需要注意的是:不同的处理器可能堆栈的增长方向不一样,有的向下增长,有的向上增长,因此使用OSTaskCreate()创建任务的时候要注意第三个参数是数组的首地址还是数组的最后一个元素的地址。
通常为了提高程序的可移植性,会将两种形式都写出来,然后根据处理器的类型来选择
void main(void)
{
# if OS_STK_GROWTH == 1
OSTaskCreate(App1Task, (void *)0, &App1Task_Stk[App1Task_StkSize-1], 20);
# else
OSTaskCreate(App1Task, (void *)0,
&App1Task_Stk[0], 20);
# endif
// 其他代码
}
任务堆栈的初始化
应用程序创建一个新任务时,需要将这个任务需要的资源都存放在堆栈里面,比如PC指针,寄存器的值等等,以便在这个任务得到cpu的使用权之后可以正确地执行。把任务初始化数据放到任务堆栈里面地工作过程就叫做任务堆栈的初始化。为了完成这个任务,uCOS-II提供了任务堆栈初始化函数OSTaskStkInit()。
OS_STK *OSTaskStkInit
(
void (*task)(void *pd),
void *pdata,
OS_STK *ptos,
INT16U opt
)
通常用户不会直接接触到这个函数,而是通过OSTaskCreate()来调用。另外这个函数在uCOS移植的时候需要重新写,因为处理器不一样。
任务控制块及其链表
前面提到过为了方便对任务进行管理,提出了任务控制块。uCOS-II是使用两条链表把系统所有的任务控制块链接起来,并通过它们管理各个任务。
任务控制块的定义如下:
typedef struct os_tcb {
OS_STK *OSTCBStkPtr; /* Pointer to current top of stack */
#if OS_TASK_CREATE_EXT_EN > 0u
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_EVENT_EN) && (OS_EVENT_MULTI_EN > 0u)
OS_EVENT **OSTCBEventMultiPtr; /* Pointer to multiple event control blocks */
#endif
#if ((OS_Q_EN > 0u) && (OS_MAX_QS > 0u)) || (OS_MBOX_EN > 0u)
void *OSTCBMsg; /* Message received from OSMboxPost() or OSQPost() */
#endif
#if (OS_FLAG_EN > 0u) && (OS_MAX_FLAGS > 0u)
#if OS_TASK_DEL_EN > 0u
OS_FLAG_NODE *OSTCBFlagNode; /* Pointer to event flag node */
#endif
OS_FLAGS OSTCBFlagsRdy; /* Event flags that made task ready to run */
#endif
INT32U 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 */
OS_PRIO OSTCBBitX; /* Bit mask to access bit position in ready table */
OS_PRIO OSTCBBitY; /* Bit mask to access bit position in ready group */
#if OS_TASK_DEL_EN > 0u
INT8U OSTCBDelReq; /* Indicates whether a task needs to delete itself */
#endif
#if OS_TASK_PROFILE_EN > 0u
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_EN > 0u
INT8U *OSTCBTaskName;
#endif
#if OS_TASK_REG_TBL_SIZE > 0u
INT32U OSTCBRegTbl[OS_TASK_REG_TBL_SIZE];
#endif
} OS_TCB;
其中OSTCBStat用来存放当前任务状态,可取的值为:
OS_STAT_RDY : 就绪
OS_STAT_SEM : 等待信号量状态
OS_STAT_MBOX : 等待消息邮箱状态
OS_STAT_Q : 等待消息队列状态
OS_STAT_SUSPEND : 被挂起状态
OS_STAT_MUTEX : 等待互斥型信号量状态
先不理解没关系的,大概知道有这么回事就行。
任务控制块链表
uCOS-II中一共存在两个关于任务控制块的链表。一个链表上全是空闲的任务块(以下简称空任务控制块链表),即没有赋值的;另一条是当前程序已经使用了的任务控制块形成的(以下简称任务控制块链表)。当有新任务时,从空任务控制块链表中取下来一个,根据当前任务信息初始化,然后加到任务控制块链表上。由于加快任务控制块的访问速度,专门用一个数组OSPrioTbl[]来存储任务控制块链表每个元素的地址,并且数组元素是根据任务的优先级来存放指向对应任务的任务控制块指针的,这样就能快速找到某个任务的任务控制块。
下面稍微具体说一下与此相关的东西。
首先:系统在调用OSInit()对uCOS-II系统进行初始化时,就先在RAM中建立一个OS_TCB结构类型的数组OSTCBTbl[],然后链接成一个链表,这就是空任务块链表。这个空任务块链表一共是:OS_MAX_TASK+OS_N_SYS_TASKS个,其中OS_MAX_TASK是定义在OS_CFG.H中的常数,指明了用户任务最大数目。而OS_N_SYS_TASKS是定义在UCOS_II.H中的常数,指明了系统任务数目。之后每当系统调用OSTaskCreate()或者OSTaskCreateExt()(另外一个创建用户任务的函数)时,系统就会将空任务控制块链表头指针OSTCBFreeList指向的任务控制块分配给该任务。在给任务控制块中的各成员赋值之后,系统就按任务控制块链表的头指针OSTCBList将其加入到任务控制块链表中。
任务控制块链表为双向链表。为了方便,把处于运行状态的任务所属的任务控制块叫做当前任务控制块,由OSTCBCur指向。
uCOS-II允许用户使用OSTaskDel()删除一个任务。删除任务实际上就是把该任务的任务控制块从任务控制块链表中拿走,并归还给空任务控制块链表。就相当于吊销了身份;任务控制块对于任务的重要性就像身份证对于人一样。
关于使用OSTaskDel()删除任务后面还会说。
任务控制块的初始化
当用户程序调用OSTaskCreate()创建任务时,会调用系统函数OSTCBInit()为任务控制块进行初始化。
INT8U OSTCBInit()
{
INT8U prio, // 任务优先级,保存在OSTCBPrio中
OS_STK * ptos, // 任务堆栈栈顶指针,保存在OSTCBStkPtr中
OS_STK * pbots, // 任务堆栈栈底指针,保存在OSTCBStkButtom中
INT16U id, // 任务标识符,保存在OSTCBId中
INT16U stk_size, // 任务堆栈长度,保存在OSTCBStkSize中
void * pext, // 任务控制块扩展指针,保存在OSTCB中
INT16U opt // 任务控制块选择项,保存在OSTCBOpt中
}
参考资料
嵌入式实时操作系统uC/OS-II原理及应用
uCOS-II系统中的任务
uCOS-II OSTaskCreate函数分析