显示标签为“μC/OS-II 系统研究”的博文。显示所有博文
显示标签为“μC/OS-II 系统研究”的博文。显示所有博文

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 -

2009年4月12日

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

内存管理

上篇中提到了「下篇预告」和「下下篇预告」, 这两部分尽管都已经研究过了, 但是似乎讲述顺序上不太合理. 所以这里要出尔反尔, 打乱一下讲述顺序了. 囧
内存管理是一个很重要的部分.
---------------------------------
为何需要内存管理
这似乎是一个很废话的问题, 但是对于我等没有接触过「操作系统」课程的, 曾经比较颓废的青年来说, 还是很必要的. 后悔莫及已经逝去的岁月, 罢了罢了, 已经过去的事情就不要谈了.
譬如, 你的用户程序需要 nmem_blk * blk_size 大小的内存块, 那么你可以使用 malloc(nmem_blk * blk_size) 来获取之. 但是这样有几个缺点: 这个函数的执行时间并不确定, 对于实时系统而言, 这不是一个好兆头; 经常的 Type *p = (Type *)malloc(N); free(p); 会导致大量的「内存碎片」产生.
说实话, 「内存碎片」这个术语我今天是第一次听说, 真的是很汗颜. 决定开始仔细研究OS相关课程, 包括「深入理解计算机系统」这样的书籍.
为了避免这样的情况, μC/OS-II 引入了自己的内存管理算法. 其执行时间是常数, 而且有效避免了内存碎片中的所谓「外部碎片」, external fragments, 也就是最让嵌入式开发头疼的内存碎片类型了.
---------------------------------
内存管理函数接口
如果不需要动态分配内存, 可以将其关闭, #define OS_MEM_EN 0, 反之开启, #define OS_MEM_EN 1.
接口函数:
/***********************
创建一个内存分区;
分区内含有nblks个内存块, 每个内存块大小是blksize个字节;
返回值是一个指针, 指向此分区的内存控制块.
***********************/
OS_MEM  *OSMemCreate (void *addr, INT32U nblks, INT32U blksize, INT8U *err);
/* 指定一个内存分区, 从中拿出一个空闲内存块; 返回值是一个指针, 指向这个内存块 */
void  *OSMemGet (OS_MEM *pmem, INT8U *err);
/* 将pblk指向的内存块, 放回pmem所控制的内存分区 */
INT8U  OSMemPut (OS_MEM *pmem, void *pblk);
/* 获取pmem所控制的内存分区的信息, 放入pdata中 */
#if OS_MEM_QUERY_EN > 0
INT8U  OSMemQuery (OS_MEM *pmem, OS_MEM_DATA *pdata);
#endif

---------------------------------
内存控制块
很重要的一个结构体, 用于控制内存分区. 每一个内存 partition 就会有一个 control block.
#if (OS_MEM_EN > 0) && (OS_MAX_MEM_PART > 0)
typedef struct {
void   *OSMemAddr;                    /* Pointer to beginning of memory partition                  */
void   *OSMemFreeList;                /* Pointer to list of free memory blocks                     */
INT32U  OSMemBlkSize;                 /* Size (in bytes) of each block of memory                   */
INT32U  OSMemNBlks;                   /* Total number of blocks in this partition                  */
INT32U  OSMemNFree;                   /* Number of memory blocks remaining in this partition       */
} OS_MEM;
#endif

注释里都阐述的很清楚了, 就不再赘述了.
---------------------------------
有点不想写了, 貌似写这类系列文章好麻烦. 篇幅太大, 时间不够. 直接就贴图吧. 反正主要是备份所需嘛. 囧
系统初始化时, 建立的空闲内存控制块链表:

创建内存分区 OSMemCreate() 函数, 将内存分区, memory partition, 中的所有内存块, memory blocks, 用一个单向链表连接起来, 这一点非常关键:

---------------------------------
具体的源码实现
不分析了, 大家伙下载阅读吧. -_-
这里说一下注意点:
#1
OS_ARG_CHK_EN 宏定义用于指出「是否检测给系统函数传递的参数」, 如果置零, 将会压缩一部分代码空间, 但是最好还是置一.
#2
内存控制块中, 有一个 void *OSMemFreeList; . 其用途有两个:
1. 当这个内存控制块未被赋值给某个内存分区时, 也就是当这个控制块依然是处于「空闲内存控制块链表」中时, OSMemFreeList 是指向链表中的下一个空闲内存控制块.
2. 此控制块被分配给某个partition后, OSMemFreeList 指向partition中的下一个free memory block 空闲内存块. 换句话说, 此时的 OSMemFreeList 指向分区中的空闲内存块单向链表的头结点.
#3
内存控制块中的 OSMemFreeList 内部标识符, 和系统的全局变量 OSMemFreeList 相同, 务必注意区分. 可以参考上面两张图片的第一张.
#4
释放某个内存块, INT8U OSMemPut (OS_MEM *pmem, void *pblk); .
牢记: 系统没法动态确认 pblk 指向的内存块是否是属于 pmem 控制的内存分区.
这需要我们自己小心. 不然随时可能会因为内存分配问题而程序崩溃.
#5
在某个 partition 没有free block 的情况下, 让某个申请内存块的任务暂时进入waiting状态, 是可以实现的.
系统并不支持, 但是可以对某个特定的内存分区使用计数型信号量, 就ok了.
#6
在阅读源码的过程中, 你可能会碰见类似这样的代码:
plink = (void **)addr;                            /* Create linked list of free memory blocks      */
pblk  = (INT8U *)addr + blksize;
for (i = 0; i < (nblks - 1); i++) {
*plink = (void *)pblk;
plink  = *plink;
pblk   = pblk + blksize;
}
*plink              = (void *)0;                  /* Last memory block points to NULL              */
OS_ENTER_CRITICAL();
if (pmem->OSMemNFree > 0) {                       /* See if there are any free memory blocks       */
pblk                = pmem->OSMemFreeList;    /* Yes, point to next free memory block          */
pmem->OSMemFreeList = *(void **)pblk;         /*      Adjust pointer to new free list          */
pmem->OSMemNFree--;                           /*      One less memory block in this partition  */
OS_EXIT_CRITICAL();
*err = OS_NO_ERR;                             /*      No error                                 */
return (pblk);                                /*      Return memory block to caller            */
}
OS_EXIT_CRITICAL();
我对于这两段代码非常惭愧, 因为我经常自认为对指针等非常熟练, 可惜这个指向指针的指针 *(void **) pblk; 让我想了差不多10分钟才想明白. 真是羞愧难当啊.
现在简单阐述如下:
pblk指向某物理地址addr_pblk;
(addr_pblk)中所存储的内容, 也是一个指针, 称作ptr_a, ptr_a指向物理地址addr_a, 也就是说, ptr_a中存储的是物理地址addr_a;
那么, pblk本质上就是指向指针的指针.
但是, 由于pblk的类型是 void *pblk, 所以如果简单的使用 *pblk, 得到的将是addr_pblk中的具体内容, 也就是物理地址addr_a的数值表示, 而非指针!
因此先强制类型转换 (void **)pblk, 再取值, *(void **)pblk, 就可以得到指向addr_a的指针了.


- EOF -

2009年4月11日

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

任务

有两种典型的嵌入式开发, 一种是所谓「前/后台系统」开发, foreground / background system development, 一种是基于某个操作系统做开发. 对于后者而言, 开发的主要工作就是编写快速而有效的用户空间任务, user space task.
因此, 研究操作系统对任务的标识和管理, Task Identification and Management, 非常重要. 这里针对 μC/OS-II 2.52 版本展开说明.
---------------------------------
如何创建任务:
有很多 API 函数用于操作任务, 包括创建任务 OSTaskCreate() OSTaskCreateExt(), 删除任务 OSTaskDel() OSTaskDelReq(), 任务堆栈检测 OSTaskStkChk(), 挂起和恢复任务 OSTaskSuspend() OSTaskResume(), 改变任务优先级 OSTaskChangePrio(), 等等.
这里仅仅是粗略介绍 OSTaskCreate() OSTaskCreateExt(), 详细的介绍将在后续文章「任务管理接口」中详细介绍.
#if OS_TASK_CREATE_EN > 0
INT8U  OSTaskCreate (void (*task)(void *pd), void *pdata, OS_STK *ptos, INT8U prio);
#endif

#if OS_TASK_CREATE_EXT_EN > 0
INT8U  OSTaskCreateExt (void   (*task)(void *pd),
void    *pdata,
OS_STK  *ptos,
INT8U    prio,
INT16U   id,
OS_STK  *pbos,
INT32U   stk_size,
void    *pext,
INT16U   opt);
#endif

首先要说明, 类似 INT32U 的类型, 就是表示32位的无符号整型, 譬如 #typedef unsigned long int INT32U, 依次类推 INT8U INT16U.
其次, 关于指针类型, 都是以小写字母 p 开头, 譬如 *ptos *pbos 等等.

ok, 下面介绍几个要点:
#1
宏标记 OS_TASK_CREATE_EN OS_TASK_CREATE_EXT_EN, 用意在于「可裁减性」, 也就是所谓的 scalable. 譬如, 如果用户需求很简单, 没必要使用扩展任务创建模式, 就 #define OS_TASK_CREATE_EXT_EN 0, 这样可以缩减所需的程序空间大小.
#2
任务创建函数 OSTaskCreate() 用于简单的创建一个任务, 需要指明任务入口 void (*task)(void *pdata), 被处理数据的指针 void *pdata, 任务优先级 INT8U prio 以及任务堆栈栈顶指针 OS_STK *ptos.
扩展型任务创建函数 OSTaskCreateExt() 用于创建一个能够更加灵活操控的任务, 除了需要指明上述的基本参数外, 还有 ---
INT16U id, 任务的额外标识, 当前, 这个参数无用. 目前, 任务的 prio 就是其标识, id 只是留作后续版本扩展用,
OS_STK *pbos, 堆栈栈底. 注意, 这里的栈底并非活跃堆栈的底部, 而是堆栈容量的底部, 也就是堆栈生长的极限位置.
INT32U stk_size,堆栈大小. 并非字节容量, 而是指针容纳量. 譬如32位的机器, 当 stk_size = 100U 时, 实际堆栈容量应该是400 Bytes,
void *pext, 指向某个自定义扩展数据块, 也就是除了 pdata 之外的另外一个能够被操作的数据块.
INT16U opt, 设定选项, 如 OS_TASK_OPT_STK_CHK OS_TASK_OPT_STK_CLR OS_TASK_OPT_SAVE_FP, 分别代表允许堆栈检测, 堆栈清0, 需要进行浮点操作; 用「位或」运算符来操作之, 譬如, OS_TASK_OPT_STK_CHK | OS_TASK_OPT_STK_CLR.
#3
关于任务堆栈.
由于处理器架构的区别, 有 high->low & low->high 两种堆栈生长方向. 譬如51系列单片机, 都是从低向高生长, 而对于 8086/8088 以及当下的 IA32 架构, 则是从高向低生长.
如果不太理解, 我在这里画了一个堆栈从高向低生长的示意图:
压入堆栈前:
high address
(0x2665) = 某地址a    --- top_of_stack
.....
...      = NULL
...      = NULL
...      = NULL
.....
(0x2600) = NULL
low address

压入堆栈后:
high address
(0x2665) = 某地址a
(0x2664) = (EFLAGS)
(0x2663) = (CS)
(0x2662) = (EIP)    --- top_of_stack
...      = NULL
...      = NULL
.....
(0x2600) = NULL
low address

μC/OS-II 的任务堆栈是彼此独立的, 各个任务的堆栈大小并非完全相同, 而是可以对每个任务指定堆栈大小. 这对于节约系统资源很有帮助.
在调试时, 可以通过 OSTaskStkChk() 来检测堆栈使用情况, 从而为每个任务划分恰当的堆栈空间大小. 此函数并非系统默认启动, 其详细信息, 后续文章会说明.
#4
大家肯定注意到了, 在 OSTaskCreateExt() 中, 既有 stk_size 堆栈大小, 又有 ptospbos 指针. 很明显, 三个变量中, 仅仅需要两个就ok了, 第三个可以通过计算得出.
那为何要传递三个变量呢?
这就是典型的以空间换取时间 --- 占用的 RAM 空间虽然增加了, 但是在某些应用上, 譬如计算堆栈使用量等等, 运行时间得到了压缩. 对于实时操作系统, 这是划算的.
#5
任务优先级. 数字越小, 优先级越高.
范围是 0~OS_LOWEST_PRIO, OS_LOWEST_PRIO 是自定义的常量.
最多可以定义的任务优先级数量是64级, 也就是0~63. 但是, 最低级永远都是给空闲任务 OSTaskIdle() 使用的. 而且由于将来扩展的需要, 最好 0~3 & (OS_LOWEST_PRIO - 3)~(OS_LOWEST_PRIO) 不要使用.
---------------------------------
用户任务的大致形式
永远不会返回的无限循环. 函数的返回值应该被设置成 void 类型, 而任务的参数应该是指向被处理的数据的指针, void *pointer_to_data_be_handled. 譬如,
void task_name(void *pdata)
{
/* 用户代码 */
while(1) {
/* 用户代码 */
}
}

---------------------------------
任务状态
主要是5种状态.
dormant 休眠, 也就是任务驻留在程序空间, 没有交付给系统进行调度.
ready 就绪, 也就是随时可以被调度程序调度.
running 运行, 整个系统中永远只能有一个任务处于运行状态.
waiting 等待, 譬如任务正在等待某个信号量被释放 OSSemPend(), 抑或是将自身延时某段时间 OSTimeDly(clock_ticks_numbers) 供系统做调度等等.
interrupt 被中断, 顾名思义也就是被中断啦.
参考示意图如下, 从书上抓图抓下来的. 上面附带了详细的状态转移函数调用.
点击查看大图.

---------------------------------
任务控制块
首先指出:
这是系统里最重要的一个自定义结构体. 定义在 ucos_ii.h 文件中.
typedef struct os_tcb {
OS_STK          *OSTCBStkPtr;      /* Pointer to current top of stack                              */

#if OS_TASK_CREATE_EXT_EN > 0
void            *OSTCBExtPtr;      /* Pointer to user definable data for TCB extension             */
OS_STK          *OSTCBStkBottom;   /* Pointer to bottom of stack                                   */

INT32U           OSTCBStkSize;     /* Size of task stack (in number of stack elements)             */
INT16U           OSTCBOpt;         /* Task options as passed by OSTaskCreateExt()                  */
INT16U           OSTCBId;          /* Task ID (0..65535)                                           */
#endif

struct os_tcb   *OSTCBNext;        /* Pointer to next     TCB in the TCB list                      */
struct os_tcb   *OSTCBPrev;        /* Pointer to previous TCB in the TCB list                      */

#if OS_EVENT_EN > 0
OS_EVENT        *OSTCBEventPtr;    /* Pointer to event control block                               */
#endif

#if ((OS_Q_EN > 0) && (OS_MAX_QS > 0)) || (OS_MBOX_EN > 0)
void            *OSTCBMsg;         /* Message received from OSMboxPost() or OSQPost()              */
#endif

#if (OS_VERSION >= 251) && (OS_FLAG_EN > 0) && (OS_MAX_FLAGS > 0)
#if OS_TASK_DEL_EN > 0
OS_FLAG_NODE    *OSTCBFlagNode;    /* Pointer to event flag node                                   */
#endif
OS_FLAGS         OSTCBFlagsRdy;    /* Event flags that made task ready to run                      */
#endif

INT16U           OSTCBDly;         /* Nbr ticks to delay task or, timeout waiting for event        */
INT8U            OSTCBStat;        /* Task status                                                  */

INT8U            OSTCBPrio;        /* Task priority (0 == highest, 63 == lowest)                   */

INT8U            OSTCBX;           /* Bit position in group  corresponding to task priority (0..7) */
INT8U            OSTCBY;           /* Index into ready table corresponding to task priority        */

INT8U            OSTCBBitX;        /* Bit mask to access bit position in ready table               */
INT8U            OSTCBBitY;        /* Bit mask to access bit position in ready group               */

#if OS_TASK_DEL_EN > 0
BOOLEAN          OSTCBDelReq;      /* Indicates whether a task needs to delete itself              */
#endif
} OS_TCB;

东西太多, 懒得总结了. 囧
大致就是根据 OS_CFG.h 中的宏定义, 裁减性的定义任务控制块 OS_TCB 结构体.
需要指出的是, 这里的 struct os_tcb *OSTCBNext; struct os_tcb *OSTCBPrev; 主要是用于构建空任务控制块单向链表 OSTCBTbl[], 以及系统当前任务的任务控制块list OS_TCB *OSTCBList; 指针所指向的双向链表.
前者是依靠 OS_TCB *OSTCBFreeList; 指向的, 每当一个任务被创建, 就将当前 OSTCBFreeList 指向的空任务控制块交付给此任务, 并将此控制块初始化, 而 OSTCBFreeList 指针也在单向链表中向后移动一位; 同理, 每当一个任务被删除, 也就是成为休眠状态后, 这个任务的控制块被清空并归还到单向链表中, OSTCBFreeList 也向前移动一位.
后者系统当前任务的任务控制块list双向链表结构, 也是很重要的一个结构. 时钟节拍函数 OSTimeTick() 就是用这个链表来刷新各个任务的 OSTCBDly 值. 这个双向链表的表头指针是 OSTCBList, 永远指向最新创建的某个任务. 如果一个任务A被创建, 那么就是插入到 OSTCBList 的前面, 而 OSTCBList 也指向这个最新创建的任务A.
ok, 或许你已经考虑到了. 由于系统优先级最多只有 OS_LOWST_PRIO + 1 个, 而任务的表示也仅仅只能够由 prio 来区分, 那么 OSTCBFreeList 所指向的空任务控制块单向链表中的元素, 最多也只能够有 OS_LOWST_PRIO + 1 个.

你看明白了么? 如果没有, 还是下载文档来看吧.
去 gigapedia 搜索一下我前面提到的那本书 MicroC/OS II: The Real Time Kernel, 就可以找到了. 毕竟人家花了N长时间写作, 所以更加全面. 我这里的介绍显得有些太快而且太简略了点.
不过呢, 最好还是下载源代码阅读. 在官方站点上就有最新版本的下载. google 可以找到以往的老版本, 譬如和文档配套的2.52版本.
---------------------------------
初始化任务控制块
再次指出: 这也是内核源码中很重要的一部分.
前面已经介绍了创建任务, 也介绍了操作系统对于任务的控制模块 OS_TCB. 也提到了在创建一个任务的时候, 对空任务控制块单向链表系统当前任务的任务控制块list的影响.
这些影响, 都体现在任务创建函数, OSTaskCreate()OSTaskCreateExt() 中. 而这两个函数, 其核心部分都是一个函数, OS_TCBInit().

这里跑题指出一点, 如果各位在内核源码中碰到了 OS_xxx() 形式的函数, 那么肯定是系统内部函数, 也就是不需要各位显式调用, 系统的内部封装函数罢了.

这段函数的源代码在 os_core.c 文件中, 基本的内容前面都指出来了, 不再赘述. 各位直接看源码吧.
---------------------------------
总结
本篇文章内容很多, 这里再次梳理一遍:
#1. 如何创建任务? OSTaskCreate() OSTaskCreateExt(), 两者的参数列表参见上文中的第一个section.
#2. 用户任务的大致形式? 一个无限循环, 永远不会返回.
#3. 任务有哪些状态? 一共5种. 休眠, 就绪, 运行, 等待, 被中断.
#4. 任务怎么被系统识别和处理? 通过任务控制块. 参见上述对于 OS_TCB 结构体的描述和源代码. 同时务必明确两个系统级别的链表, 空任务控制块单向链表, 以及系统当前任务的任务控制块list双向链表. 前者由 OSTCBFreeList 指针指向当前空任务控制块, 随时等待着被付给某个新创建的任务. 后者由 OSTCBList 指针指向最新创建的某个任务的任务控制块.
#4. 新任务被创建时, 操作系统如何初始化任务控制块? 主要做哪些工作? 参考上面的源代码, 以及上面的#3「任务怎么被系统识别和处理」.
---------------------------------
ok, 终于写完了.
我真是精神可嘉啊. 囧. 前不久肌肉训练过了头, 加之生活习惯不好, 经常熬夜, 导致颈椎部位的肌肉组织有点拉伤, 今天下午去武汉体育学院医院做了推拿和拔火罐, 舒服多了.
而且现在我的Ubuntu上设置了keyboard typing break, 每隔30分钟就强制关闭键盘响应2分钟, 活动颈椎肌肉, 做一做颈椎保健操. 很舒服.

下篇预告:
操作系统初始化, OSInit(),
操作系统启动, OSStart(),
下下篇预告:
系统任务就绪表 OSRdyGrp, OSRdyTbl[],
操作系统的任务调度器, OS_Sched(void),
如何给调度器加锁, OSSchedLock(void), 和解锁, OSSchedUnlock(void),
任务级别的任务切换, OS_TASK_SW(), 一般使用汇编完成, 这里仅仅是写出pseudo code就ok了.

- EOF -

2009年4月7日

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

临界段代码处理.

所谓临界段代码, Critical Sections of Code, 就是这段代码在执行时, 不希望也不允许有中断发生.
所有使用了系统变量, 也就是全局变量, 的代码段, 基本上都是属于这一类, 除非使用信号量的方式对全局变量加以了保护.
---------------------------------
处理方法:
封装了两个宏, OS_ENTER_CRITICAL(), OS_EXIT_CRITICAL(), 分别用于进入临界段, 退出临界段.
封装方式:
OS_CRITICAL_METHOD 宏定义, 用来指示采用何种封装方式.
#1
直接定义为关中断, 开中断.
/* pseudo code*/
#if OS_CRITICAL_METHOD == 1
#define OS_ENTER_CRITICAL() __asm_tag__ (CLI;)
#define OS_EXIT_CRITICAL() __asm_tag__ (STI;)
#endif

上面是伪代码, 使用的两条汇编指令 CLI STI 是 IA-32 以及 IA-64 架构中的关中断 Clear Interrupt, 开中断 Set Interrupt 指令. 具体可以参考 Intel 64 and IA-32 Architectures -- Software Developer's Manual 的第一卷 5.1.11.
pros:
# 简便, 迅速. 对于处理器速度很慢的系统尤其适用.
# 有些系统仅仅能够使用此类方法.
cons:
如果系统本身就是关中断状态, 那么使用 OS_ENTER_CRITICAL() OS_EXIT_CRITICAL() 组合后, 系统中断将打开. 这时便有可能产生严重错误.
#2
先保存当前状态, 再进入临界段代码, 退出时恢复.
保存方式可以使用堆栈, 如,
/* pseudo code*/
#if OS_CRITICAL_METHOD == 2
#define OS_ENTER_CRITICAL() __asm_tag__ (PUSHF; CLI;)
#define OS_EXIT_CRITICAL() __asm_tag__ (POPF;)
#endif

也可以根据编译器提供的方式, 如,
/* pseudo code*/
#if OS_CRITICAL_METHOD == 3
#define OS_ENTER_CRITICAL() \
cpu_sr = get_processor_psw(); \
disable_interrupt();
#define OS_EXIT_CRITICAL() \
set_processor_psw(cpu_sr);
#endif

其中 cpu_sr 是和系统的程序状态字寄存器, Program Status Register, 大小一致的变量.
如8051中的 PSW 是8位, 就应该定义, typedef unsigned char OS_CPU_SR; OS_CPU_SR cpu_sr;. 如 IA-32 架构的 EFLAGS 是32位, 则应该是 typedef unsigned long int OS_CPU_SR;.
pros:
没有第一种实现方式的错误.
cons:
占用的时钟周期自然就增加了, 对于系统的速度有所拖累.
---------------------------------
总结:
这里所提到的两个宏定义, 都是在 OS_CPU.h 文件中, 用于移植所需.
至于移植时, 采取何种办法, 根据各类架构应用而定.
如果对于「是否屏蔽了中断」不敏感, 那么使用第一种方式足矣. 如果敏感, 务必使用第二种, 否则系统定然崩溃或者挂起.
而在第二种中, 推荐使用编译器提供的方式, 而非内联汇编的堆栈操作. 因为有时候编译器对内联汇编支持不好, 可能导致对堆栈指针 SP 的处理出错.
---------------------------------
** 注意:
一旦进入临界段代码, 所有中断便随之关闭, 包括定时时钟中断 Clock Tick Interrupt. 如果在此时调用类似 OSTimeDly() 的延时功能函数, 便会导致系统挂起, 也就是死机, 因为时钟节拍中断无法得到服务. 谨慎起见, 调用任何功能函数前, 都要保证中断是开着的.
而且, 如果临界段代码太长, 占用时钟周期太多, 会导致系统对中断的响应变慢, 中断延时增加, 这对于商业实时系统是不可忍受的. 因此务必良好规划和设计, 将临界段代码压缩至最小, 切记切记.

- EOF -

2009年4月6日

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

状态很糟糕, 没法集中精神, 一天一天就这么过去, 非常不爽.
前几天颈椎疼, 转动起来居然还发出咯吱咯吱的声音. 休息了两天, 每天早睡, 现在缓和了很多.
明天开始, 一个星期时间, 研究 μC/OS-II 的内核结构, 并在此做详细的记录. 主要就是参考 μC/OS-II 的圣经, MicroC/OS II: The Real Time Kernel.
---------------------------------
Todo list
RTOS 基本概念, Basic Concepts,
临界段代码处理, Handling of Critical Sections,
任务标识, 任务状态, 任务控制, Task Identification, Task States, Task Control,
任务就绪表, 任务调度, Ready List of Tasks, Scheduling of Tasks,
锁定 & 解锁调度器, Locking & Unlocking Scheduler,
空闲任务 & 统计任务, Idle Task & Statistics Task,
中断服务程序 ISR, Interrupt Service Routine,
信号量, 互斥信号量, Semaphore, Mutex,
消息邮箱, 消息队列, Message Box, Message Queue,
内存管理, Memory Management,
时钟节拍, Clock Tick,
系统初始化 & 启动, Initialization & Starting of μC/OS-II.


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