一、任务的定义
在嵌入式开发中,面对的都是单个 CPU 的情况,而在这个开发过程中,我们会涉及到裸机开发或者是跑操作系统的开发,在裸机开发的过程中,整个系统是以模块的角度来看的,也就是系统在运行完了这个模块之后,再去运行另外一个模块。
但是,在有操作系统的情况下,我们是把系统处理的一件一件事情以任务的角度来进行划分的,这任务与任务之间是并发执行的。每个任务的运行看起来是独立的,从宏观的角度看是多个任务同时在占据着 CPU 的执行,就像是多 CPU 一样,在真正的多 CPU 系统中,每个 CPU 都有一套自己的寄存器,而为了实现这样一种多 CPU 运行的机制,那么操作系统就为每个任务用一块专用的存储空间构建了一个“虚拟 CPU”,用来保存 CPU 内存各个寄存器的信息,这块专用的存储器空间就是“任务堆栈”,有多少个任务就会有多少个任务堆栈。
操作系统为了能够有效地在各个任务之间进行切换,也就是任务之间的调度,那么就必须掌握各个任务的详细动态信息。为此,操作系统为每个任务建立了档案,用来记录任务的这些信息,这就是“任务控制块”。另外,任务有各自的内容,这就是作为开发者来编写的任务函数,来实现这个任务所需要的功能。
综上,我们知道了每个任务都会具有如下三种特征:
- 任务函数
- 任务堆栈
- 任务控制块
二、任务的特性
任务的基本特性是独立性、并发性和动态性。这也是任务和程序模块的本质区别,程序模块通常是用于没有操作系统的裸机开发中。
1、独立性
在传统的程序模块中,一个模块是可以调用另外一个模块的,但是在操作系统中,每个任务都具有自己的 CPU ,即 CPU 为自己独占,这样,一个任务也就不能够像调用子程序那样去调用另外一个任务了。这是关于独立性一个体现。
关于独立性的另一个体现就是任务之间传递信息时,模块之间的传递信息时主模块以实参的形式将信息传递给模块的形参,子模块以返回值的形式将结果传输给主模块。但是在任务之间传递信息却不是这样的,任务与任务之间的传递信息需要借助于第三者,也就是跟操作系统相关联的信号量、邮箱和消息队列等,通过第三者来传递信息也就造成了信息传输是异步的,这也是任务独立性的一个体现。
2、并发性
任务 A 在时刻 t1 到 t4 之间完成,任务 B 在时刻 t2 到 t3 之间完成,它们的运行时间段有重叠部分,这种运行方式称为“并发”运行。在多 CPU 系统中,并发运行着的任务确实都有自己的 CPU ,它们的运行状态就是真正的并发执行,如下图所示:
在单 CPU 中,操作系统的任务调度功能解决了这样一个问题,嵌入式实时操作系统的内核都是采用 “可剥夺型”的任务调度算法,这也就意味着一个已经就绪的高优先级任务可以剥夺另一个正在运行的低优先级任务的运行权而进入运行状态,如下图所示:
3、动态性
任务的状态是动态变换的,这意味着这些任务并不是随时都可以运行的,任务具有以下五种不同的状态下图用状态图的形式表示出来。
三、任务的划分
对一个具体的嵌入式应用系统进行任务划分,是基于实时操作系统应用软件设计的关键,任务划分是否合理将直接影响到软件设计的质量。当任务划分的合理时,软件设计将比较简单高效,否则将可能比较繁杂,甚至失败。在进行任务划分时,具备以下几个原则:
- 首要目标是满足实时性指标。
- 即使是系统处于最坏的情况下,系统中对于实时性要求的功能都能够得到实现。
- 任务数目合理。
- 任务数目合理,当任务数比较多的时候,每个任务需要实现的功能就简单一些,任务的设计也简单一些,但是任务调度操作和任务之间的通信活动增加,使得系统的效率下降,资源开销也变大。当任务划分的数目比较少的时候,每个任务需要实现的功能就比较复杂一些,但是可以免除不少任务之间的通信工作,减少共享资源的数量,减轻系统的负担,减少资源的开销。因此关于任务数目的设计是比较关键的。
- 简化软件系统。
- 一个系统要实现功能,除了设计其本身的功能以外,还需要其具备相应的时间管理,任务同步,任务通信,内存管理等功能。合理地划分任务,可以降低对操作系统的服务要求,能够简化系统软件设计,减小软件代码规模。
- 降低资源需求。
- 减少或者简化任务之间的同步和通信功能,就可以减少相应数据结构的内存模型,从而降低对系统资源的需求。
因此,为了使得任务划分更加合理,通常采用以下几种方法进行任务划分:
四、设备依赖性任务划分
假设,现在有如下一个具备输入输出功能的系统:
通过上述框图大致可以明白整个系统的工作流程,通过键盘输入数据,以及摄像头采集图片信息,送至 CPU 进行处理,然后系统通过输出设备液晶屏以及扬声器输出相关的信息,围绕 CPU 为中心,那么我们就可以这样来进行划分任务:键盘任务,显示任务,数据采集任务,控制输出任务和通信任务。
五、关键任务的划分
“关键性”是指某种功能在应用系统中的重要性,如果这种功能不能够正常实现,则会造成重大影响,甚至能够引发灾难性后果。包含关键功能的任务称为“关键任务”,关键任务必须得到运行机会,即使遗漏一次也是不可行的。
那如何使得关键任务能够准确得到执行呢,我们第一时间所想到的就是提升关键任务的优先级,使其优先级为最高,但是这还不够,我们假设现在有一个火灾报警系统,火灾报警系统大致完成这么几件事,检测火警信号,拨打火警电话,启动喷淋灭火系统,生成并保存火警记录以及打印火警记录。
如果我们把这几件事都包装成一个任务,优先级设置为最高,在系统运行的过程当中,生成并保存火警记录以及打印火警记录时打印机出问题了,这个时候,就会导致当前任务被挂起,而任务被挂起之后,检测火警信号也不能够正常工作了,所以整个系统也就瘫痪了。
因此,对于关键功能来说:必须尽可能和其他功能进行剥离,独立成为一个任务,然后通过通信方式再触发其他任务,完成后续操作。
除了将关键任务和其他功能的任务相剥离,并设置最高优先级以外,还有一种方法能够使得关键任务得到准确执行,那就是采用中断的方式,比如说,在火警的报警系统中,让传感器的火警信号触发一个外部中断,中断发生便完成了信号检测功能,再由中断服务函数使用某种通信机制通知其他任务。下面是示意图:
我们知道为了提高系统的实时性,中断服务函数应该尽可能地短,所以,我们可以进一步进行划分,将与任务通信这部分的程序剥离出来成为一个任务专门用于通信,这样, ISR 负担就更小了,下图是示意图:
由上图我们可以知道多出了一个任务,消息分发任务,消息任务的存在要不能干预到关键任务的运行,但是同时呢,又必须能够及时地通知到其他任务的运行,因此,消息分发任务的优先级也就确定了,其优先级要低于所有关键任务,优先级要高于所有的操作任务。
还有一种情况,就是关键任务不能由中断启动,则可以将该关键功能用一个独立的任务来实现,如下图所示:
这个时候,已经不能用中断的方式来检测报警信号了,那么就需要不停的查询烟雾报警器的状态,防止漏掉了重要的信息。当查询到了报警信息的时候,在通过通信机制通知其他任务完成相应的操作。
最后,要指出的一点是,如果关键任务有严格的实时性要求,那么必须赋予它足够高的优先级,以便及时获得运行权,如果没有实时性要求,那么高优先级并不是必须的,关键是将其他非关键的操作进行剥离,以免受其拖累。
六、紧迫任务的划分
”紧迫性“是指某种功能必须在规定的时间内得到运行权(及时运行),并在规定的时刻前执行完毕(按时完成),可见这类功能有严格的实时性要求,大多数紧迫任务是由异步事件触发的,这些异步事件一般能够引发某种中断。
在这种情况下,将紧迫任务安排在相应的 ISR 中是最有效的方法。如果紧迫任务不能够安排在 ISR 中,那么为它安排尽可能高的优先级是解决“及时性”的有效方法。
要达到按时完成的目的,必须使得紧迫任务需要执行的时间尽可能的短。办法就是对紧迫任务进行瘦身,尽可能地剥离不太紧迫的任务,只剩下必须立刻做的操作,将被剥离的不太紧迫的操作另外封装成一个任务。
下图是一个数据采集任务,数据采集时一个紧迫任务,通过峰值检测电路触发中断,在中断里完成 A/D 转换,下图是整个数据采集系统的示意图:
七、数据处理任务的划分
用户应用程序中消耗时间最多的就是各种数据处理单元,这种单元通常不止一个,他们通常为不同的功能服务。应该将这些单元划分出来,分别包装成不同的任务。由于这类任务需要消耗较多的时间,那么他们的优先级必须安排的比较低。
除此之外的方法,如果操作系统支持将多个任务安排相同优先级的机制,那么当有多个数据处理任务时,可安排相同的优先级,采用时间片轮转的方式运行。如果操作系统不支持多个任务具有相同的优先级,那么可以将多个需要并行的数据处理任务分成多个数据处理任务,原理如下图所示:
八、功能聚合任务的划分
正如标题的意思所示,功能聚合任务的划分,也就是将关系紧密的任务组合成一个任务,达到功能聚合的效果,那什么样的任务才能称之为是关系紧密的任务呢,一般满足以下两点要求:
- 数据关联紧密
- 时序关联紧密
至于要将其组合成一个任务的原因也很简单,是因为如果将关系密切的功能分别用不同的任务来实现,那么就需要进行大量的数据通信和同步通信,这对于系统而言是一个很大的负担。
九、触发条件相同任务的划分
如果若干功能由相同的事件触发,则可以将这些功能组合成为一个任务,从而免除将事件分发给多个任务的工作量。
但这也必须有一个条件,就是当以某种次序顺序执行这些功能的时候,各个功能的实时性要求仍然可以得到满足,这是关键。
同时,这里说的触发条件相同的任务,通常外部触发的任务一般是关键任务或者紧迫任务,需要按照前文所述的方法来进行任务的划分,符合本类任务的触发条件通常是内部事件,比如说一个时钟事件,也就是说到了指定时间就触发这个任务,或者说是某个任务运行之后得到一个结论,这结论又触发了一个任务。如下图示意图所示:
另外,需要注意的是,在任务内部,各个功能的执行顺序要尽可能安排的合理一些:
- 如果各个功能之间存在有因果关系。则按因果关系安排执行顺序,如先计算,后输出结果
- 如果各个功能之间完全独立,则按照实时性要求的强弱安排执行顺序。
十、运行周期相同任务的划分
将周期相同的功能组合在一起封装成一个任务,就能可以避免一个时间触发几个任务,省略去事件分发操作和他们之间的通信,能够减轻系统的负担。
十一、顺序操作的任务的划分
如果若干功能按照固定的顺序进行流水作业,相互之间完全没有并发性,那么应该将这些功能组合成为一个任务。
十二、总结
通过上述的论述,我们知道了在一个 RTOS 中应该如何进行任务的划分,在最后,再进行精炼一下,总结为如下几点:
-
以 CPU 为中心,将与各种输入/输出相关的功能划分为独立的任务
-
将关键功能剥离出来用一个独立的任务或者是 ISR 去完成,剩余的部分用另外一个任务实现,两者之间通过通信机制进行沟通
-
将紧迫功能剥离出来用一个高优先级的任务或者 ISR 去完成,剩余部分用另外一个任务实现,两者之间通过通信机制进行沟通
-
对于既关键又紧迫的功能,按照紧迫功能的处理方法对齐进行处理
-
将消耗 CPU 时间较多的数据处理功能划分出来,封装成低优先级的任务
-
将关系密切的若干任务组合成一个任务,达到功能聚合的效果
-
将由相同事件触发的若干功能组合成为一个任务,从而免除事件分发机制
-
将运行周期相同的任务组合成为一个任务,从而免除时间事件分发机制
-
将若干按固定顺序执行的功能组合成为一个功能,从而免除同步接力通信的麻烦