ucosⅡ分析——同步机制
uCOS同步机制的硬件支持是一个比较坏的方案——关中断,不过由于uCOS的应用领域是单核的嵌入式实时机器,关中断的无法用于多核情况的弊端可以忽略;但关中断的时间消耗还是让效率有所下降(可能是成本较高导致原子操作的底层支持没有)!至于其实现方案和细节下面会仔细陈述;另外在最后讲述一些关键却零碎的关键点。
1 实现的机制
同步机制主要通过事件的释放和等待操作实现,(取消)挂起任务作为事件实现同步机制的补充。事件的具体实现一共有邮箱(mail box)、互斥锁、信号量、消息队列和标志组五种形式;以释放和等待操作含义划分可以分为三种:
① 消息的放入和取出。邮箱和消息队列的释放操作是将消息放入相应的结构中;而等待就是取出一个消息,不过若无消息则进入阻塞状态。
② 资源的释放和占有。信号量和互斥锁的释放操作是将占有的资源释放,将事件所代表的资源数加一;而等待就是占有事件中资源的一份,不过若无空闲资源则进入阻塞状态。(互斥锁有特殊的地方)
③ 状态的更新和匹配。标志组的释放操作是以指定的模式和状态去更新标志组的标志状态,若更新后其阻塞队列中任务等待的标志状态有与之匹配的标志则将其转变就绪状态(可能改变多个任务的状态),并在最后任务调度一次;而等待是以一个特定的标志状态去匹配标志组的标志状态,若不匹配则进入阻塞。
前两种在释放的最后将其阻塞队列中优先级最高的任务转变成就绪状态,并也进行一次任务调度。特别注意,三种类型释放将等待任务转变到就绪状态的最大前置条件是,任务没有因其他情况被阻塞,在uCOSⅡ中,其他情况只可能是被挂起(零碎点会讲为什么)!
2 实现细节
在这五种实现中,除了消息队列和FLAG_GRP标志组实现,event结构体可以表示所有的事件。其属性event type记录了事件的类型(宏定义规定值和类型的映射);属性OSEventPtr指针指向邮箱的信息或指向占有互斥锁的任务控制块、消息队列的实质结构;属性OSEventCnt有些特殊,在信号量时它代表资源的数目;而在互斥锁的时候低八位表示占用锁任务的优先级,高八位表示防止优先级导致而预备的较高优先级,另外,互斥锁还用OSEventPtr指向占有锁的进程。
除了互斥锁,OSEventPtr和OSEventCnt在一个事件里,只有一个有效。值得一提的是,互斥锁预备的较高优先级是其占用的,也就是说在此互斥锁在系统中仍存在时,互斥事件的OSEventCnt所代表的两个优先级有一个不能给实际任务使用,这导致了稀有资源优先级的减少,因此在设计的应用中存在互斥锁时要考虑优先级是否够用(除非设计逻辑上避免了优先级倒置的现象发生)。
一些以_DATA为后缀的事件结构体的存在,仅仅是为了更好地;更具体地表示特定一类事件类型,以供用户进程查询;这也是为什么所有事件实现的方法中都提供XXX Query函数。
而特殊的消息队列和标志组的细节如下所示:
① 消息队列:
a) 结构体。其实质是一个指向消息指针的指针数组,不过其封装了下一条插入、获取消息的地址(实际上还是数组中的一个);并有着数组首尾(尾是一个无效地址即不在数组内)地址和队列目前信息数量的信息。这些信息提升了消息队列操作的效率。
b) 对event的依赖。不过其数据结构存储在event结构体之外,并通过event的OSEventPtr记录其存储地址;之所以不完全脱离event,是因为它需要借助event了解具体有哪些进程在等待它。
② FLAG_GRP:
a) 数据结构。FLAG_GRP结构体由一个标志状态、事件标识和指向阻塞队列指针组成。标志状态是一个16位的数,其取决于OS_FLAG_GRPS被定义多长字节的数据类型。
b) 独立性。FLAG_GRP同步实现是不属于event体系的一种事件实现,其没有依赖event结构体存储等待队列,甚至连事件类型都是FLAG_GRP结构体自身存储;另一方面,event体系阻塞队列中的任务都是等待同一个事件,但FLAG_GRP中的阻塞队列task等待可以是不同的事件,这是一个本质区别!而这个区别是依赖FLAG_GRP的阻塞队列的结构。
c) 阻塞队列结构。其结构是一个链表,而每个节点存储了一个等待任务和其期望的标志。但特殊的是每个等待的进程所期望的标志状态可以不同,即16位0/1可以的组合可以不同!
3 零碎的关键点
首先简单介绍一下,系统如何识别任务的状态。识别机制是依靠TCB中状态字(OSTCBStat)属性,其二进制上的每一位的值表示任务是否处于某一个状态(状态可以组合),第一位的值若是1则代表着任务状态属于第一位所规定的状态;而当每一位均为0的时候,进程便是就绪状态。而第几位隐喻任务属于哪一类状态,请参考图2-1。值得注意的是,不同任务状态的二进制上的1是无任何重叠的,因此系统允许状态组合,不过仅存在一个事件类型状态和悬挂的阻塞状态组合(由于休眠状态没有任务状态,所以没有算),接下来详细论述。
其次论述,不考虑休眠状态,uCOSⅡ中的进程只可能同时因一个事件和悬挂而阻塞。由于事件等待若超时,事件等待的队列中会删除进程,进程的任务状态同时会移除相应事件的任务状态,即将当前任务状态与上对应的事件任务状态字(OS_EventTaskRdy的功能),因此task及时在等待超时后去等待另一个事件,其任务状态字和阻塞队列中只会显示等待一个事件。而悬挂和事件体系是独立的;且在task在等待事件的同时,其他任务可以悬挂其;因此进程最多同时因等待一个事件和被悬挂而阻塞。
最后抛出并解决一个关键问题——等待事件的进程如何获得事件的句柄?这个问题是同步机制的第一步,而且本来很好解决,只需要在事件数组中索引检索即可,但是由于uCOSⅡ的事件建立和删除都是在事件的空闲事件链表(线性数组)的头进行操作,即使是有单独的空闲链表的消息队列和标志组也是如此;在多任务情况下无法预计事件数组中某一特定位置的事件一定是假设事件!因此从全局变量事件数组中获得是不可靠、不可行的。我研究出解决该问题的可行方案有两个:
① 父进程建立子进程的时候将事件的句柄作为参数传递给子程序。考虑到嵌入式系统中大多以作业为单位允许,即一个父进程构建一些子进程完成一个任务的情况,该方案可以满足绝大数需求,但是比较大型的应用中作业间同步无法获得,而方案二可以解决这个问题。
② 规定一个空闲的内存地址为事件的首地址,并用内存控制块管理其整个数据结构的内存空间;而且事件的拥有者不依赖系统创建和删除事件。该方案其实破坏了事件的封装性,但可以事件作业间通讯同步的第一步。