在学习单片机上使用uscos系统时,思考的一些问题和总结,自己也画了一些图来描述这些想法。本文主要分析问题和模型结构,不会过多涉及具体代码。
为什么需要多任务系统?
在思考这个问题之前,需要首先回答什么是实时操作系统,如今的嵌入式系统大多是实时操作系统,实时意味着系统必须及时并快速地响应外部事件,如中断等。想象一个打印系统,多份文件正在排队等待打印,此时有一个高优先级的资料进入打印队列,为了让这份资料优先打印,需要怎么设计该系统:
- 使用一个单任务系统,该系统循环工作,每次循环都会先执行检查工作,再打印一份文件,该系统维护了一个文件打印队列,每次有高优先级的资料进入,将插队到它该处的位置待打印(不会打断当前打印过程),系统会遍历打印队列中的文件。
- 使用一个多任务系统,维护一个任务栈,所有的检查维护工作都是系统任务,需要打印的文件也划分到任务级,每个任务有自己的优先级和工作方式,将新进的高优先级任务进入就绪状态,系统会自动根据优先级顺序进行循环工作。
粗略一看,两种方案的核心思想都是一样的,都有根据优先级排队的意思,第一种方案实现还更简洁明了,但如果放到一个实际需求中去分析,就是另一种情况了。
此时我需要提前检查墨水和纸张含量,以保证后续的打印工作,因为我不想等到已经没墨没纸了,再去添加。在第一种方案下,我需要等待当前的打印工作完成,并进入到下一个循环的检查工作时,才能响应我的需求,如果该文件过大,或许要等半个小时才能结束!整个过程好像一个单线程的工作方式,一旦进入某个环节就必须完成它,对外界变化的响应时间是不确定的。而在第二种方案下,我可以提高“检查墨水和纸张工作”的优先级,将它作为整体的任务加入到任务栈中,系统可以在打印完当前页面后,进行任务调度,先进行我要求的任务,响应时间是基本固定且迅速的。
再比如,此时我需要打印的某份资料是彩色的,需要去增加彩色墨水盒。在第一种方案下,系统必须等待我加入彩色墨水盒的动作完成并确认,才能恢复整体工作。这一动作看上去没啥问题,但在添加墨水盒的过程中,打印机没有做任何事情,不能暂时跳过当前任务,去安排下一个黑白文件的打印,利用率是低下的。而第二种方案,在我加入墨水的过程中,系统可以安排很多其他的无关任务而不会停顿。
从上面两个例子可以总结出两种系统的优缺点:
- 循环系统是一个单线程的系统,工作非常稳定,系统相对简单,缺点是难以拓展,响应变化的时间不稳定,只有在特定场景才能发挥高效率(工作任务固定)。
- 多任务系统是一个多线程系统,缺点是需要相对复杂的同步方法来稳定工作秩序,且需要额外的开销来进行任务切换。
回到最开始的思考,现在可以知道,实时系统必须是一个多任务系统。
多任务长什么样?
在处理一个大而复杂的问题时,我们通常倾向于“分而治之”,即把一个大问题分解成多个相对简单、容易解决的小问题,这些小问题也可以叫做小任务,通过运行多个小任务,可以最终达到大任务的目的。μC/OS-Ⅱ就是一个能对这些小任务小问题进行管理和调度的多任务系统,如下图,每一个任务都有三部分组成:任务控制块、任务堆栈、任务程序代码。
其中,任务控制块记录了任务的各个属性,并存放了各种指针;任务堆栈用来保存任务的工作环境和数据;程序代码即任务的具体执行部分。
不同的任务通过各自的任务控制块相链接,形成一条任务控制块链。在μC/OS-Ⅱ中,最多能运行64个不同的任务,优先级从0~63,数字越小优先级越高,即任务控制块链的长度最多为64。且μC/OS-Ⅱ明确规定:不能定义优先级相同的任务,这样才能保证系统运行的稳定。这里面包括了系统任务和用户自定义的任务,其中系统任务只预定义了两个,这两个任务分别占据了最低的两个优先级(最低优先级或称任务数由用户自定义的OS_LOWEST_PRIO值决定):
- 空闲任务:系统在没有其他任务时会处于空闲状态,为了使其“有事可做”,创造了一个所谓的“空闲任务”。空闲任务是必须存在的,它可以使系统处于循环的工作状态中,以满足系统的工作机制(模式),而不需要额外创造规则去适应这个空闲状态(有点绕,思考一下)。
- 统计任务:统计CPU的利用率,每秒计算一次CPU在单位时间内被使用的时间,用户可以根据应用的需要来选择是否需要统计任务。
那么问题来了,任务控制块链是如何产生的呢?
实际上,μC/OS-Ⅱ中共有两条任务控制块链表,均为双向链表结构,一条是空任务控制块链表OSTCBFreeList,一条是实实在在的任务控制块链表OSTCBList。这里所说的空链表并不是指链表上没有元素,而是系统在调用OSInit()进行初始化时,会建立一条长度为OS_MAX_TASKS(用户自定义值)的链表,这上面的每一个元素都是空的任务控制块,仅有数据结构,没有数据,如图:
而另一条任务控制块链表在初始化时没有元素,会在创建任务的过程中向链表添加任务控制块元素。本质就是一个替换的过程,先在一个容器中预留足够的空盒子,每创建一个任务,就把该任务装进一个空盒子里,并取出该盒子放到另一个容器中,具体描述如下:
- 将空链表OSTCBFreeList的头指针指向的空任务控制块分配给新创建的任务;
- 利用新建任务的属性,对该空任务控制块的各成员进行赋值,使之成为该任务的唯一识别“身份证”;
- 使用另一条任务控制块链表OSTCBList的头指针将该任务控制块加入到链表头部位置,原来的空链表OSTCBFreeList头指针则指向下一个空任务控制块元素,完成任务控制块的转移。
除了任务控制块链表,系统还定义了一个任务优先级数组OSPrioTbl[ ]。该数组以按优先级顺序一次存放了各个任务控制块的指针,这样在访问某个任务的任务控制块时,就不必遍历整条任务控制块链表了(空间替换时间),上述的这些结构都可以用下图来表示。
这也解释了为什么我们在对任务进行操作时,通常使用任务的优先级来进行索引,这种方式可以快速定位到该任务的任务控制块,再对控制块及其相连接的结构进行操作。
在此基础上,我们也可以很容易猜想到删除任务的原理,即:把任务控制块链表OSTCBList的某个任务控制块的数据还原为空,再逆向操作,将其归还到空任务控制块链表OSTCBFreeList中。这个过程相当于将任务控制器进行“吊销”,但任务本身还是存在于内存中的(未激活)。
由此可见,任务控制块结构是实现多任务管理的核心,任何针对任务的操作都离不开任务控制块,关于任务控制块内部的具体内容之后有时间可以继续分享。
多任务如何运行?
我们在第一个问题中已经了解,在系统的运行过程中,会不断地对就绪表中的任务进行获取和处理,这些任务自身就是个循环体,在不断地重复运行,确保系统不会停下来。在第二个问题中也提到,多个任务通过任务控制块相连,实现任务的快速定位和操作。那么,就绪表和任务控制块链表是什么关系?
先来看就绪表的结构:
就绪表使用一个数组OSRdyTbl[ ]来表示,数组长度为8,表示8个优先级组,优先级按自然索引顺序依次减小。每一组都是一个INT8U类型的整数,可以表示成八个二进制位(1/0),即八个优先级,其中低位为高优先级,某一位置1表示该优先级的任务已经就绪,置0表示该优先级没有任务就绪。为了进一步知道哪一个优先级组中存在就绪任务,μC/OS-Ⅱ引入了一个INT8U类型的变量OSRdyGrp,如OSRdyGrp=10100101表示OSRdyTbl[0]、OSRdyTbl[2]、OSRdyTbl[5]、OSRdyTbl[7]优先级组中存在任务就绪,具体哪一个优先级有任务还得具体看看OSRdyTbl[ ]的值。
了解了就绪表的结构,进一步就要知道就绪表如何操作和运行。就绪表主要包括三个操作:登记、注销和查找(从就绪任务中找到最高优先级任务的标识,当然标识就是“优先级“):
- 登记。就是在就绪表中将该任务对应的优先级位置置1;
- 注销。同上,将任务对应的优先级位置置0;
- 查找。从上述的OSRdyGrp变量来获取优先组别,再拿该优先组去OSRdyTbl[ ]寻找最高的优先级任务,找到该优先级后返回给系统,系统会根据该优先级获取到相应的任务控制块(看第二节)。
不过要让多任务运转起来,还需要有任务切换的过程,即任务调度。这包含两个步骤,一是寻找当前的最高优先级就绪任务,二就是进行任务切换。在这里有个细节:发生任务调度的原因,不是因为任务彻底完成了(任务是一个无尽的循环),而是因为进入了延时、等待事件或中断等。当调度发生时,需要获取到下一个任务的任务控制块指针OSTCBHighRdy,和当前需要被中止的任务控制块指针OSTCBCur(第二节有提到过),这些经过之前的介绍已经可以做到,接下来使用任务切换宏OS_TASK_SW()进行任务切换,其底层是函数OSCtxSw(),主要做了这两件事:
- 当前任务是被中止的,而不是被销毁,因此未来可能会重新登记,那么当该任务恢复运行时,必须在”断点“处以当时的”断点数据“作为初始值继续运行,才能实现”无缝衔接“。因此必须将任务被中止时的断点数据保存到任务堆栈中,这些断点数据包括被中止任务的断点指针(即运行到任务的具体代码位置)和CPU通用寄存器的值,并将任务堆栈的指针保存为任务控制块的OSTCBStkPtr成员;
- 待执行任务是被恢复的(包括初次执行),需要通过待运行任务的任务控制块找到其任务堆栈,将存储在任务堆栈中的通用寄存器值和断点指针恢复到CPU的各寄存器中,并继续运行。
这样就完成了就绪表和任务控制块的联动,也解释了μC/OS-Ⅱ系统多任务的运行流程。
总结上面的三大问题,多任务的运行流程就可以用这样的过程来描述:
- 创建任务,获得自己的任务控制块,并在就绪表上登记,在相应优先级位置置1;
- 系统按就绪表的优先级顺序取出已登记的优先级,以此去优先级数组找到对应的任务控制块,并处理任务;
- 处理任务的过程中,可能进入延时、等待事件或中断,此时系统会进入任务调度,重复上一个步骤,执行当前最高优先级任务,同时在就绪表将被中止的任务注销;
- 任务被挂起或者注销时,任务控制块被还原,任务虽然能保留在内存中,但已经与多任务系统没有关系了。
以上分享如有问题和错误,欢迎讨论和指正,关于任务之间的协调和通信,以后有机会继续分享。