μcos-Ⅱ中的任务
任务及种类
任务和任务控制块
任务是一个独立运行单位,在 uCOS 中类似于普通平台上的 main()函数,需要自己来保护其因调用或中断而产生的断点,所以它需要一个自己的私有堆栈一一任务堆栈。
任务的管理
任务分为:用户任务和系统任务,μCOS-Ⅱ最多有64个任务
任务的注册:把一个任务控制块加入链表叫做任务的注册
由于控制块中记录了程序断点值,也就是程序断点处程序计数器 PC 的值,而这个值又是程序待执行指令的地址,它反映了一个程序的运行进程,于是上述存在于内存并接受系统管理的带有任务控制块的程序实体,在计算机技术中根据不同情况就被叫做了“进程”或“线程”。
具有独立内存运行空间的叫做“进程”,例如我们通常编写的C程序,在运行时它在内存中的实体就叫做进程。而多个运行程序实体共用一个内存空间的,它们的整体叫做进程,而这里面的单个实体则叫做“线程”。从 COS-||任务的组成来看,COS-||并没有给任务分配独立的运行空间,而是 C/0S-I 中的所有任务都共同使用一个内存空间,故这里的任务属于线程。
任务的状态
COS-II 是按照系统中只有一个 CPU 来设计的,只能单独运行一个任务,其他任务在其他状态,共有5种状态
用户任务代码的一般结构
超循环结构
一个任务就是一个函数,任务的参数为void类型的指针(为了传递各种数据甚至函数)。
宏:
- 临界段宏:OS_ENTER_CRITICAL()关中断
- 推出临界段宏:OS_EXIT_CRITICAL()开中断
有了任务函数和建立了任务堆栈空间后,任务创建函数OSTaskCreate(),为任务函数创建任务控制块。编程能被调度的真正任务
用户应用程序的一般结构
一个使用了C/OS-II操作系统的用户应用程序就是一个main()函数,用户应用程序可以有被main()调用的函数(初始化),但是被C/OS-II操作系统管理的应用程序是不被main()函数调用的任务函数,main()只负责任务的创建并把它们交给系统。
系统任务
系统自己所需要的任务
C/OS-II 预定义了两个系统任务:空闲任务和统计任务。其中,空闲任务是每个应用程序必须使用的,而统计任务则是应用程序可以根据实际需要来选择使用的。
- 空闲任务
任务有5种状态,系统极有可能会某个时间内无任务运行,为使CPU不停止运行,ucos提供了空闲任务:OSTaskIdle()
空闲任务只是对系统定义的空闲任务运行次数计数器OSdleCtr进行加1,用户也可以在任务中加一些用户工作的代码
如果空闲任务中实现了一个用户可以输入命令并且系统可以按照命令执行相应操作的界面,那么这个ucos就类似于通用操作系统
2.统计任务
OSTaskStat()。该任务每秒计算一次CPU在单位时间内被使用的时间,并把计算结果以百分比的形式存放在变量 OSCPUsage 中以便其他应用程序来了解 CPU 的利用率。
是否使用统计任务,用户可以根据应用程序的实际需要来进行选择。如果用户应用程序决定要使用统计任务,则必须把定义在系统头文件OS_CFG.H 中的系统配置常数 0S_TASK_STAT_EN 设置为1,并且在程序中要调用函数OSStatInit()对统计任务进行初始化。
任务优先权及优先级别
优先级别高的任务先运行,优先级别低的任务后运行。
由于最多可以在 C/OS 中创建 64 个任务,所以任务的优先级别最多有 64 级,每个级别都用一个整数数字来表示.即 01,2---.63。数字越小优先级别越高
为了使用户设置所需要的实际数目,系统配置文件OS_CFG.H中定义了OS_LOWEST_PRIO,如果配置了它,系统优先级别为0、1、2...OS_LOWEST_PRIO,任务总数不能超过OS_LOWEST_PRIO+1
系统总是把最低优先级别 OS_LOWEST_PRIO 自动赋给空闲任务。如果应用程序使用了统计任务,则系统还会把优先级别 OS_LOWEST_PRIO-1自动赋给统计任务,因此用户任务可以使用的优先级别是 0,1,OS_LOWEST_ PRIO-2共 OS_LOWEST_PRIO-1个。当然,用户任务的优先级别要由用户在创建一个任务时显式地定义。
任务堆栈
为了满足任务切换和响应中断时保存 CPU 寄存器中的内容及任务调用其他函数时的需要,每个任务都应该配有自己的堆栈。所有 COS - II 任务的任务控制块中都含有一个指向该任务堆栈的指针。
任务堆栈的创建
为了方便定义任务堆栈,在文件 OS_CPU.H 中专门定义了一个数据类型 OS _STK
typedef unsigned int OS_STK; //该类型数据长度为32位
这样,在定义任务堆栈的栈区时,只要定义一个 OS_STK 类型的数组即可。
#define TASK_STK_SIZE 512 //定义堆栈的长度(2048字节)
OS_STK TaskStk[TASK_STK_SIZE];//定义数组当作任务堆栈
当调用函数OSTaskCreate()创建一个任务时,把数组的指针传给函数OSTaskCreate()中的堆栈栈顶参数ptos,就可以把该数组与任务关联起来而成为该任务的任务堆栈。
使用例 3-1的代码来作为任务代码创建一个任务,任务堆栈长度为 128 字节,优先级别为20,任务参数 pdata的实参为 MyTaskAgu。试写出main()函数的代码。
#define MyTaskStkN 32
OS_STK MyTaskStk[MyTaskStkN];
void main(void)
{
......
OSTaskCreate(
MyTask, //任务的指针
&MyTaskAgu, //传递给任务的参数
&MyTaskStk[MyTaskStkN-1],//任务堆栈栈顶位置(向下增长)
//&MyTaskStk[0], //任务堆栈栈顶位置(向上增长)
20
);
......
}
为了提高应用程序的可移植性,在编写程序时也可把两种代码都编写出来,利用OS_CFG.H 文件中的常数 OS_STK_GROWTH作为选择开关,使用户可通过定义该常数的值来选择相应的代码段,以适应不同的堆栈增长方式的需要。例如:
任务堆栈的初始化
当CPU 在启动运行一个任务时,CPU 的各寄存器总是需要预置一些初始数据,例如指向任务的指针、程序状态字 PSW 等。由于它们都是任务的私有数据,所以应该将它们都存放在任务堆栈。
应用程序在创建一个新任务时,必须把在系统启动这个任务时所需要的CPU各寄存器初始数据(任务指针、任务堆栈指针及程序状态字等)事先存放在任务堆栈。这样当任务获得 CPU 使用权时,就把堆栈的内容复制到 CPU 的各寄存器从而可使任务顺利地启动并运行。
把任务初始数据存放到任务堆栈的工作就叫做任务堆栈的初始化。为了完成这个任务C/OS-II 提供了任务堆栈初始化函数 OSTaskStkInit()。该函数原型如下
通常用户不会直接接触到这个函数,该函数由 C/OS-II所提供的任务创建函数OSTaskCreate()来调用。 另外,因为处理器中寄存器及对堆栈的操作方式不尽相同,所以该函数需要用户在进行uC/OS-II的移植时,按所使用的处理器来编写。
任务控制块及其链表
系统通过任务控制块来感知和管理任务,相当于任务的身份证,uCOS把所有任务的控制块链接为两条链表,通过它们管理各个任务
任务控制块结构
是一个结构类型数据。当调用OSTaskCreate()函数创建一个用户任务时,该函数会对任务控制块中的所有成员赋予与该任务相关的数据,并驻留在RAM中。
任务控制块链表
C/OS-II 在初始化时也要按照配置文件所设定的任务数事先定义一批空白任务控制块,这样当程序创建一个任务需要一个任务控制块时,只要拿一个空白块填上任务的属性即可。
在任务控制块的管理上,C/OS需要两条链表:一条空任务块链表(其中所有任务控制块还未分配给任务)和一条任务块链表(其中所有任务控制块已分配给任务)。
具体做法为:系统在调用函数 OSInit()对 C/OS -II系统进行初始化时,就先在 RAM 中建立一个OS_TCB 结构类型的数组OSTCBTb[]然后把各个元素链接成一个如图 3-5 所示的链表从而形成一个空任务块链表。
初始化建立的空任务链表元素一共是OS_CFG.H->OS_MAX_TASKS(用户任务最大数目)+UCOS_Ⅱ->OS_N_SYS_TASKS(系统任务的数目)个
每当应用程序调用系统函数OSTaskCreate()或 OSTaskCreateExt()创建一个任务时,系统就会将空任务控制块链表头指针 OSTCBFreeList 指向的任务控制块分配给该任务。在给任务控制块中的各成员赋值后,系统就按任务控制块链表的头指针 OSTCBList 将其加入到任务控制块链表中。
C/OS-II 允许用户应用程序使用函数 OSTaskDel()删除一个任务。删除一个任务,实质上就是把该任务的任务从任务控制块链表中删掉,并把它归还给空任务控制块链表。这样,C/OS-II 对这个没有任务控制块的任务就不再理会了因为与这个任务对应的任务控制块已经被“吊销”了。
任务控制块的初始化
给用户任务分配任务控制块及对其进行初始化也是操作系统的职责。当应用程序调用函数OSTaskCreate()创建一个任务时,这个函数会调用系统函数 OSTCBInit ()来为任务控制块进行初始化。
该函数的主要任务如下:
- 为被创建任务从空任务控制块链表获取一个任务控制块
- 用任务的属性对任务控制块各个成员进行赋值;
- 把这个任务控制块链入到任务控制块链表
任务就绪表及任务调度
为系统中处于就绪状态的任务分配 CPU 是多任务操作系统的核心工作。这项工作涉及两项技术:一是判断哪些任务处于就绪状态;二是进行任务调度。
所谓任务调度,就是通过一个算法在就绪任务中确定应该马上运行的任务,操作系统用于负责这项工作的程序模块叫做调度器。
任务就绪表结构
系统总是从处于就绪状态的任务中来选择一个任务进行,因此需要一个就绪任务登记表。在Ucos中这个登记表是一个位图,系统的每个任务都在这个位图中占据一个二进制位,该位值的状态(1或0)就表示任务是否处于就绪状态。
类型为INT8U的数组OSRdyTbl[],按任务优先级别排序
可以看出8个任务就能看成一个任务组,为了方便查找,有个INT8U的变量OSRdyGrp,并使该变量的每一个位都对应OSRdyTbl[]的一个任务组,如果组内有任务就绪,则在变量OSRdyGrp里就把该组对应的位设置为1,否则为0
由于变量OSRdyGrp 有 8 个二进制位,每位对应 OSRdyTbl[]数组的一个元素,每个元素又可以记录8个任务的就绪状态,因此uC/OS-II最多可以管理8X8=64 个任务
根据优先级找到任务在就绪表的位置:
对任务就绪表的操作
主要有三个操作:登记、注销和从就绪表的就绪任务中得知具有最高优先级任务的标志(优先级prio)
-
登记
指的是当某个任务处于就绪状态时,系统将该任务登记在任务就绪表中,即在就绪表中将该任务的对应位设置为 1。
在程序中,可用类似于下面的代码把优先级别为 prio 的任务置为就绪状态。
-
注销
当任务脱离就绪状态时,系统在就绪表中设置为0。
在程序中,可用类似于下面的代码把优先级别为 prio 的任务置为非就绪状态。
-
最高优先级就绪任务查找
uCOS调度器用于获取优先级别最高的就绪任务的代码如下:
或
该代码执行后得到的是最高优先级就绪任务的优先级别
任务调度
uC/OS的任务调度思想是:“近似地每时每刻让优先级最高的就绪任务处于运行状态。在具体做法上,它在系统或用户任务调用系统函数及执行中断服务程序结束时调用调度器,以确定应该运行的任务并运行它。
调度器的主要工作
在多任务系统中,令CPU 中止当前正在运行的任务转而去运行另一个任务的工作叫做任务切换,而按某种规则进行任务切换的工作叫做任务的调度。
任务调度由任务调度器完成。任务调度器的主要工作有两项:
-
在任务就绪表中查找具有最高优先级别的就绪任务
-
实现任务的切换,分为两个步骤
1.获得待运行任务的TCB指针
2.进行断点数据的切换
uCOS-Ⅱ中有两种调度器
- 任务级的调度器:由函数OSSched()来实现
- 中断级的调度器:由函数OSIntExt()来实现
获得待运行任务的TCB指针
操作系统通过任务的任务控制块TCB来管理任务,因此调度器真正实施任务切换之前的主要工作就是要获得待运行任务的任务控制块指针和当前任务的任务控制块指针。
因为被中止任务的任务控制块指针就存放在全局变量 OSTCBCur 中所以调度器这部分的工作主要是要获得待运行任务的任务控制块指针。
任务调度器OSSched()源代码:
任务切换宏OS_TASK_SW()
任务切换的工作是靠OSCtxSw()来完成的,任务调度器中的其余部分只是为了确认最高优先级的指针和当前任务的指针,主要切换还是OSCtxSw()
简单地说,任务切换就是中止正在运行的任务(当前任务),转而去运行另外一个任务的操作。当然,这个任务应该是就绪任务中优先级别最高的那个任务。
为了了解调度器是如何进行任务切换的,先来探讨一下一个被中止运行(可能因为中断或者调用)任务,将来又要”无缝”地恢复运行应该满足什么条件
把任务被中止运行时的位置叫做断点,把当时存放在 CPU 的PC、PSW 和通用寄存器等各寄存器中的数据叫做断点数据,当任务恢复运行时必须在断点处以断点数据作为初始数据接着运行,才能无缝衔接。要实现则必须被终止时就把该任务的断点数据保存在堆栈中,在被重新运行时,要把堆栈中的这些断点数据再恢复到CPU的各寄存器中。
一个被中止的任务能否正确地在断点处恢复运行,其关键在于是否能正确地在 CPU各寄存器中恢复断点数据,而能够正确恢复断点数据的关键是 CPU的堆栈指针 SP是否有正确的指向。因此也可知.在系统中存在多个任务时,如果在恢复断点数据时用另一个任务的任务堆栈指针(存放在控制块成员OSTCBStkPtr 中)来改变CPU的堆栈指针SP,那么CPU 运行的就不是刚才被中止运行的任务,而是另一个任务了,也就是实现任务切换了。当然,为防止被中止任务堆栈指针的丢失,被中止任务在保存断点时,要把当时 CPU 的 SP 的值保存到该任务控制块的成员 OSTCBStkPtr(指向当前任务栈顶的指针) 中。
综上所述,任务的切换就是断点数据的切换,断点数据的切换就是CPU堆栈指针的切换,被中止运行任务的任务堆栈指针要保护到该任务的任务控制块中,待运行任务的任务堆栈指针要由该任务控制块转存到CPU的SP中。
①把任务的断点指针(在PC寄存器中)压入任务堆栈
② 用压栈指令把CPU通用寄存器R1,R2,...压入堆栈
③OSTCBCur->OSTCBStkPtr = SP;把SP保存在中止任务控制块中
④ OSTCBCur = OSTCBHighRdy;使系统获得待运行任务控制块
⑤ SP = OSTCBHighRdy ->OSTCBStkPtr; 把待运行任务堆栈指针赋予SP
⑥ 用出出栈指令把R1、R2、···弹入CPU的通用寄存器
⑦待运行任务: 把任务堆栈里上次任务被中止时存放在堆栈中的中断指针推入PC寄存器
任务的创建
创建任务的工作实质上是创建一个任务控制块,并通过任务控制块把任务代码和任务堆栈关联起来形成一个完整的任务。还要使刚创建的任务进入就绪状态,并接着引发一次任务调度。
两个创建任务的函数:OSTaskCreate()和OSTaskCreateExt()。Ext是扩展,提供了一些附加功能,用户自行选择一个创建任务
用函数OSTaskCreate()创建任务
OSTaskCreate()源代码:
用函数OSTaskCreateExt()创建任务
用这个函数创建任务更加灵活但是也会增加一些额外的开销
创建任务的一般方法
一般任务可在调用函数OSStart()启动任务调度前来创建,也可以在任务中创建。但是uCos有个规定:在调用启动任务函数OSStart()之前,必须已经创建了至少一个任务。所以一般在调用函数OSStart()之前先创建一个任务并赋予它最高的优先级别,从而使它成为起始任务,然后在这个起始任务中,再创建其他各任务。
如果要使用系统提供的统计任务,则统计任务的初始化函数也必须在这个起始任务中来调用。
创建任务的示意性代码:
uCOS-Ⅱ不允许在中断服务程序中创建任务
任务的挂起和恢复
挂起任务:停止这个任务的运行
用户任务可通过调用系统提供的OSTaskSuspend()函数来挂起自身或者除空闲任务之外的其他任务。挂起的任务只能在其他任务中通过调用恢复函数OSTaskResume()使其恢复为就绪状态
挂起任务
挂起任务函数OSTaskSuspend()原型如下:
INT8U OSTaskSuspend(INT8U prio);
函数的参数 prio 为待挂起任务的优先级别。如果调用函数 OSTaskSuspend ()的任务要挂起自身,则参数必须为常数 OS_PRIO_SELF(该常数在文件 uCOS_Ⅱ.H中被定义为0xFF)。
当雨数调用成功时,返回信息 OS_NO_ERR;否则根据出错的具体情况返回OS_TASK_SUSPEND_IDLE,OS_PRIO_INVALID 和 OS_TASK_SUSPEND_PRIO 等。
恢复任务
恢复任务函数OSTaskResume()的原型如下:
INT8U OSTaskResume(INT8U prio);
其他任务管理函数
任务优先级别的修改
OSTaskChangePrio()
任务的删除
所谓删除一个任务,就是把该任务置于睡眠状态。具体做法是,把被删除任务的任务控制块从任务控制块链表中删除,并归还给空任务控制块链表,然后在任务就绪表中把该任务的就绪状态位设置为0.于是该任务就不能再被调度器所调用了。简单地说,就是把它的身份证给吊销了。
在任务中,可以通过调用函数 OSTaskDel()来删除任务自身或者除了空闲任务之外的其他任务。
如果一个任务调用这个函数是为了删除任务自己,则应在调用函数时令函数的参数 prio为 OS_PRIO_SELF。
有时,任务会占用一些动态分配的内存或信号量之类的资源。这时,如果有其他任务把这个任务删除了,那么被删除任务所占用的一些资源就会因为没有被释放而丢失,这是任何系统都无法接受的。因此,在删除一个占用资源的任务时,一定要谨慎。具体的办法是,提出删除任务请求的任务只负责提出删除任务请求,而删除工作则由被删除任务自己来完成。这样,被删除任务就可以根据自身的具体情况来决定何时删除自身,同时也有机会在删除自身之前把占用的资源释放掉。
显然,如果想使提出删除任务请求的任务和被删除任务之间能够像上述方式一样来执行删除工作,则它们双方必须有某种通信方法。C/OS -I1 利用被删除任务的任务控制块成员OSTCBDelReq 作为请求删除方的被删除方的联络信号,同时提供了一个双方都能调用的函数一请求删除任务函数OSTaskDelReq()。这样,提出删除任务请求的任务和被删除任务的双方就都能使用这个函数来访问 OSTCBDelReq 这个信号,从而可以根据这个信号的状态来决定各自的行为。
提出删除任务请求的任务在调用这个函数时,函数的参数应该为被删除任务的优先级别prio;被删除任务在调用这个函数时,函数的参数应该为OS_PRIO_SELF
查询任务的信息
有时,在应用程序运行中需要了解一个任务的指针、堆栈等信息,这时就可以通过调用函数OSTaskQuery()来获取选定的任务的信息。函数 OSTaskQuery()的原型如下:
若调用函数 OSTaskQuery()查询成功,则函数将返回OS_NO_ERR,并把查询得到的任务信息存放在结构 OS_TCB类型的变量中。
μC/OS-Ⅱ的初始化和任务的启动
在使用 C/OS-II的所有服务之前,必须调用 C/OS -II 的初始化函数 OSInit(),对C/0S-II自身的运行环境进行初始化。
函数OSInit()将对 C/OS-II的所有全局变量和数据结构进行初始化,同时创建空闲任务OSTaskIdle,并赋之以最低的优先级别和永远的就绪状态。如果用户应用程序还要使用统计任务(常数 OS_TASK_STAT_EN=1),则 OSInit()还要以优先级别为 OS_LOWESTPRIO-I来创建统计任务。
μC/OS-Ⅱ的启动
μC/OS-II 进行任务的管理是从调用启动函数 OSStart()开始的。当然,其前提条件是在调用该函数之前至少创建了一个用户任务。
0SStartHighRdy()在多任务系统启动函数OSStart()中调用。完成的功能是:设置系统运行标志位OSRunning=TRUE,将就绪表中最高优先级任务的栈指针 Load 到 SP 中并强制中断返回。这样就绪的最高优先级任务就如同从中断返回到运行状态一样,使得整个系统 得以运转。