学习依据:Jean J. Labrosse《嵌入式实时操作系统 μC/OS-II(第2版)》第4章"任务管理"。本文沿用书中的 API 名称与经典 μC/OS-II 内核机制。代码示例说明原理;与 CPU 有关的栈地址、数据类型和宏配置,应以实际移植版本为准。
目录
- 一、任务管理究竟在管理什么
- 二、建立任务:
OSTaskCreate() - 三、扩展建立任务:
OSTaskCreateExt() - 四、任务堆栈:任务为什么能够中断后继续运行
- 五、堆栈检验:
OSTaskStkChk() - 六、删除任务:
OSTaskDel() - 七、请求删除任务:
OSTaskDelReq() - 八、改变任务优先级:
OSTaskChangePrio() - 九、挂起任务:
OSTaskSuspend() - 十、恢复任务:
OSTaskResume() - 十一、查询任务信息:
OSTaskQuery() - 十二、任务管理与内核调度的整体关系
- 十三、结合电机控制系统分析任务管理
- 十四、典型问答
- 十五、总结与参考资料
一、任务管理究竟在管理什么
1. 先区分任务、任务函数和任务控制块
任务(Task) 是由内核独立调度的基本执行单位;任务函数(Task Function) 是任务要运行的程序代码;任务控制块(Task Control Block,TCB) 是内核记录任务运行信息的数据结构。在 μC/OS-II 中,TCB 的类型是 OS_TCB。
任务并不是单独的一个 C 函数。内核还必须知道这个任务的优先级、当前状态、任务栈位置以及是否正在等待某个事件。
#mermaid-svg-5kPXrzcqyiVrAUDI{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5kPXrzcqyiVrAUDI .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5kPXrzcqyiVrAUDI .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5kPXrzcqyiVrAUDI .error-icon{fill:#552222;}#mermaid-svg-5kPXrzcqyiVrAUDI .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5kPXrzcqyiVrAUDI .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5kPXrzcqyiVrAUDI .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5kPXrzcqyiVrAUDI .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5kPXrzcqyiVrAUDI .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5kPXrzcqyiVrAUDI .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5kPXrzcqyiVrAUDI .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5kPXrzcqyiVrAUDI .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5kPXrzcqyiVrAUDI .marker.cross{stroke:#333333;}#mermaid-svg-5kPXrzcqyiVrAUDI svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5kPXrzcqyiVrAUDI p{margin:0;}#mermaid-svg-5kPXrzcqyiVrAUDI .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-5kPXrzcqyiVrAUDI .cluster-label text{fill:#333;}#mermaid-svg-5kPXrzcqyiVrAUDI .cluster-label span{color:#333;}#mermaid-svg-5kPXrzcqyiVrAUDI .cluster-label span p{background-color:transparent;}#mermaid-svg-5kPXrzcqyiVrAUDI .label text,#mermaid-svg-5kPXrzcqyiVrAUDI span{fill:#333;color:#333;}#mermaid-svg-5kPXrzcqyiVrAUDI .node rect,#mermaid-svg-5kPXrzcqyiVrAUDI .node circle,#mermaid-svg-5kPXrzcqyiVrAUDI .node ellipse,#mermaid-svg-5kPXrzcqyiVrAUDI .node polygon,#mermaid-svg-5kPXrzcqyiVrAUDI .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5kPXrzcqyiVrAUDI .rough-node .label text,#mermaid-svg-5kPXrzcqyiVrAUDI .node .label text,#mermaid-svg-5kPXrzcqyiVrAUDI .image-shape .label,#mermaid-svg-5kPXrzcqyiVrAUDI .icon-shape .label{text-anchor:middle;}#mermaid-svg-5kPXrzcqyiVrAUDI .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-5kPXrzcqyiVrAUDI .rough-node .label,#mermaid-svg-5kPXrzcqyiVrAUDI .node .label,#mermaid-svg-5kPXrzcqyiVrAUDI .image-shape .label,#mermaid-svg-5kPXrzcqyiVrAUDI .icon-shape .label{text-align:center;}#mermaid-svg-5kPXrzcqyiVrAUDI .node.clickable{cursor:pointer;}#mermaid-svg-5kPXrzcqyiVrAUDI .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-5kPXrzcqyiVrAUDI .arrowheadPath{fill:#333333;}#mermaid-svg-5kPXrzcqyiVrAUDI .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-5kPXrzcqyiVrAUDI .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-5kPXrzcqyiVrAUDI .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5kPXrzcqyiVrAUDI .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-5kPXrzcqyiVrAUDI .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5kPXrzcqyiVrAUDI .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-5kPXrzcqyiVrAUDI .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-5kPXrzcqyiVrAUDI .cluster text{fill:#333;}#mermaid-svg-5kPXrzcqyiVrAUDI .cluster span{color:#333;}#mermaid-svg-5kPXrzcqyiVrAUDI div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-5kPXrzcqyiVrAUDI .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-5kPXrzcqyiVrAUDI rect.text{fill:none;stroke-width:0;}#mermaid-svg-5kPXrzcqyiVrAUDI .icon-shape,#mermaid-svg-5kPXrzcqyiVrAUDI .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5kPXrzcqyiVrAUDI .icon-shape p,#mermaid-svg-5kPXrzcqyiVrAUDI .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-5kPXrzcqyiVrAUDI .icon-shape rect,#mermaid-svg-5kPXrzcqyiVrAUDI .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5kPXrzcqyiVrAUDI .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-5kPXrzcqyiVrAUDI .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-5kPXrzcqyiVrAUDI :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-5kPXrzcqyiVrAUDI .default>*{fill:#ffffff!important;stroke:#000000!important;color:#000000!important;stroke-width:1px!important;}#mermaid-svg-5kPXrzcqyiVrAUDI .default span{fill:#ffffff!important;stroke:#000000!important;color:#000000!important;stroke-width:1px!important;}#mermaid-svg-5kPXrzcqyiVrAUDI .default tspan{fill:#000000!important;} 任务函数:执行具体业务
任务控制块 OS_TCB
独立任务栈:保存执行现场
优先级、状态、延时等管理信息
就绪表:记录可运行的任务
调度器:选择最高优先级就绪任务
任务函数、任务栈与任务控制块的关系
注意:图中的箭头表示管理关联,不代表函数调用方向。TCB 保存栈指针等信息,而不是把整个任务栈复制进 TCB。
2. 一个任务必须提供怎样的入口函数
教材给出的典型形式是:
c
void MotorTask(void *pdata)
{
(void)pdata;
for (;;) {
/* 执行用户程序 */
OSTimeDly(1);
}
}
void *pdata 是通用指针参数 ,即用一个地址把任务运行所需的参数传入函数;for (;;) 是无限循环,与 while (1) 等价。OSTimeDly(1) 表示任务请求等待一个操作系统时钟节拍(Tick),进入延时等待状态。
教材强调:μC/OS-II 的普通任务函数不应像一般子函数那样直接返回。 如果确实需要结束任务,应通过任务删除接口退出任务管理。无限循环也并不意味着一直占有 CPU;任务执行延时、等待信号量等操作后,可以让出 CPU。
3. 第4章各项操作分别改变了什么
| 操作 | 影响对象 | 本质变化 |
|---|---|---|
| 创建任务 | 栈、TCB、就绪表 | 为新任务建立可调度的运行环境 |
| 删除任务 | TCB、就绪表及等待队列 | 从内核的任务管理结构中移除任务 |
| 修改优先级 | TCB、优先级映射和有关位图 | 改变任务参与调度的顺序 |
| 挂起任务 | TCB 状态、就绪表 | 暂时禁止任务获得 CPU |
| 恢复任务 | TCB 状态、就绪表 | 撤销挂起限制,但未必立即运行 |
| 查询任务 | TCB 信息 | 读取任务的管理状态,不改变运行逻辑 |
就绪表(Ready Table) 是记录各优先级是否已经满足运行条件的位图结构;调度(Scheduling) 是决定接下来让哪个就绪任务使用 CPU 的过程。
经典 μC/OS-II 是固定优先级抢占式内核 :每个任务具有不同的优先级编号,且编号越小,优先级越高 。并非所有优先级都能交给用户任务:空闲任务、可选的统计任务以及特定版本预留的优先级需要避开,可用范围以 OS_CFG.H 等配置为准。
二、建立任务:OSTaskCreate()
1. 函数原型与四个参数
教材4.00节给出的基本接口为:
c
INT8U OSTaskCreate(
void (*task)(void *pd),
void *pdata,
OS_STK *ptos,
INT8U prio
);
| 参数 | 术语解释 | 具体作用 |
|---|---|---|
task |
函数指针(保存函数入口地址的指针) | 指向新任务执行的 C 函数 |
pdata |
参数指针 | 将应用数据传给任务函数 |
ptos |
Top of Stack,初始栈顶指针 | 告诉内核从哪里初始化新任务的栈 |
prio |
Priority,任务优先级 | 决定新任务参与调度的先后顺序 |
返回值类型 INT8U 是 μC/OS-II 定义的无符号8位整数类型,用来返回成功或失败状态。例如 OS_NO_ERR 表示操作成功,OS_PRIO_EXIST 表示要使用的优先级已经被占用。
2. 任务创建的五个关键步骤
查看教材的 OSTaskCreate() 程序清单,主要过程可以归纳为:
- 检查优先级是否有效:如果启用了参数检查,拒绝非法优先级。
- 检查优先级是否被占用 :通过
OSTCBPrioTbl[prio]判断该编号能否分配。 - 保留优先级位置:先写入特定的保留标记,避免其他创建操作同时占用。
- 初始化任务栈和 TCB :调用
OSTaskStkInit()构造任务初始 CPU 现场,再用OS_TCBInit()初始化任务控制块。 - 必要时执行调度:创建成功后任务进入就绪态;如果内核已启动,则可能立即切换到新任务。
#mermaid-svg-GMHoWpvYz7iAru0m{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-GMHoWpvYz7iAru0m .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GMHoWpvYz7iAru0m .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GMHoWpvYz7iAru0m .error-icon{fill:#552222;}#mermaid-svg-GMHoWpvYz7iAru0m .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GMHoWpvYz7iAru0m .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GMHoWpvYz7iAru0m .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GMHoWpvYz7iAru0m .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GMHoWpvYz7iAru0m .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GMHoWpvYz7iAru0m .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GMHoWpvYz7iAru0m .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GMHoWpvYz7iAru0m .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GMHoWpvYz7iAru0m .marker.cross{stroke:#333333;}#mermaid-svg-GMHoWpvYz7iAru0m svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GMHoWpvYz7iAru0m p{margin:0;}#mermaid-svg-GMHoWpvYz7iAru0m .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-GMHoWpvYz7iAru0m .cluster-label text{fill:#333;}#mermaid-svg-GMHoWpvYz7iAru0m .cluster-label span{color:#333;}#mermaid-svg-GMHoWpvYz7iAru0m .cluster-label span p{background-color:transparent;}#mermaid-svg-GMHoWpvYz7iAru0m .label text,#mermaid-svg-GMHoWpvYz7iAru0m span{fill:#333;color:#333;}#mermaid-svg-GMHoWpvYz7iAru0m .node rect,#mermaid-svg-GMHoWpvYz7iAru0m .node circle,#mermaid-svg-GMHoWpvYz7iAru0m .node ellipse,#mermaid-svg-GMHoWpvYz7iAru0m .node polygon,#mermaid-svg-GMHoWpvYz7iAru0m .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GMHoWpvYz7iAru0m .rough-node .label text,#mermaid-svg-GMHoWpvYz7iAru0m .node .label text,#mermaid-svg-GMHoWpvYz7iAru0m .image-shape .label,#mermaid-svg-GMHoWpvYz7iAru0m .icon-shape .label{text-anchor:middle;}#mermaid-svg-GMHoWpvYz7iAru0m .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-GMHoWpvYz7iAru0m .rough-node .label,#mermaid-svg-GMHoWpvYz7iAru0m .node .label,#mermaid-svg-GMHoWpvYz7iAru0m .image-shape .label,#mermaid-svg-GMHoWpvYz7iAru0m .icon-shape .label{text-align:center;}#mermaid-svg-GMHoWpvYz7iAru0m .node.clickable{cursor:pointer;}#mermaid-svg-GMHoWpvYz7iAru0m .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-GMHoWpvYz7iAru0m .arrowheadPath{fill:#333333;}#mermaid-svg-GMHoWpvYz7iAru0m .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-GMHoWpvYz7iAru0m .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-GMHoWpvYz7iAru0m .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GMHoWpvYz7iAru0m .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-GMHoWpvYz7iAru0m .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GMHoWpvYz7iAru0m .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-GMHoWpvYz7iAru0m .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-GMHoWpvYz7iAru0m .cluster text{fill:#333;}#mermaid-svg-GMHoWpvYz7iAru0m .cluster span{color:#333;}#mermaid-svg-GMHoWpvYz7iAru0m div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-GMHoWpvYz7iAru0m .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-GMHoWpvYz7iAru0m rect.text{fill:none;stroke-width:0;}#mermaid-svg-GMHoWpvYz7iAru0m .icon-shape,#mermaid-svg-GMHoWpvYz7iAru0m .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GMHoWpvYz7iAru0m .icon-shape p,#mermaid-svg-GMHoWpvYz7iAru0m .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-GMHoWpvYz7iAru0m .icon-shape rect,#mermaid-svg-GMHoWpvYz7iAru0m .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GMHoWpvYz7iAru0m .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-GMHoWpvYz7iAru0m .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-GMHoWpvYz7iAru0m :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-GMHoWpvYz7iAru0m .default>*{fill:#ffffff!important;stroke:#000000!important;color:#000000!important;stroke-width:1px!important;}#mermaid-svg-GMHoWpvYz7iAru0m .default span{fill:#ffffff!important;stroke:#000000!important;color:#000000!important;stroke-width:1px!important;}#mermaid-svg-GMHoWpvYz7iAru0m .default tspan{fill:#000000!important;} 否
是
否
是
否
是
调用 OSTaskCreate
优先级有效且未占用?
返回错误码
预留优先级位置
OSTaskStkInit:初始化任务栈
OS_TCBInit 成功?
释放预留位置并返回错误
新任务进入就绪态
内核已经启动?
返回;等待 OSStart
执行调度判断
创建任务时内核执行的主要步骤
OSTCBPrioTbl[] 是优先级到 TCB 的映射表 ,可以通过优先级快速找到对应任务控制块。OSTaskStkInit() 属于处理器移植相关函数,因为不同 CPU 保存的寄存器和栈结构不同。
3. 为什么任务栈初始化后,新任务就能开始运行
任务首次运行时,CPU 需要获得任务入口地址和适当的初始寄存器值。OSTaskStkInit() 通过在任务栈上预先布置一个符合 CPU 切换规则的初始现场(Initial Context),使新任务第一次被调度时可以像"恢复一个旧任务"一样启动。
这里的上下文(Context) 是指 CPU 继续执行某个任务所必需的状态,例如程序计数器、状态寄存器及通用寄存器等。它不是 C 语言函数中的某个普通局部变量。
4. 创建成功以后,任务会立刻运行吗
不一定。创建成功只保证任务已经纳入内核管理并满足就绪条件。
- 在
OSStart()调用前创建任务:内核尚未开始多任务调度,任务需要等待操作系统启动。 - 在内核运行过程中创建任务:内核会检查调度条件;如果新任务的优先级高于当前运行任务,并且允许调度,新任务可能立即抢占 CPU。
- 如果新任务的优先级较低:它通常留在就绪态,等待更高优先级任务阻塞或让出 CPU。
这是理论考试中常见的区分:任务创建、任务就绪和任务运行是三个不同概念。
三、扩展建立任务:OSTaskCreateExt()
1. 为什么还需要扩展创建函数
OSTaskCreate() 只有四个参数,功能较简洁,但不能充分描述任务栈容量、栈检查及任务扩展信息。教材4.01节因此介绍 OSTaskCreateExt()。
c
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
);
与基本创建函数相比,它在前四个参数的基础上又增加了五项:
| 扩展参数 | 含义 | 为什么需要 |
|---|---|---|
id |
用户任务标识号(Identifier) | 为任务提供附加识别信息;书中版本未充分使用其扩展能力 |
pbos |
Bottom of Stack,栈底指针 | 便于确定栈边界并进行栈检查 |
stk_size |
栈容量,单位为 OS_STK 元素个数 |
供内核计算可用栈空间 |
pext |
用户扩展数据指针 | 关联任务额外的应用信息 |
opt |
Options,创建选项位 | 控制栈清零、栈检查等功能 |
位选项(Bit Options) 是用一个整数的不同二进制位分别控制不同功能。多个选项可以使用按位或 | 组合,例如:
c
OS_TASK_OPT_STK_CHK | OS_TASK_OPT_STK_CLR
OS_TASK_OPT_STK_CHK 表示允许该任务接受栈检查;OS_TASK_OPT_STK_CLR 表示创建任务时清零栈区域。进行书中介绍的栈空间估算时,通常需要同时设置这两个选项。教材还给出 OS_TASK_OPT_SAVE_FP,用于要求支持浮点运算现场保存的移植层处理浮点寄存器;是否生效取决于具体处理器与移植实现。
2. 一段对应教材的扩展创建示例
下面的示例假设移植层定义 OS_STK_GROWTH == 1,即任务栈从高地址向低地址增长。
c
#define MOTOR_TASK_PRIO 5u
#define MOTOR_TASK_STK_SIZE 256u
static OS_STK MotorTaskStk[MOTOR_TASK_STK_SIZE];
void MotorTask(void *pdata)
{
(void)pdata;
for (;;) {
/* 任务实际业务代码 */
OSTimeDly(1);
}
}
void CreateMotorTask(void)
{
INT8U err;
err = OSTaskCreateExt(
MotorTask, /* 任务入口函数 */
(void *)0, /* 任务入口参数 */
&MotorTaskStk[MOTOR_TASK_STK_SIZE - 1u], /* 初始栈顶 */
MOTOR_TASK_PRIO, /* 任务优先级 */
MOTOR_TASK_PRIO, /* 用户任务 ID */
&MotorTaskStk[0], /* 栈底地址 */
MOTOR_TASK_STK_SIZE, /* 栈元素个数 */
(void *)0, /* 用户扩展数据 */
OS_TASK_OPT_STK_CHK | OS_TASK_OPT_STK_CLR
);
if (err != OS_NO_ERR) {
/* 处理任务创建失败,例如记录错误码 */
}
}
以上是接口用法示例,不表示已在某一块 MCU 上编译运行。在具体工程中,头文件、内核配置、栈增长方向和创建时机都应与移植包一致。
3. 两种创建函数怎么选
基本创建函数适合只需提供入口函数、参数、栈顶和优先级的场景。扩展创建函数额外提供任务栈容量、栈边界、任务扩展数据和检查选项,便于分析任务的内存使用情况。
需要特别注意:两个创建函数并不会自动替任务分配任意大小的栈空间;用户必须在创建前提供合法的任务栈存储区域。
四、任务堆栈:任务为什么能够中断后继续运行
1. 什么是堆栈
栈(Stack) 是一块遵循"后进先出"(Last In, First Out,LIFO)访问规律的内存区域。函数调用时,返回地址、部分寄存器和局部数据可能被保存到栈中;函数退出时,再按照约定恢复相关信息。
任务被切走以后,为了将来能回到原来的执行位置,必须保留它的执行现场。因此在 μC/OS-II 中,每个任务都必须有自己的任务栈。
三个容易混淆的术语是:
- SP(Stack Pointer,栈指针):CPU 当前使用的栈位置;发生任务切换时需要妥善保存与恢复。
- 栈顶(Top of Stack):某个任务初始化或当前压栈时所使用的一端,具体地址由 CPU 栈增长规则决定。
- 栈底(Bottom of Stack):任务栈相对的另一端,通常用于确定边界和检查容量。
"栈顶"并不恒等于"内存的最高地址"。必须根据 OS_STK_GROWTH 的定义理解:取值为 1 表示书中所述的向低地址增长 ,取值为 0 表示向高地址增长。
2. 为什么向下增长时传入数组的最后一个元素
假设:
c
OS_STK TaskStk[256];
任务栈向低地址增长时,初始栈顶通常取:
c
&TaskStk[255]
栈底对应:
c
&TaskStk[0]
| 内存地址方向 | 数组位置 | 在向下增长的栈中的作用 |
|---|---|---|
| 高地址 | TaskStk[255] |
初始栈顶,任务现场从这一端开始布置 |
| 中间地址 | TaskStk[1]~TaskStk[254] |
可供运行期间使用的栈空间 |
| 低地址 | TaskStk[0] |
栈的另一端,作为栈边界参考 |
若处理器栈向高地址增长,则传参方式不同,不能照搬上述数组下标。教材通过这一差别说明 μC/OS-II 的可移植性(Portability):内核管理思想基本一致,但处理器相关细节由移植层实现。
3. 为什么实际工程不宜随便在运行中分配任务栈
教材4.02节列举了使用 malloc() 分配任务栈可能带来的内存碎片(Memory Fragmentation) 问题:多次申请和释放内存后,虽然空闲字节总数足够,但空闲区域被分割成多个小块,未必能找到一段连续空间满足新任务栈的要求。
对于资源受限、任务数量相对固定的控制器,通常更容易管理的方案是预先准备静态任务栈:
c
static OS_STK ControlTaskStk[256];
static OS_STK ComTaskStk[192];
静态分配不是绝对规则,但它有利于在系统启动前明确 RAM 占用和任务栈边界。
五、堆栈检验:OSTaskStkChk()
1. 栈大小为什么不能完全靠猜
栈空间太小,任务可能发生栈溢出(Stack Overflow),即函数调用或保存现场所需空间超过分配边界,可能破坏邻近变量甚至造成系统异常。栈空间太大,又会浪费嵌入式系统有限的 RAM。
教材4.03节介绍 OSTaskStkChk(),用于估算已用和未使用的任务栈空间。
c
INT8U OSTaskStkChk(INT8U prio, OS_STK_DATA *pdata);
OS_STK_DATA 是保存栈统计信息的数据结构,其中重要成员有:
| 成员 | 说明 |
|---|---|
OSFree |
估算的空闲栈空间,单位为字节 |
OSUsed |
估算的已使用栈空间,单位为字节 |
2. 检验到底如何工作
教材采用的思路是:创建任务前把栈初始化为0;任务运行时,一部分栈空间会被现场、调用数据等写入;检查函数再从特定栈边界扫描连续的0,推算尚未使用的容量。
#mermaid-svg-mV8WuDkusHnIOtlw{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-mV8WuDkusHnIOtlw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-mV8WuDkusHnIOtlw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-mV8WuDkusHnIOtlw .error-icon{fill:#552222;}#mermaid-svg-mV8WuDkusHnIOtlw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-mV8WuDkusHnIOtlw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-mV8WuDkusHnIOtlw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-mV8WuDkusHnIOtlw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-mV8WuDkusHnIOtlw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-mV8WuDkusHnIOtlw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-mV8WuDkusHnIOtlw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-mV8WuDkusHnIOtlw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-mV8WuDkusHnIOtlw .marker.cross{stroke:#333333;}#mermaid-svg-mV8WuDkusHnIOtlw svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-mV8WuDkusHnIOtlw p{margin:0;}#mermaid-svg-mV8WuDkusHnIOtlw .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-mV8WuDkusHnIOtlw .cluster-label text{fill:#333;}#mermaid-svg-mV8WuDkusHnIOtlw .cluster-label span{color:#333;}#mermaid-svg-mV8WuDkusHnIOtlw .cluster-label span p{background-color:transparent;}#mermaid-svg-mV8WuDkusHnIOtlw .label text,#mermaid-svg-mV8WuDkusHnIOtlw span{fill:#333;color:#333;}#mermaid-svg-mV8WuDkusHnIOtlw .node rect,#mermaid-svg-mV8WuDkusHnIOtlw .node circle,#mermaid-svg-mV8WuDkusHnIOtlw .node ellipse,#mermaid-svg-mV8WuDkusHnIOtlw .node polygon,#mermaid-svg-mV8WuDkusHnIOtlw .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-mV8WuDkusHnIOtlw .rough-node .label text,#mermaid-svg-mV8WuDkusHnIOtlw .node .label text,#mermaid-svg-mV8WuDkusHnIOtlw .image-shape .label,#mermaid-svg-mV8WuDkusHnIOtlw .icon-shape .label{text-anchor:middle;}#mermaid-svg-mV8WuDkusHnIOtlw .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-mV8WuDkusHnIOtlw .rough-node .label,#mermaid-svg-mV8WuDkusHnIOtlw .node .label,#mermaid-svg-mV8WuDkusHnIOtlw .image-shape .label,#mermaid-svg-mV8WuDkusHnIOtlw .icon-shape .label{text-align:center;}#mermaid-svg-mV8WuDkusHnIOtlw .node.clickable{cursor:pointer;}#mermaid-svg-mV8WuDkusHnIOtlw .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-mV8WuDkusHnIOtlw .arrowheadPath{fill:#333333;}#mermaid-svg-mV8WuDkusHnIOtlw .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-mV8WuDkusHnIOtlw .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-mV8WuDkusHnIOtlw .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mV8WuDkusHnIOtlw .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-mV8WuDkusHnIOtlw .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mV8WuDkusHnIOtlw .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-mV8WuDkusHnIOtlw .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-mV8WuDkusHnIOtlw .cluster text{fill:#333;}#mermaid-svg-mV8WuDkusHnIOtlw .cluster span{color:#333;}#mermaid-svg-mV8WuDkusHnIOtlw div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-mV8WuDkusHnIOtlw .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-mV8WuDkusHnIOtlw rect.text{fill:none;stroke-width:0;}#mermaid-svg-mV8WuDkusHnIOtlw .icon-shape,#mermaid-svg-mV8WuDkusHnIOtlw .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mV8WuDkusHnIOtlw .icon-shape p,#mermaid-svg-mV8WuDkusHnIOtlw .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-mV8WuDkusHnIOtlw .icon-shape rect,#mermaid-svg-mV8WuDkusHnIOtlw .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mV8WuDkusHnIOtlw .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-mV8WuDkusHnIOtlw .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-mV8WuDkusHnIOtlw :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-mV8WuDkusHnIOtlw .default>*{fill:#ffffff!important;stroke:#000000!important;color:#000000!important;stroke-width:1px!important;}#mermaid-svg-mV8WuDkusHnIOtlw .default span{fill:#ffffff!important;stroke:#000000!important;color:#000000!important;stroke-width:1px!important;}#mermaid-svg-mV8WuDkusHnIOtlw .default tspan{fill:#000000!important;} 任务创建:清零栈空间
任务运行:保存现场、调用函数
堆栈中留下使用痕迹
OSTaskStkChk 扫描连续零值
计算 OSFree 与 OSUsed
任务栈使用情况的估算过程
要使用书中的这一机制,需要在 OS_CFG.H 中开启扩展任务创建等必要功能,并在建立任务时设置 OS_TASK_OPT_STK_CHK | OS_TASK_OPT_STK_CLR;OS_TASK_OPT_STK_CHK 和 OS_TASK_OPT_STK_CLR 是创建选项位,不是 OS_CFG.H 中的功能开关。
3. 为什么栈检查不能证明任务绝对安全
因为它依据栈内存中的写入痕迹来估算使用量,并不是实时监控每一条压栈指令。函数调用层次、局部数组、中断嵌套(即一个中断处理过程被其他更高优先级中断打断)和某些异常路径,都可能影响实际栈需求;中断使用任务栈还是独立中断栈,还取决于具体移植方式。
因此,检查结果适合辅助估算栈余量,不能据此断言所有运行路径都不会发生溢出。理论上还应结合最深函数调用路径、最坏情况中断嵌套以及移植层的上下文保存开销分析。
六、删除任务:OSTaskDel()
1. 删除任务究竟删掉什么
教材4.04节给出了:
c
INT8U OSTaskDel(INT8U prio);
prio 是待删除任务的优先级;如果传入 OS_PRIO_SELF,表示要求当前任务删除自己。OS_PRIO_SELF 是内核规定的特殊参数值,而不是用户任务实际被分配的优先级。
删除任务并不是把 C 语言函数从 Flash 中抹掉,也不是自动释放所有用户申请的内存。 它的主要意义是把该任务从内核的调度管理体系中撤销。
从源码流程上看,删除操作涉及:清除就绪表中的对应位、将任务从有关事件等待结构中移除、处理任务 TCB 的链表连接、释放 TCB 供后续任务创建复用,并重新判断调度。
2. 为什么可以删除自己,却不能随意由 ISR 删除任务
ISR(Interrupt Service Routine,中断服务程序) 不是一个普通任务。任务删除会修改内核用于调度的数据结构,而且当前任务删除自身后应切换到其他就绪任务。书中的 OSTaskDel() 会检查中断嵌套状态,不允许在 ISR 中直接删除普通任务。
如果某个中断表示"当前功能不再需要",更合适的思路通常是让 ISR 通知某个任务,再由任务上下文执行必要的清理和删除。
3. 为什么删除其他任务存在资源风险
假设通信任务获得某个资源后被直接删除:
text
通信任务已占用共享缓冲区
↓
其他任务直接删除通信任务
↓
通信任务未执行释放资源代码
↓
其他任务可能一直等待该资源
互斥资源(Mutual-Exclusion Resource) 是同一时刻不允许多个任务同时使用的资源。强制删除资源持有者,可能使资源长期无法正常交接。
因此,在任务仍持有互斥量、外设或动态内存时,直接删除通常具有风险。内核回收 TCB,不代表自动释放任务使用的所有应用资源。
七、请求删除任务:OSTaskDelReq()
1. 与直接删除的根本区别
教材4.05节为资源安全退出提供了另一种协作方法:
c
INT8U OSTaskDelReq(INT8U prio);
该接口的核心思想是:其他任务可以提出删除请求,但由目标任务在合适的时刻清理资源,再自行删除。
调用方式有两类:
- 管理任务调用
OSTaskDelReq(目标优先级):向目标任务登记删除请求。 - 目标任务调用
OSTaskDelReq(OS_PRIO_SELF):检查是否已经有人请求删除自己。
当目标任务检查到 OS_TASK_DEL_REQ,表示内核中已有待处理的删除请求。
2. 协作删除的完整时序
#mermaid-svg-eGk2WlysVzOj2DsC{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-eGk2WlysVzOj2DsC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-eGk2WlysVzOj2DsC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-eGk2WlysVzOj2DsC .error-icon{fill:#552222;}#mermaid-svg-eGk2WlysVzOj2DsC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-eGk2WlysVzOj2DsC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-eGk2WlysVzOj2DsC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-eGk2WlysVzOj2DsC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-eGk2WlysVzOj2DsC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-eGk2WlysVzOj2DsC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-eGk2WlysVzOj2DsC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-eGk2WlysVzOj2DsC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-eGk2WlysVzOj2DsC .marker.cross{stroke:#333333;}#mermaid-svg-eGk2WlysVzOj2DsC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-eGk2WlysVzOj2DsC p{margin:0;}#mermaid-svg-eGk2WlysVzOj2DsC .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-eGk2WlysVzOj2DsC .cluster-label text{fill:#333;}#mermaid-svg-eGk2WlysVzOj2DsC .cluster-label span{color:#333;}#mermaid-svg-eGk2WlysVzOj2DsC .cluster-label span p{background-color:transparent;}#mermaid-svg-eGk2WlysVzOj2DsC .label text,#mermaid-svg-eGk2WlysVzOj2DsC span{fill:#333;color:#333;}#mermaid-svg-eGk2WlysVzOj2DsC .node rect,#mermaid-svg-eGk2WlysVzOj2DsC .node circle,#mermaid-svg-eGk2WlysVzOj2DsC .node ellipse,#mermaid-svg-eGk2WlysVzOj2DsC .node polygon,#mermaid-svg-eGk2WlysVzOj2DsC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-eGk2WlysVzOj2DsC .rough-node .label text,#mermaid-svg-eGk2WlysVzOj2DsC .node .label text,#mermaid-svg-eGk2WlysVzOj2DsC .image-shape .label,#mermaid-svg-eGk2WlysVzOj2DsC .icon-shape .label{text-anchor:middle;}#mermaid-svg-eGk2WlysVzOj2DsC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-eGk2WlysVzOj2DsC .rough-node .label,#mermaid-svg-eGk2WlysVzOj2DsC .node .label,#mermaid-svg-eGk2WlysVzOj2DsC .image-shape .label,#mermaid-svg-eGk2WlysVzOj2DsC .icon-shape .label{text-align:center;}#mermaid-svg-eGk2WlysVzOj2DsC .node.clickable{cursor:pointer;}#mermaid-svg-eGk2WlysVzOj2DsC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-eGk2WlysVzOj2DsC .arrowheadPath{fill:#333333;}#mermaid-svg-eGk2WlysVzOj2DsC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-eGk2WlysVzOj2DsC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-eGk2WlysVzOj2DsC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eGk2WlysVzOj2DsC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-eGk2WlysVzOj2DsC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eGk2WlysVzOj2DsC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-eGk2WlysVzOj2DsC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-eGk2WlysVzOj2DsC .cluster text{fill:#333;}#mermaid-svg-eGk2WlysVzOj2DsC .cluster span{color:#333;}#mermaid-svg-eGk2WlysVzOj2DsC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-eGk2WlysVzOj2DsC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-eGk2WlysVzOj2DsC rect.text{fill:none;stroke-width:0;}#mermaid-svg-eGk2WlysVzOj2DsC .icon-shape,#mermaid-svg-eGk2WlysVzOj2DsC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eGk2WlysVzOj2DsC .icon-shape p,#mermaid-svg-eGk2WlysVzOj2DsC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-eGk2WlysVzOj2DsC .icon-shape rect,#mermaid-svg-eGk2WlysVzOj2DsC .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eGk2WlysVzOj2DsC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-eGk2WlysVzOj2DsC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-eGk2WlysVzOj2DsC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-eGk2WlysVzOj2DsC .default>*{fill:#ffffff!important;stroke:#000000!important;color:#000000!important;stroke-width:1px!important;}#mermaid-svg-eGk2WlysVzOj2DsC .default span{fill:#ffffff!important;stroke:#000000!important;color:#000000!important;stroke-width:1px!important;}#mermaid-svg-eGk2WlysVzOj2DsC .default tspan{fill:#000000!important;} 否
是
管理任务:调用 OSTaskDelReq
内核:记录目标任务的删除请求
目标任务:检查本任务删除请求
检测到 OS_TASK_DEL_REQ?
继续正常业务,稍后再次检查
目标任务:释放占用的应用资源
目标任务:调用 OSTaskDel 删除自身
内核:移除该任务并重新调度
协作式任务删除过程
该过程体现了任务的协作式退出:删除请求由管理任务提出,而资源清理与最终自删除由目标任务完成。
3. 为什么请求删除适合需要善后的任务
例如:通信任务可能需要完成当前数据帧发送、归还共享缓冲区、释放互斥资源后才能退出;存储任务可能需要等待一次写操作完成。
但请求删除机制有一个重要前提:目标任务必须有机会运行并检查请求。 如果目标任务永远阻塞或不主动检查删除请求,仅仅发出请求并不能保证它立即退出。
八、改变任务优先级:OSTaskChangePrio()
1. 修改优先级的目的
教材4.06节介绍运行过程中动态改变任务优先级:
c
INT8U OSTaskChangePrio(INT8U oldprio, INT8U newprio);
oldprio 表示原优先级,newprio 表示新优先级。oldprio 可以通过 OS_PRIO_SELF 指代当前任务。新优先级必须处于允许使用的范围内,并且不能已被其他任务占用。
2. 只修改 TCB 中的一个数字就够了吗
不够。 第3章已经介绍,μC/OS-II 用多个数据结构管理优先级。修改优先级时,内核需要协调更新:
OSTCBPrio:TCB 保存的优先级编号;OSTCBPrioTbl[]:优先级到 TCB 的映射关系;OSRdyTbl[]、OSRdyGrp:任务处于就绪态时的就绪位;- 任务正等待某个事件时,该事件对应的任务等待位图。
事件等待位图(Event Wait Bitmap) 与普通就绪表不同:它记录哪些任务正在等待某个信号量、邮箱或其他事件。如果等待任务的优先级改变,内核也应同步改变它在事件等待结构中的位置。
#mermaid-svg-XMJD8XEiP7ekdHWb{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-XMJD8XEiP7ekdHWb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XMJD8XEiP7ekdHWb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XMJD8XEiP7ekdHWb .error-icon{fill:#552222;}#mermaid-svg-XMJD8XEiP7ekdHWb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XMJD8XEiP7ekdHWb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XMJD8XEiP7ekdHWb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XMJD8XEiP7ekdHWb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XMJD8XEiP7ekdHWb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XMJD8XEiP7ekdHWb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XMJD8XEiP7ekdHWb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XMJD8XEiP7ekdHWb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XMJD8XEiP7ekdHWb .marker.cross{stroke:#333333;}#mermaid-svg-XMJD8XEiP7ekdHWb svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XMJD8XEiP7ekdHWb p{margin:0;}#mermaid-svg-XMJD8XEiP7ekdHWb .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-XMJD8XEiP7ekdHWb .cluster-label text{fill:#333;}#mermaid-svg-XMJD8XEiP7ekdHWb .cluster-label span{color:#333;}#mermaid-svg-XMJD8XEiP7ekdHWb .cluster-label span p{background-color:transparent;}#mermaid-svg-XMJD8XEiP7ekdHWb .label text,#mermaid-svg-XMJD8XEiP7ekdHWb span{fill:#333;color:#333;}#mermaid-svg-XMJD8XEiP7ekdHWb .node rect,#mermaid-svg-XMJD8XEiP7ekdHWb .node circle,#mermaid-svg-XMJD8XEiP7ekdHWb .node ellipse,#mermaid-svg-XMJD8XEiP7ekdHWb .node polygon,#mermaid-svg-XMJD8XEiP7ekdHWb .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XMJD8XEiP7ekdHWb .rough-node .label text,#mermaid-svg-XMJD8XEiP7ekdHWb .node .label text,#mermaid-svg-XMJD8XEiP7ekdHWb .image-shape .label,#mermaid-svg-XMJD8XEiP7ekdHWb .icon-shape .label{text-anchor:middle;}#mermaid-svg-XMJD8XEiP7ekdHWb .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-XMJD8XEiP7ekdHWb .rough-node .label,#mermaid-svg-XMJD8XEiP7ekdHWb .node .label,#mermaid-svg-XMJD8XEiP7ekdHWb .image-shape .label,#mermaid-svg-XMJD8XEiP7ekdHWb .icon-shape .label{text-align:center;}#mermaid-svg-XMJD8XEiP7ekdHWb .node.clickable{cursor:pointer;}#mermaid-svg-XMJD8XEiP7ekdHWb .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-XMJD8XEiP7ekdHWb .arrowheadPath{fill:#333333;}#mermaid-svg-XMJD8XEiP7ekdHWb .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-XMJD8XEiP7ekdHWb .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-XMJD8XEiP7ekdHWb .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XMJD8XEiP7ekdHWb .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-XMJD8XEiP7ekdHWb .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XMJD8XEiP7ekdHWb .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-XMJD8XEiP7ekdHWb .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-XMJD8XEiP7ekdHWb .cluster text{fill:#333;}#mermaid-svg-XMJD8XEiP7ekdHWb .cluster span{color:#333;}#mermaid-svg-XMJD8XEiP7ekdHWb div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-XMJD8XEiP7ekdHWb .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-XMJD8XEiP7ekdHWb rect.text{fill:none;stroke-width:0;}#mermaid-svg-XMJD8XEiP7ekdHWb .icon-shape,#mermaid-svg-XMJD8XEiP7ekdHWb .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XMJD8XEiP7ekdHWb .icon-shape p,#mermaid-svg-XMJD8XEiP7ekdHWb .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-XMJD8XEiP7ekdHWb .icon-shape rect,#mermaid-svg-XMJD8XEiP7ekdHWb .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XMJD8XEiP7ekdHWb .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-XMJD8XEiP7ekdHWb .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-XMJD8XEiP7ekdHWb :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-XMJD8XEiP7ekdHWb .default>*{fill:#ffffff!important;stroke:#000000!important;color:#000000!important;stroke-width:1px!important;}#mermaid-svg-XMJD8XEiP7ekdHWb .default span{fill:#ffffff!important;stroke:#000000!important;color:#000000!important;stroke-width:1px!important;}#mermaid-svg-XMJD8XEiP7ekdHWb .default tspan{fill:#000000!important;} 就绪
等待事件
其他状态
修改任务优先级
检查新优先级是否可用
更新 TCB 的优先级与位掩码
更新优先级到 TCB 的映射
任务原来处于什么状态?
更新就绪表中的位置
更新事件等待位图
保留相应状态信息
重新进行调度判断
修改优先级涉及的内核信息
这也是一道典型的原理题:为什么修改任务优先级可能引起任务切换? 因为优先级改变后,最高优先级就绪任务可能不再是当前任务,内核必须重新决定 CPU 使用权。
3. 数值变大到底是升高还是降低优先级
在 μC/OS-II 中,数字越小优先级越高。因此:
| 修改前 | 修改后 | 实际含义 |
|---|---|---|
| 10 | 4 | 优先级升高 |
| 4 | 10 | 优先级降低 |
| 10 | 10 | 优先级没有变化 |
考试中必须先判断编号规则,不能用日常语言中的"数字更大意味着级别更高"理解。
九、挂起任务:OSTaskSuspend()
1. 挂起究竟是什么意思
教材4.07节给出:
c
INT8U OSTaskSuspend(INT8U prio);
挂起(Suspend) 是暂时禁止某个任务进入可调度的就绪集合。内核会在 TCB 的任务状态字段中设置 OS_STAT_SUSPEND 标志,并在必要时从就绪表清除该任务对应的位。
挂起并不删除任务。任务的代码、TCB 和任务栈仍然存在,它之前已经建立的运行现场也可以保留。
2. 挂起与延时为什么不是同一个概念
延时(Delay) 是任务主动请求在一段时间内不参与运行,时间由内核节拍管理;挂起(Suspend) 则是额外设置的禁止调度条件,通常需要显式调用恢复函数解除。
例如,一个任务先调用 OSTimeDly(100),随后又被其他任务挂起:即使100个时钟节拍已到,任务也不能仅凭延时结束就立即继续运行,因为它仍然具有挂起标志。
教材明确指出:挂起状态可以与其他等待条件同时存在。将"挂起"等同于"延时停止计数"是错误理解。
3. 为什么任务自己挂起后会发生调度
当正在运行的任务调用 OSTaskSuspend(OS_PRIO_SELF),它设置自己的挂起标志并失去就绪资格。由于它不再能够使用 CPU,调度器需要选择其他符合条件的任务运行。
如果是一个任务挂起另一个尚未运行的任务,那么正在运行的任务未必需要立即切换。
十、恢复任务:OSTaskResume()
1. 恢复的本质是清除挂起标志
教材4.08节给出:
c
INT8U OSTaskResume(INT8U prio);
**恢复(Resume)**并不是强制让目标任务立即运行,而是撤销原先由 OSTaskSuspend() 设置的 OS_STAT_SUSPEND 标志。
教材中还有两项判断非常关键:
- 目标任务是否仍在等待信号量、邮箱等事件;
- 延时计数
OSTCBDly是否已经归零。
只有这些阻塞条件也全部解除后,任务才能重新进入普通就绪表,再按照优先级参与调度。
2. 挂起、延时和事件等待可能同时存在
| 挂起标志 | 其他事件等待 | 延时计数 | 调用 OSTaskResume() 后 |
|---|---|---|---|
| 有 | 无 | 0 | 清除挂起标志,成为就绪任务 |
| 有 | 有 | 0 | 清除挂起标志,但继续等待事件 |
| 有 | 无 | 大于0 | 清除挂起标志,但继续等待延时结束 |
| 有 | 有 | 大于0 | 清除挂起标志,仍需满足其他等待条件 |
| 无 | 任意 | 任意 | 本来没有挂起,调用恢复通常返回相应错误状态 |
表中"大于0"表示内核维护的任务延时或等待超时计数仍未结束,不是 CPU 正在执行的运行时间。
3. 一个需要推导的任务状态例子
假设任务 A 的优先级为5,任务 B 的优先级为10。A 先进入等待状态,B 随后挂起 A,再恢复 A。
| 时刻 | 发生的操作 | 任务 A 的结果 |
|---|---|---|
| t0t_0t0 | A 等待一个信号量 | A 阻塞,不在就绪表中 |
| t1t_1t1 | B 调用 OSTaskSuspend(5) |
A 既等待信号量,又带有挂起标志 |
| t2t_2t2 | B 调用 OSTaskResume(5) |
只撤销挂起,A 仍等待信号量 |
| t3t_3t3 | 信号量被发布 | A 具备就绪条件,可能抢占 B |
结论:恢复挂起不等于解除所有等待,更不等于直接切换到该任务。
4. 任务状态和 CPU 运行资格可以分层理解
#mermaid-svg-PwaApBYwghIWiOMJ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PwaApBYwghIWiOMJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PwaApBYwghIWiOMJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PwaApBYwghIWiOMJ .error-icon{fill:#552222;}#mermaid-svg-PwaApBYwghIWiOMJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PwaApBYwghIWiOMJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PwaApBYwghIWiOMJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PwaApBYwghIWiOMJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PwaApBYwghIWiOMJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PwaApBYwghIWiOMJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PwaApBYwghIWiOMJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PwaApBYwghIWiOMJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PwaApBYwghIWiOMJ .marker.cross{stroke:#333333;}#mermaid-svg-PwaApBYwghIWiOMJ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PwaApBYwghIWiOMJ p{margin:0;}#mermaid-svg-PwaApBYwghIWiOMJ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-PwaApBYwghIWiOMJ .cluster-label text{fill:#333;}#mermaid-svg-PwaApBYwghIWiOMJ .cluster-label span{color:#333;}#mermaid-svg-PwaApBYwghIWiOMJ .cluster-label span p{background-color:transparent;}#mermaid-svg-PwaApBYwghIWiOMJ .label text,#mermaid-svg-PwaApBYwghIWiOMJ span{fill:#333;color:#333;}#mermaid-svg-PwaApBYwghIWiOMJ .node rect,#mermaid-svg-PwaApBYwghIWiOMJ .node circle,#mermaid-svg-PwaApBYwghIWiOMJ .node ellipse,#mermaid-svg-PwaApBYwghIWiOMJ .node polygon,#mermaid-svg-PwaApBYwghIWiOMJ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-PwaApBYwghIWiOMJ .rough-node .label text,#mermaid-svg-PwaApBYwghIWiOMJ .node .label text,#mermaid-svg-PwaApBYwghIWiOMJ .image-shape .label,#mermaid-svg-PwaApBYwghIWiOMJ .icon-shape .label{text-anchor:middle;}#mermaid-svg-PwaApBYwghIWiOMJ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-PwaApBYwghIWiOMJ .rough-node .label,#mermaid-svg-PwaApBYwghIWiOMJ .node .label,#mermaid-svg-PwaApBYwghIWiOMJ .image-shape .label,#mermaid-svg-PwaApBYwghIWiOMJ .icon-shape .label{text-align:center;}#mermaid-svg-PwaApBYwghIWiOMJ .node.clickable{cursor:pointer;}#mermaid-svg-PwaApBYwghIWiOMJ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-PwaApBYwghIWiOMJ .arrowheadPath{fill:#333333;}#mermaid-svg-PwaApBYwghIWiOMJ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-PwaApBYwghIWiOMJ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-PwaApBYwghIWiOMJ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PwaApBYwghIWiOMJ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-PwaApBYwghIWiOMJ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PwaApBYwghIWiOMJ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-PwaApBYwghIWiOMJ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-PwaApBYwghIWiOMJ .cluster text{fill:#333;}#mermaid-svg-PwaApBYwghIWiOMJ .cluster span{color:#333;}#mermaid-svg-PwaApBYwghIWiOMJ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-PwaApBYwghIWiOMJ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-PwaApBYwghIWiOMJ rect.text{fill:none;stroke-width:0;}#mermaid-svg-PwaApBYwghIWiOMJ .icon-shape,#mermaid-svg-PwaApBYwghIWiOMJ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PwaApBYwghIWiOMJ .icon-shape p,#mermaid-svg-PwaApBYwghIWiOMJ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-PwaApBYwghIWiOMJ .icon-shape rect,#mermaid-svg-PwaApBYwghIWiOMJ .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PwaApBYwghIWiOMJ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-PwaApBYwghIWiOMJ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-PwaApBYwghIWiOMJ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-PwaApBYwghIWiOMJ .default>*{fill:#ffffff!important;stroke:#000000!important;color:#000000!important;stroke-width:1px!important;}#mermaid-svg-PwaApBYwghIWiOMJ .default span{fill:#ffffff!important;stroke:#000000!important;color:#000000!important;stroke-width:1px!important;}#mermaid-svg-PwaApBYwghIWiOMJ .default tspan{fill:#000000!important;} 否
是
否
是
否
是
任务已建立并具有 TCB
挂起标志已清除?
不能进入就绪表
延时已到且事件条件满足?
仍处于等待状态
进入就绪表
是否为最高优先级就绪任务?
保持就绪,等待 CPU
具备被调度运行的资格
任务恢复后能否运行的判断条件
图中最后一项仍需满足内核允许调度等条件。例如调度器锁定或中断尚未完成时,即使某个任务优先级最高,也不会在任意指令位置不受约束地立即切换。
十一、查询任务信息:OSTaskQuery()
1. 查询函数能够得到什么
教材4.09节提供:
c
INT8U OSTaskQuery(INT8U prio, OS_TCB *pdata);
调用者提供一个 OS_TCB 对象用于接收查询结果;内核会将指定任务的 TCB 信息复制到该对象中。传入 OS_PRIO_SELF 可用于查询当前任务。
重点区别:它返回的是任务控制块的副本(Copy),而不是把原始 TCB 的所有权交给应用程序。
2. 为什么查询信息不能随意修改后当作内核状态
查询结果是独立复制的数据,修改这份副本中的 OSTCBPrio、OSTCBDly 等成员,不会同步修改内核正在维护的原始任务控制块。
TCB 中还可能含有指向内核其他结构的指针。即使复制后能看到这些地址,也不意味着应用程序可以随意沿指针修改内部数据结构,否则可能破坏调度器正常工作。
因此,OSTaskQuery() 更适合状态查看、调试和诊断,不是修改任务内部状态的通用接口。
十二、任务管理与内核调度的整体关系
1. 各个 API 最终是否会触发调度
| 函数 | 是否可能改变就绪资格 | 是否可能改变 CPU 使用者 |
|---|---|---|
OSTaskCreate() |
是,新任务可进入就绪表 | 是;若内核已运行且新任务优先级更高 |
OSTaskCreateExt() |
是 | 是;判断方式与基本创建相同 |
OSTaskStkChk() |
否 | 不因检查结果主动调度 |
OSTaskDel() |
是,任务被移除 | 是,特别是删除当前任务时 |
OSTaskDelReq() |
仅登记请求时不直接改变 | 请求本身通常不直接触发切换 |
OSTaskChangePrio() |
可以改变位图中的优先级位置 | 是 |
OSTaskSuspend() |
是,取消可运行资格 | 是,尤其是当前任务挂起自己时 |
OSTaskResume() |
解除挂起;是否就绪取决于其他状态 | 是;仅在目标真正就绪且优先级足够高时 |
OSTaskQuery() |
否 | 不因查询本身主动调度 |
表中"可能"非常重要。内核调用了调度函数,不意味着一定发生上下文切换。 如果调度前后最高优先级就绪任务没有变化,就不需要切换到另一任务。
2. 为什么多个 API 都离不开临界区
临界区(Critical Section) 是对共享数据结构进行不可被有关执行单元交错破坏的访问区域。任务管理操作经常要同时修改 TCB、优先级映射和就绪位图,因此需要保证这些关联结构保持一致。
例如,不能让调度器读到了"新任务在就绪表中",却还找不到该任务对应的 TCB。临界区的设计目标正是避免这种更新到一半就被打断的情况。
但临界区并非越大越好。内核要在数据一致性(Consistency) 和中断响应时间(Interrupt Latency) 之间取得平衡。教材删除任务的源码中,就采用暂时撤销中断保护、执行必要操作后再恢复保护等步骤,避免一次长时间关闭中断。
3. 三个层次的区别
学习时建议分别判断以下三个问题:
- 任务是否存在? 看任务是否已经建立、是否存在 TCB。
- 任务是否就绪? 看挂起、延时、事件等待等条件是否允许它参与调度。
- 任务是否正在运行? 看调度器最终是否把 CPU 分配给它。
任务存在不代表就绪;任务就绪不代表正在运行;当前正在运行的任务也可能因为调用任务管理 API 而立即失去运行资格。
十三、结合电机控制系统分析任务管理
假设一套 MCU 电机控制软件中,存在参数管理、CAN 通信与显示三个软件任务,同时还使用 ADC 中断和 PWM 外设完成快速控制。这里仅讨论 RTOS 如何组织后台任务,并不假定高速电流环必须在 RTOS 任务中完成。
| 模块 | 推荐理解方式 | 对应本章知识 |
|---|---|---|
| 参数管理任务 | 系统启动时创建,等待参数更新事件 | 创建、事件等待 |
| CAN 通信任务 | 可能在不同工作模式下调整任务优先级 | 修改优先级、调度 |
| 显示任务 | 特定模式下暂停刷新,恢复后继续执行 | 挂起与恢复 |
| 自检任务 | 自检完成后清理资源并退出 | 请求删除与自删除 |
| 监测任务 | 查询任务状态、评估任务栈余量 | OSTaskQuery()、OSTaskStkChk() |
例如系统进入保护模式后,如果暂时不需要刷新显示,可以挂起显示任务。故障解除后调用恢复函数,显示任务才重新取得参与调度的资格。但它仍须服从优先级规则,不会因为"恢复"就一定抢占当前任务。
另一个容易忽视的问题是,高速实时控制的周期要求可能远高于普通 RTOS 时钟节拍。对于 10 kHz、20 kHz 级别的电流控制环,通常还需考虑硬件定时触发、中断延迟、任务切换开销与执行抖动,不能简单地以 OSTimeDly() 代替精密硬件定时。
十四、典型问答
1. 什么是任务管理?它的主要内容有哪些?
答: 任务管理是操作系统对任务生命周期及其调度属性进行维护的过程。μC/OS-II 的任务管理包括任务的创建、删除、请求删除、优先级修改、挂起、恢复,以及任务栈检查和任务信息查询。其核心是维护任务控制块、任务栈、优先级映射及任务就绪信息,使调度器能够正确选择任务。
2. OSTaskCreate() 与 OSTaskCreateExt() 的区别是什么?
答: 两者都可创建任务并初始化任务运行环境。OSTaskCreate() 使用任务入口、参数、初始栈顶与优先级四个参数;OSTaskCreateExt() 另外提供任务标识号、栈底指针、栈容量、扩展数据指针和创建选项,可支持任务栈的检查、清零等扩展功能。
3. 为什么 μC/OS-II 的每个任务都需要独立任务栈?
答: 任务可能在任意允许切换的位置暂停,再在以后的某个时刻继续执行。每个任务必须保存自己的函数调用环境和 CPU 运行现场;若多个任务共用同一套任务栈,任务切换后原先保存的数据和返回位置可能被其他任务覆盖。因此每个任务需要独立任务栈,并由 TCB 保存相应栈指针。
4. OSTaskStkInit() 和 OSTaskStkChk() 有什么区别?
答: OSTaskStkInit() 用于任务创建时构造符合处理器要求的初始任务现场,使任务首次运行能够正确恢复寄存器与入口地址;OSTaskStkChk() 用于检查已经建立的任务栈使用情况,估算剩余空间。前者参与创建与移植,后者属于运行诊断。
5. 为什么任务创建成功,不一定立刻运行?
答: 创建成功只表示任务的 TCB、任务栈等初始化完成并具有就绪资格。调度器仍须按照优先级选择运行任务;如果内核尚未启动,或者系统中已有更高优先级就绪任务,新任务就不会立即运行。
6. 删除任务与删除任务函数有什么不同?
答: OSTaskDel() 从内核任务管理结构中撤销任务,使任务不能再被调度;它不会擦除存放在程序存储器中的任务函数代码,也不保证自动释放该任务申请的全部应用资源。
7. 为什么不建议直接强制删除一个正在占用资源的任务?
答: 如果任务被强制删除,它可能来不及释放互斥资源、缓冲区或动态内存,导致其他任务长时间等待、内存泄漏或状态不一致。对于需要善后的任务,可以先用 OSTaskDelReq() 发送请求,让被删除任务完成清理后自行调用 OSTaskDel(OS_PRIO_SELF)。
8. OSTaskDelReq() 是否会直接删除任务?
答: 不会。由其他任务调用时,它主要登记删除请求;目标任务通过 OSTaskDelReq(OS_PRIO_SELF) 查询请求状态,必要时完成资源清理并主动删除自己。仅登记请求并不等于内核已完成删除。
9. 为什么不能只改变 OSTCBPrio 就认为完成了优先级修改?
答: 内核还使用优先级到 TCB 的映射表、就绪位图和事件等待位图维护任务。仅修改 TCB 的优先级字段会使这些结构不一致,可能引起错误调度。因此 OSTaskChangePrio() 需要协调更新相关信息,再重新进行调度判断。
10. 任务挂起与任务延时有什么不同?
答: 任务延时由时钟节拍和延时计数管理,延时结束后任务可在其他条件满足时重新就绪。任务挂起则是在状态字段中增加额外的禁止调度标志,通常需要 OSTaskResume() 解除。延时到期不会自动撤销挂起标志。
11. 为什么执行 OSTaskResume() 后任务仍可能不能运行?
答: 恢复函数只清除挂起标志。如果任务还在等待信号量等事件,或者延时计数尚未结束,它仍然不能进入就绪表。即使所有等待条件已经解除,任务也必须按照优先级与其他就绪任务竞争 CPU。
12. 挂起任务会使任务栈和 TCB 消失吗?
答: 不会。挂起属于暂时改变任务的调度资格;任务控制块与任务栈一般继续保留,便于以后恢复执行。只有任务被删除时,它才从内核任务管理结构中撤销。
13. OSTaskQuery() 返回的是原任务控制块吗?
答: 不是。教材实现通过内存复制,将指定任务的 TCB 信息复制到用户提供的缓冲区,因此返回的是一份副本。修改副本不等于更改内核中原有任务的状态。
14. 调用任务管理 API 后,一定发生任务切换吗?
答: 不一定。任务创建、删除、修改优先级、挂起或恢复等操作可能改变最高优先级就绪任务,从而引起调度。但如果调度结果仍为当前任务,或者系统处于暂不允许切换的状态,就不会因此立即切换到另一任务。
15. 为什么任务管理操作经常需要临界区保护?
答: 因为这些操作可能同时改变任务控制块、优先级映射、就绪位图和事件等待结构。如果更新过程被不合适的中断或调度打断,多个数据结构可能出现不一致。临界区可在关键更新期间保证内核共享数据结构的完整性,但应尽量缩短,以减小中断响应延迟。
16. 综合分析:任务 A 优先级5,任务 B 优先级10。B 正在运行,调用 OSTaskResume(5) 后会怎样?
答: 先判断任务 A 是否已经被挂起。如果没有挂起,恢复操作不能按预期生效。如果 A 确实被挂起,恢复时清除挂起标志;若 A 没有剩余延时,也不再等待任何事件,它将进入就绪态。由于优先级5高于10,在系统允许抢占时 A 可能立即取代 B 运行;如果 A 仍等待其他条件,则 B 可以继续运行。
十五、总结与参考资料
第4章的知识可以归纳为一条连续的逻辑:
创建任务 建立栈和 TCB,使任务具备参与调度的基础;修改优先级 改变它的调度先后;挂起与恢复 改变它是否允许参与调度;请求删除与删除 负责安全结束任务;栈检查与状态查询为运行诊断提供依据。
复习时应重点掌握三个判断条件:任务是否仍然存在、任务是否满足就绪条件、任务是否是当前最高优先级的就绪任务。所有任务管理 API 都可以沿着这三个问题展开推理。
参考资料
- Jean J. Labrosse:《嵌入式实时操作系统 μC/OS-II(第2版)》,第4章"任务管理",第4.00~4.09节。
- 同书第3章"内核结构":任务控制块、就绪表、调度器和任务切换的相关内容。