有两种典型的嵌入式开发, 一种是所谓「前/后台系统」开发, 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
堆栈大小, 又有 ptos
和 pbos
指针. 很明显, 三个变量中, 仅仅需要两个就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 -
没有评论:
发表评论
不要使用过激的暴力或者色情词汇.
不要充当勇猛小飞侠 --- 飘过 飞过 扑扑翅膀飞走 被雷得外焦里嫩地飞走.
万万不可充当小乌龟 --- 爬过.
构建河蟹社会 责任你有 我有 大家有 -_-