2009年4月17日

μC/OS-II 研究系列 - 004

事件控制块 & 任务间同步

上篇中, 我出尔反尔了一回, 没有按照上上篇的「预告篇」来写这个系列. 在这篇里, 咱们继续出尔反尔. 囧
下周五要和一个老师去某公司谈一个项目的具体事宜, 所以接下来一段时间里, 除了毕业设计, 就是那个项目了, 争取两个月时间, 也就是这个月, 和下个月搞定之.
不知道这个「研究系列」还有没有时间来写.
至于这个「研究系列」的命运, 参考文章末尾.

ok, 步入正题.
对于一个操作系统而言, 任务和任务, 以及任务和中断服务子程序之间, 同步和通信是必不可少的.
既然需要同步和通信, 那么彼此之间发送的信号, signal, 就是其媒介了. 在 μC/OS-II 中, 信号被看作是事件, event, 而用于描述和表征「事件」的, 是被称为事件控制块的结构体, 简称做 ECB, 也就是 Event Control Block, 下文对「事件控制块」和 ECB 不加区分, 混合使用.
---------------------------------
三种同步方式
#1
信号量 Semaphore:
譬如, 对某资源A实施信号量保护, 也就是, 只有获得信号量的任务, 才能够访问资源A.
如果该资源允许 N 个任务同时访问, 那么信号量定义成 N 就ok了.
每被一个任务捕获, 信号量减一. 当信号量为0时, 所有试图访问A的任务都会被返回一个出错代码, 抑或是 suspend, 开始等待另外的任务释放信号量.
#2
互斥量 Mutex: 也就是 mutual exclusion.
如果打个比方, 就是「一山不容二虎」, 你可以先将其简单的理解成 semaphore = 1; 时的情况.
不过, 在 μC/OS-II 中, 千万不能这么简单理解, 否则代码就 100% 功能紊乱了. 下文中将加以详细的描述.
#3
事件标志组 Event Flag Group: 还是举例吧, 语言表达没有直接写代码迅速.
下文中将贯穿这个思想, 毕竟有时候, 读代码确实比看文字更容易理解整体的架构, especially when the author himself become impatient of writing the tedious article. -_-

首先指出, 下面的代码可不是能够直接用于 μC/OS-II 的代码, 是我为了方便描述而临时写的一段.
typedef unsigned long os_event_flag_grp;
/* 事件标志组中的某bit置1时, 表示某事件发生 */
#define FLAG_TYPE_SET 1
#define FLAG_TYPE_CLR 0

os_event_flag_grp flag_grp;
if (something_a)
flag_grp |= 0x0001;
if (something_b)
flag_grp |= 0x8000;
if (something_c && something_d && something_e)
flag_grp |= 0x0400;

明白了么? 譬如当 something_b 发生时, 标志组的第15个bit就会置一. 至于内核中具体的实现方式, 下文中将加以描述.
---------------------------------
事件控制块
这算是内核中最为重要的结构体之一了. 其重要程度, 丝毫不逊色于前面提到的「任务控制块」, task control block.
为了贯彻上文中提到的「写代码, 读代码, 少打字」的主张, 咳咳, 大家伙直接看源码吧:
#if (OS_EVENT_EN > 0) && (OS_MAX_EVENTS > 0)
typedef struct {
INT8U    OSEventType;                    /* Type of event control block (see OS_EVENT_TYPE_???)     */
INT8U    OSEventGrp;                     /* Group corresponding to tasks waiting for event to occur */
INT16U   OSEventCnt;                     /* Semaphore Count (not used if other EVENT type)          */
void    *OSEventPtr;                     /* Pointer to message or queue structure                   */
INT8U    OSEventTbl[OS_EVENT_TBL_SIZE];  /* List of tasks waiting for event to occur                */
} OS_EVENT;
#endif

这里要特别指出的是, 在v2.52的后续版本中, 此结构体还多出了一个 OSEventName[], 顾名思义, 也就是用来存储 ECB 的名字了.
#1
OSEventType 是事件类型, 也就是譬如 semaphore, mutex, message box, message queue 等等.
譬如, 信号量 sem 和互斥量 mutex 都是使用 ECB 来描述. 因此, 在调用相应的系统函数前, 就要先检查, 被处理的 ECB 究竟是被 sem 调用了, 还是被 mutex 调用了.
#2
OSEventGrp & OSEventTbl[] 用来存放正在等待此 event 的 tasks 的信息. 其原理和系统的任务就绪表一模一样.
#3
OSEventCnt 是一个16位的整型.
当事件是信号量时, 其用于表示信号量的数值.
当事件是互斥量时, 高8位用于存放 PIP, Priority Inheritance Priority, 低8位用于存放当前占用此 mutex 的任务的优先级.
#4
void *OSEventPtr 元素. 可以这么说, 在消息队列和消息邮箱中, 这个指针才会被真正用到, 所以这里就先不提了.
在「空余ECB列表」中, 用于指向下一个空余的ECB. 在信号量和互斥量中, 指向 NULL 指针, 也就是不被使用.
#5
事件控制块的数量是有限的.
这个最大数值由 os_cfg.h 中的 #define OS_MAX_EVENTS ? 指定.
系统中有一个所谓「空余ECB列表」, 由一个指针变量, *OSEventFreeList, 指向一串没有被使用的ECB, 形成一个单向链表. ok, 你或许已经联想到了, 和前面一篇文章的 *OSMemFreeList 一样的道理.
如果需要创建一个 sem 抑或是 mutex, 那么必须先从这个单向链表中分配到一个空余ECB.
#6
如何操控某个ECB? 也就是:
如何初始化ECB? 如何将某个任务添加到某个事件的 waiting list 中? 如何在事件发生时, 确定该由 waiting list 中的哪个任务捕获该事件? 如果任务等待了太长的时间, 超过了其「忍耐的极限」, 也就是 time out 的时候, 作何处理?
带着这些疑问, 请您分别阅读源代码中这几个函数:
OS_EventWaitListInit();
OS_EventTaskWait();
OS_EventTaskRdy();
OS_EventTO();

** 注意: 这几个函数都是 kernel 的内部函数, 供其他系统函数调用.
用户空间的代码严禁直接调用. 也就是说, 您可以和我一样, 出于对内核大换血的态度来研究之, 但是, 别在用户空间的 task 中直接使用之.
---------------------------------

---------------------------------
....
---------------------------------

彻底懒得写了. 太多了. 这个东西不是线性的描述, 而是图状的描述, 所以写起来感觉格外艰难. 如果只是写「API使用指南」倒是轻松多了, 毕竟外围的 API 都做了很好的粒度和范畴切分, 可以按照 linear 的顺序一步步介绍.
而且, 感觉这个 serial 貌似已经超越了起初 study-note 的初衷, 成为「内核剖析指南」了.
ok, 罢笔. -_-

下面将我没有写完的内容在这里列出来:
任务间同步: 信号量 Semaphore; 互斥量 Mutex; 事件标志组 Event Flag Group.
任务间通信: 消息邮箱 Message box; 消息队列 Message Queue.
基础结构: 任务管理 Task management; 时间管理 Time management.
其他: 就绪表, 任务调度; 任务切换, 中断里的切换; 时钟节拍; 系统初始化 & 启动.

然后大致说一下:
#1
信号量和互斥量一定要弄清楚. 特别是互斥量的 PIP 机制, 由于内核缺乏「优先级继承」的机制, 容易出现优先级反转的问题, 所以在互斥量中人为引入 PIP, 能够在一定程度上破解优先级反转.
#2
事件标志组也很重要, 而且范围广泛, 运用很灵活. 某一组事件发生后, 所有关联的 task 全部被激活. 这一点和信号量以及互斥量不同, 这也决定了其使用的场合也不同.
在使用时, 尤其要记得 wait_type | OS_FLAG_CONSUME 的组合等待模式设置.
#3
消息邮箱和消息队列非常重要, 是任务间通信的重要方式. 至于所谓「消息」, message, 本质上也就是一个 pointer 了, pointer 用于指向若干任务间通信的某个信息片.
#4
任务管理, 没得说, 创建任务删除任务挂起恢复任务都得靠它. 时间管理, 更加不用说了, 在时序系统中, 其重要性应该算是路人皆知了. 这两部分都是最基础的构件, 需要重点掌握.
#5
就绪表, 任务调度. 其实这部分的实现挺容易的. 就是讲述起来需要图文结合才行, 大家伙如果有兴趣, 就自己看看源代码, 看看我先前提到的 MicroC/OS II: The Real Time Kernel 吧.
#6
任务切换, 中断里的切换. 这两者是不同的.
具体的实现和硬件联系紧密, 移植时需要用汇编编写之.
不过我只是知道原理, 暂时还没来得及具体研究针对某个处理器的移植范例. 囧
针对处理器的移植范例可以在官方站点上下载.
#7
时钟节拍. 依靠硬件的定时装置, 定时运行 OSTimeTick(), TCB 中的 OSTCBDly 就依靠这个来定时自动减一, 一旦到0, 便进入就绪态等待调度.
#8
系统初始化 & 启动. 算是一个小综合部分了. 同用户空间 task 编写联系也很紧密.

ok, 到此为止吧. 我终于体会到那些英文原版的作者为何都要在 preface 中加上一句: 感谢我的家人没有对我日复一日的敲击键盘和不理不睬感到愤怒.
我在这里也写上一句, 算是后记吧, 哈哈哈哈. 囧

I hope you, the kind reader, will not find this serial be wordy or even tedious, though myself think it somewhat really is.
I will be very happy and proud if you step into the world of μC/OS-II & RTOS with this serial of articles.
Enjoy your adventure. You brave hacker. ;)


- End of Serial -

没有评论:

发表评论

不要使用过激的暴力或者色情词汇.
不要充当勇猛小飞侠 --- 飘过 飞过 扑扑翅膀飞走 被雷得外焦里嫩地飞走.
万万不可充当小乌龟 --- 爬过.
构建河蟹社会 责任你有 我有 大家有 -_-

Creative Commons License 转载请指明出处. 谢谢合作.
/***********************
author: jtuki
http://jtuki.blogspot.com/
***********************/