引言
μC/OS-II是可移植性好、可裁减的抢占式、实时多任务嵌入式操作系统内核,具有代码量少、安全性高、简洁易懂的特点。在工业自动化、交通控制、医疗仪器、军事国防、航空航天等领域都有广泛的应用,关于μC/OS-II嵌入式操作系统各方面的研究也在不断地深入。
μC/OS-II系统任务间的同步和通信部分主要通过信号量、互斥信号量、事件标志组、消息邮箱、消息队列等实现。其中,事件标志组比信号量、互斥信号量有更丰富的控制功能,事件标志组有多个状态位,任务可以等待其中某一个或多个的状态位置位(或清零),这些状态位对任务起到阻塞作用,这样任务是否能够进入就绪状态就可以由多个条件决定。只有事件标志组的相应状态位满足任务的要求,任务才可以进入就绪状态,这种机制能够有效提高系统的稳定性。
但在应用实践中,发现μC/OS-II的事件标志组机制存在缺陷,这种缺陷可能导致重要敏感领域的灾难性后果。
1 事件标志组缺陷的发现
发现μC/OS-II系统缺陷的工程Mulflag设计如下:用户定义4个任务,其中两个任务TaskpostA、TaskpostB优先级分别为1和3,负责向同一事件标志组的A、B位置位;另外两个任务Taskpend1、Taskpend2优先级为2和5,以OS_FLAG_WAIT_SET_ALL + OS_FLAG_CONSUME的方式等待这一事件标志组的A、B状态位。
图1 工程Mulflag运行图
如图1所示,在PC平台上运行该工程,当TaskpostB事件标志组的状态位置位时,唤醒两个等待的任务Taskpend1和Taskpend2,优先级较高的任务Taskpend1获得CPU使用权,事件标志组的相关状态位复位,而优先级较低的任务Taskpend2没有紧随其后运行,而是被另一个置位事件标志组状态位的任务TaskpostA抢占了CPU使用权。任务Taskpend2在任务TaskpostA之后运行,错误地复位了TaskpostA任务新置位的事件标志组的状态位,即该状态位还没发挥作用就被复位了。
2 缺陷成因分析
μC/OS-II主要通过两个系统调用OSFlagPost和OSFlagPend来实现对事件标志组的相关操作。任务调用OSFlagPend等待事件标志组的事件标志位,调用OSFlagPost置位(或清零)事件标志组的事件标志位。
OSFlagPend的工作原理和步骤如下:
① 查看所需的状态位是否置位(或清零)。如果条件得到满足,则判断是否需要消费事件标志组的状态位,若是则进行消费操作,并从OSFlagPend正常返回;如果条件得不到满足,则挂起等待,并且做任务调度。
② 任务在等待状态被唤醒后,检测被唤醒原因,如果是由于等待条件满足而被唤醒,则判断是否需要消费事件标志组的状态位,若是则消费相关事件标志位。
OSFlagPost的工作原理和步骤如下:
① 置位(或清零)相应事件标志组的状态位。
② 若等待链表不为空,检查等待链表中的任务,若其所等待的状态位都已满足,则将任务的等待事件标志组状态清除,并移出等待链表;若任务状态已就绪,将其加入任务就绪表。
③ 根据移出的任务状态决定是否做任务调度。
当系统有任务执行,OSFlagPost函数对事件标志组的事件标志进行置位(或清零)时,若等待链表不为空,则遍历等待链表,将所有条件得到满足的任务移出该链表,并进行系统调度。若有多个任务被移出等待列表,那么这些被唤醒的任务都会执行复位事件标志组的标志位的操作。但是,在这些被唤醒任务得以执行前存有一个时间窗口,这个时间窗口会导致新的Post任务对标志组的操作被错误地复位。时间窗口产生的原因有两个:一是其他高优先级的Post任务可能抢占CPU而先于被唤醒的Pend任务运行;二是如果Pend任务在等待期间被其他任务挂起,则所请求条件得到满足后依然处于未就绪状态,那么Pend任务在恢复运行前还可能被其他低优先级的Post任务插入运行。
如图2所示,任务TaskpostB唤醒任务Taskpend1、Taskpend2之后,存在时间窗口1、2,随时可能有新的Post任务插入运行,事件标志组的事件标志位有被重新置位或清零的可能性,而这些新设置的标志位将会被后面运行的Pend任务错误地复位。
图2 缺陷原因解析图
3 缺陷的修补方案
从事件标志组缺陷形成的原因看,主要是时间窗口的存在造成的,故消除时间窗口成为解决问题的关键。时间窗口的形成主要是因为从事件标志组等待链表中删除任务和消费任务标志位两个操作分别属于OSFlagPost和OSFlagPend两个函数,如果把消费任务标志位操作移至OSFlagPost当中,则时间窗口就得以消除。
OSFlagPend函数的示意性代码如下:
OS_FLAGSOSFlagPend (OS_FLAG_GRP *pgrp, OS_FLAGS flags, INT8U wait_type, INT16U timeout, INT8U *err){
① 在中断处理函数中不允许调用此函数,是则返回出错信息;
② 做参数的有效性检测;
③ 判断任务是否需要消费任务标志位;
④ 通过switch语句判断任务等待事件标志组的方式,进一步判断等待条件是否得到满足,若是则消费任务标志位并正常返回,否则将任务加入等待链表;
⑤ 做任务调度;
⑥ 判断任务被唤醒的原因,若因为等待条件得到满足而被唤醒,则消费相关事件标志位;
⑦ 返回事件标志组的新状态。
}
其中对第③部分的代码处理如下:
if (wait_type & OS_FLAG_CONSUME) {
//wait_type &= ~OS_FLAG_CONSUME;
//保留任务是否消费的信息,将其传入等待链表保存在OSFlag
//NodeWaitType变量中,供 OSFlagPost查验
consume = TRUE;
} else {
consume = FALSE;
}
将第④部分的switch语句修改为:
switch (wait_type & (~OS_FLAG_CONSUME)) //清除变量wait_type中是否消费的信息,
//以确保能正确判断任务等待事件标志组的方式
第⑥部分的如下代码实现唤醒后消费的操作,注释掉它,将其移至OS_FlagPost实现。
if (consume == TRUE) {
switch (wait_type) {
case OS_FLAG_WAIT_SET_ALL:
case OS_FLAG_WAIT_SET_ANY:
pgrp->OSFlagFlags &= ~OSTCBCur->OSTCBFlagsRdy;
break;
#if OS_FLAG_WAIT_CLR_EN > 0
case OS_FLAG_WAIT_CLR_ALL:
case OS_FLAG_WAIT_CLR_ANY:
pgrp->OSFlagFlags |= OSTCBCur->OSTCBFlagsRdy;
break;
#endif
}
}
OSFlagPost函数的示意性代码如下:
OS_FLAGS OSFlagPost (OS_FLAG_GRP *pgrp, OS_FLAGS flags, INT8U opt, INT8U *err){
(1) 做参数的有效性检测;
(2) 对事件标志组进行置位(或清零)操作;
(3) 查看等待链表,将等待条件得到满足的任务移出等待链表,然后检测任务状态,若就绪则将调度标志置位;
(4) 根据调度标志的布尔值决定是否进行调度;
(5) 获得事件标志组新状态并返回。
}
在OSFlagPost中定义以下新变量:
OS_FLAGSflags_consume; //保存需要复位的相关标志位
OS_FLAGSwaitType; //保存任务等待事件标志组的方式
BOOLEANconsume; //保存任务是否消费的信息
flags_consume=(OS_FLAGS)0; //flags_consume初值为0
第(3)部分遍历等待链表的while循环的起始位置插入如下代码,以判断该任务是否需要做消费操作,并把消费标志从waitType中剔除:
if (pnode->OSFlagNodeWaitType & OS_FLAG_CONSUME) {
waitType = pnode->OSFlagNodeWaitType & ~OS_FLAG_CONSUME;
consume = TRUE;
} else {
consume = FALSE;
}
在第(3)部分,while循环的switch语句内的每一个case分支中添加一段代码,若该任务需要做消费操作,则将需要复位的事件标志组的相关标志位保存在 flags_consume变量中:
switch (waitType) {
case OS_FLAG_WAIT_SET_ALL:
flags_rdy = pgrp->OSFlagFlags & pnode->OSFlagNodeFlags;
if (flags_rdy == pnode->OSFlagNodeFlags) {
if (consume == TRUE) {//所添加代码段
flags_consume |= pnode->OSFlagNodeFlags;
}
if (OS_FlagTaskRdy(pnode, flags_rdy) == TRUE) {
sched = TRUE;
}
}
break;
}
在第(3)部分中while循环之后插入一段代码,对所有满足条件的任务进行统一的消费操作:
switch (waitType) {
case OS_FLAG_WAIT_SET_ALL:
case OS_FLAG_WAIT_SET_ANY:
pgrp->OSFlagFlags &= ~flags_consume;
break;
#if OS_FLAG_WAIT_CLR_EN > 0
case OS_FLAG_WAIT_CLR_ALL:
case OS_FLAG_WAIT_CLR_ANY:
pgrp->OSFlagFlags |= flags_consume;
break;
#endif
}
4 缺陷修补测试
将发现缺陷的工程Mulflag在修改后的内核代码下再次运行,不再出现之前所述的错误,修改内核后任务运行时序图如图3所示。任务TaskpostB在置位并唤醒任务Taskpend1、Taskpend2后,及时复位相关的标志位,而被唤醒的Taskpend1、Taskpend2任务不再执行复位操作。在时间窗口中插入运行的Post任务所做的置位操作,不会再被错误复位。
图3 修改内核后任务运行时序图
结语
该解决方案没有改动事件标志组的数据结构,仅修改了OSFlagPend和OSFlagPost两个函数,并且两个函数的接口也没有变动,修改只涉及到μC/OS-II系统内核源码的OS_FLAG.C文件。其中,OSFlagPend只是注释掉了部分源码,所在的临界区大幅缩短,提高了系统的实时性。而OSFlagPost函数中新增加的代码量只占到所在临界区代码量的1/5,对实时性影响也很小。μC/OS-II的现有工程只需在修改后的内核源码上重新编译即可正常运行,兼容性强。该方案从根源上对事件标志组存在的不足进行改进,直接、彻底地解决了存在的问题,可行性较强。