摘要:Small RTOS51是一款重要的小型实时内核,消息队列是其提供的重要任务间通信的机制。针对其消息队列实现代码中的缺陷以及可能导致的消息丢失这一严重问题,从操作系统等待与唤醒机制理论的角度出发,剖析Small RTOS51内核在消息队列甚至互斥型信号量等实现机制上的漏洞所在;进一步指出原内核实现方式的修改方法,以及《Small RTOS51中消息队列的一处隐患》作者提出的第2种修改方法的完美实现。
关键词:Small RTOS51 消息队列 唤醒模型 隐患分析
引言
贵刊2005年第7期《Small RTOS51中消息队列的一处隐患》一文,对Small RTOS51V1.12.1版本的消息队列机制进行了周密的分析,不但找出了问题所在,也提出了相应的两种解决方法[1]。实时嵌入式系统对于安全性有很高的要求,作为实时嵌入式系统的内核,不但要求精简高效,更要加强安全,防止因操作系统出错而在应用领域导致灾难性的后果。因此原文作者所做的工作极有价值,同时也感谢贵刊对这一领域的高度重视。
因为这一问题涉及到内核的等待与唤醒机制,并且正是由于对内核的等待与唤醒机制的理解与运用不同,才导致了问题的出现,所以本文从操作系统理论的高度以及目前主流的实时内核的实现方法两方面入手论述这一问题,并揭示如何才能完美实现原文的第2种方法。
1 内核唤醒机制的三种模型
当利用系统调用接口获取资源时,如果资源不满足,系统调用可以返回错误,也可以根据选项悬挂等待;当有任务释放资源从而资源可以满足时,就要将资源等待队列中的相关任务唤醒。唤醒模型有三种[2]:
第1种,将该资源等待队列中的任务全部唤醒,让这些任务与系统中的其他任务平等竟争资源。这种策略会使系统在一段时间内繁忙,因为最终只有一个任务获取到资源,其他任务可能将经历一个从就绪态到运行态再到阻塞态的过程。这种现象在操作系统理论上称为“千军万马奔腾”。就目前的一些主流实时内核VxWorks、Nucleus、uC/OS?II等来讲,都没有采用这种策略。
第2种,将该资源等待队列中的一个任务唤醒,依据所采用的策略不同,可以是等待任务中优先级最高的,也可以是第1个进入等待队列中的任务。这个任务被唤醒后将和系统中的其他任务一起竞争这个资源。如果这个任务最终没有竞争到这个资源,它将再次进入该资源的等待队列并进行任务调度。
第3种,将该资源等待队列中的一个任务唤醒,依据所采用的策略不同,可以是等待任务中优先级最高的,也可以是第1个进入等待队列中的任务,这点和第2种方法是一样的。和第2种情况不同的是,这个任务被指定为资源的获得者。主流实时内核VxWorks、Nucleus、uC/OS?II等都采用这种策略。以VxWorks为例,其内核文档指出[3]:“任务或ISR调用msgQSend()向消息队列发送消息。此时如果没有任务在等待该队列中的消息,那么该消息进入消息队列的缓冲;如果有任务等待该队列的消息,那么这个消息立即提交给第1个等待的任务。”这段话有两方面的含义:① 明确指出第1个等待的任务获得资源;② 第1个等待的任务获得资源的方式是直接从消息的发送者那里获得,也就是说这个消息将不进入消息队列进行缓冲,消息在发送者和接收者之间进行手把手的传递。对于这种机制的实现,可以以著名的源代码公开的实时嵌入式操作系统Nucleus为例。下面是Nucleus内核关于接收消息的一段精彩的代码:
else {
/* 消息队列为空,决定是否悬挂等待*/
if (suspend) {
/* 增加等待该消息队列的任务数量 */
queue -> qu_tasks_waiting++;
/* 填充悬挂块数据结构并且悬挂该任务*/
suspend_ptr =&suspend_block;
suspend_ptr -> qu_queue=queue;
suspend_ptr -> qu_suspend_link.cs_next=NU_NULL;
suspend_ptr -> qu_suspend_link.cs_previous=NU_NULL;
suspend_ptr -> qu_message_area=
(UNSIGNED_PTR) message;
suspend_ptr -> qu_message_size=size;
task=(TC_TCB *) TCT_Current_Thread();
suspend_ptr -> qu_suspended_task=task;
/* 判断该消息队列的等待方式是先进先出还是按任务
的优先级 */
if (queue -> qu_fifo_suspend) {
/* 是先进先出等待方式,将悬挂块链入消息队列
的等待链表 */
CSC_Place_On_List((CS_NODE **)
&(queue -> qu_suspension_list),
&(suspend_ptr -> qu_suspend_link));
}
else {
/* 按优先级方式将悬挂块链入任务等待链表的
合适位置 */
suspend_ptr -> qu_suspend_link.cs_priority =
TCC_Task_Priority(task);
CSC_Priority_Place_On_List((CS_NODE **)
&(queue -> qu_suspension_list),
&(suspend_ptr -> qu_suspend_link));
}
/* 悬挂调用任务,并自动取消该消息队列的临界区
保护 */
TCC_Suspend_Task((NU_TASK *) task,
NU_QUEUE_SUSPEND,
QUC_Cleanup, suspend_ptr, suspend);
/* 获取该系统调用要求的返回状态以及返回值*/
status =suspend_ptr -> qu_return_status;
*actual_size =suspend_ptr -> qu_actual_size;
}
else
/* 在消息队列为空以及不等待的方式下,返回状态
指示消息队列为空*/
status =NU_QUEUE_EMPTY;
}
这段代码是处理消息队列中没有消息时的情况的,并且在不进行悬挂等待时返回码是NU_QUEUE_EMPTY,提示队列为空。我们注意到在选择悬挂等待的情况下,填充了suspend_ptr指针所指的一个悬挂块结构,suspend_ptr -> qu_message_area填充的是接收任务指定的接收缓冲区指针,suspend_ptr -> qu_message_size填充的是接收任务指定的接收消息长度。接下来依据不同的等待策略(任务优先级或FIFO),将填充好的消息队列悬挂块链入该消息队列的悬挂等待链表中,进行任务调度。正是有了这个消息队列悬挂块数据结构,将来发送消息的任务依据这个悬挂块中指定的接收消息缓冲区指针,把消息从发送任务直接复制到接收任务。当接收消息的任务被唤醒并获得执行权后,只是简单地依据悬挂块中的相关域的内容返回系统调用而已。从上述分析可以看出,悬挂块数据结构起着重要的作用,它不仅标明了是哪个任务在等待,也标明了等待任务的一些详细信息,同时也有结果状态域。通过对Nucleus内核定时器机制的分析得知,在任务等待资源超时的情况下,悬挂等待块的结果状态域将被填充NU_TIMEOUT。
2 针对Small RTOS51消息队列的分析
有了上述三种模型的分析,很容易看出Small RTOS51V1.12.1版消息队列所采用的是第2种模型,只是在实现时出现重大遗漏,被唤醒的任务没有竞争到资源时应重新进入等待表,而其内核代码却没有体现到这一点。这一点《Small RTOS51中消息队列的一处隐患》的作者已经分析得很清楚,其提出的第1种解决方案也很正确。重点是第2种解决方案。第2种解决方案属于第3种模型,但其实现技术欠佳。正如原文作者所指出的那样,第2种方案具有其自身不可调和的矛盾:“在发送消息的OSQIntPost()函数中,如果检测到有任务正在等待此消息,则并不把消息数(buf[0])加1”,但这个消息毕竟进入消息队列了,这就造成了一种矛盾状态,消息数与消息队列中的实际消息不相符。为了实现第3种模型的效果,即被唤醒的等待任务获取资源,在消息数为0的情况下,原文作者通过进一步判断该任务是否还处在消息队列的等待任务表中,来决定该任务是否从消息队列中获取消息;但消息数为0而消息队列中还有消息却为发送消息带来隐患。要想解决这一矛盾,OSQIntPost()在唤醒等待任务的同时就应该将该消息传递给这个任务,这样消息数仍然为0才不留隐患。uC/OS?II实现这一策略的技术是任务被唤醒后检查任务控制块中的OSTCBCur->OSTCBMsg这一数据域[4,5],获取到的消息指针在此。注意,OSQPost()在有等待任务的情况下,如下处理:
if (pevent->OSEventGrp != 0x00) { /* 判断是否有任务悬挂在消息队列的等待表中 */28OS_EventTaskRdy(pevent, msg,OS_STAT_Q); /*将等待表中最高优先级任务唤醒*/
OS_EXIT_CRITICAL();
OS_Sched(); /* 进行任务调度,运行最高优先级任务*/
return (OS_NO_ERR);
}
即消息指针没有进消息队列并且消息指针通过OS_EventTaskRdy(pevent, msg, OS_STAT_Q)传给被唤醒的任务。这一作法符合第3种模型。
由此可见,Small RTOS51V1.12.1要想实现第3种模型,其内核的数据结构需要有一些变化,像原文第2种方案那样修改代码,是不能最终解决问题的。同Nucleus相比,实现消息队列时,uC/OS?II虽然没有引入悬挂等待块的概念,但其通过在任务控制块中引入相应数据项来最终实现第3种模型,并且结果是在任务被唤醒后进行判断的。
3 结论
虽然各种各样的实时嵌入式操作系统千差万别,但从操作系统理论的角度分析,很容易将它们纳入到某一具体的模型;实现细节有很大的不同,但其实现的功能应符合通用原理。在操作系统理论的指导下,结合具体的实例源代码分析、理解和应用,才能有更大的把握。
参考文献
1 陈皓. Small RTOS51中消息队列的一处隐患. 单片机与嵌入式系统应用,2005(7)
2 Jim Mauro,Richard McDougall.Solaris内核结构.北京:机械工业出版社,2001
3 孔祥营,等. 嵌入式实时操作系统VxWorks及其开发环境Tornado. 北京:中国电力出版社,2001
4 Labrosse Jean J.uC/OS?II——源码公开的实时嵌入式操作系统.北京:中国电力出版社,2001
5 Labrosse Jean J.嵌入式实时操作系统uC/OS?II.北京:北京航空航天大学出版社,2003
韩明峰:硕士,主要研究方向为实时嵌入式系统。