3.1 任务模型与状态机


3.1 任务模型与状态机

本节摘要:任务是 RTOS 里的独立执行流,由函数、栈、优先级三要素加上生命周期状态刻画。本节讲清任务如何创建、状态如何迁移、栈如何预算,为 3.2 节的调度分析提供准确的模型对象。

为什么一段顺序执行的采集加显示代码,拆成两个任务后就变得难以推算?答案在「并发交错」:单核处理器上两条执行流轮流占用处理器,交错点出现在任何一次抢占或阻塞处,可分析性取决于你对这些交错点的控制力。本节先建立任务的准确模型——把「任务是什么、有几个状态、栈里装了什么」钉牢,交错点的分析才能进入 3.2 节。

任务的四要素与创建

一个任务由四要素定义:入口函数(一段永不返回的循环)、栈空间(它的私有现场)、优先级(调度的唯一依据)、以及一个名字或参数。动态创建的写法:

/* 动态创建:栈与 TCB 从内核堆分配,失败需检查返回值 */ BaseType_t ok = xTaskCreate( vSamplingTask, /* 入口函数 */ "sampling", /* 名字:调试跟踪时的身份 */ 256, /* 栈深度,单位为栈字长而非字节 */ NULL, /* 传给函数的参数 */ tskIDLE_PRIORITY + 3, /* 优先级:数值越大越高 */ &xSamplingHandle); /* 句柄,供后续控制调用使用 */ configASSERT(ok == pdPASS);

正式产品更推荐静态创建:调用带 StaticTask_t 与 StackType_t 缓冲的变体,栈与控制块由你自己的数组提供。差别不止是「少用堆」——静态创建把内存 footprint 变成编译期可见的确定值,链接器直接告诉你还剩多少余量,这在做功能安全或严苛内存预算时是硬优势。

状态迁移:谁推动、谁记录

第二章的全景图里出现过状态机,这里补上推动力。就绪到运行的迁移由调度器在两种时刻推动:时钟节拍到点(时间触发)或某任务阻塞、某中断唤醒了高优先级任务(事件触发)。运行到阻塞由任务自己的 API 调用推动——请求信号量、读队列、延时,都是自愿交出处理器,这是最「守规矩」的让出方式,时机完全可预期。运行到就绪的被动迁移(被抢占)只有一种触发:比当前任务优先级更高的任务进入了就绪态。

理解状态的意义在于推算等待:3.2 节的响应时间公式里,一个任务的最坏等待由两部分构成,被高优先级抢占的累积时间(就绪态里的停留)加被低优先级临界区阻塞的时间(阻塞态里的特殊停留)。状态模型分清楚了,公式每一项才有落点。

栈:任务里最需要预算的部分

栈是任务现场的全部寄托:局部变量、函数调用链、中断发生时硬件压入的寄存器组、上下文切换时手动压入的另一半寄存器,都在这里。栈溢出的表现极具迷惑性——踩坏相邻任务的栈或内核数据,故障现场与真凶相距甚远,是嵌入式最著名的「玄学崩溃」来源之一。

预算方法按顺序做三步。第一步估初始值:按调用链最深函数的局部变量求和,加上中断现场(Cortex-M 上约几十字节,若用浮点再加倍),乘一点余量。第二步实测:把栈填成固定图案(内核的 uxTaskGetStackHighWaterMark 就是这么做的),跑全功能用例后读水位,剩余越少越危险。第三步设防:打开栈溢出检查钩子,让溢出第一时间进入你自己的处理函数而不是默默破坏数据。

图:任务栈与控制块的内存布局

图:任务栈与控制块的内存布局

图底那行换算提醒值得单独强调:FreeRTOS 的栈深度参数单位是栈字长,32 位机上 256 意味着 1024 字节。工程里见过太多「按字节思维」配 256 以为够用、实际连中断现场都放不下的案例。

周期形态与事件形态

任务代码的骨架只有两种正确形态。周期任务用绝对延时锚定节拍,适合采样、控制这类有固定节奏的工作,示例见 1.1 节;事件任务阻塞在某个同步对象上等待唤醒,适合报文处理、按键响应这类由外界驱动的工作:

/* 事件驱动形态:无事件时完全不让出处理器的等待 */ void vCanRxTask(void *arg) { CanMsg msg; for (;;) { if (xQueueReceive(xCanRxQueue, &msg, portMAX_DELAY) == pdTRUE) { CAN_Dispatch(&msg); /* 处理一帧,耗时须有上界 */ } } }

反模式是把事件任务写成「轮询加短暂延时」:每几毫秒醒来看一眼有没有事。它的问题不是「慢」,而是每一眼醒来都产生一次调度与切换,既浪费预算又给系统注入固定节拍的抖动源。能用阻塞等待表达的,就不要轮询。

本节要点回顾

  • 任务四要素中,优先级是调度唯一依据,栈是最需要预算的资产;
  • 静态创建让 footprint 编译期可见,是正式产品的默认选择;
  • 状态机三种迁移各有推动者,实时分析关心的是被动抢占的频率;
  • 栈预算三步走:估算、水印实测、溢出钩子兜底,缺一步都是隐患;
  • 栈深度参数按字长计,换算错误是最常见的配置事故;
  • 任务骨架只有周期与事件两种形态,轮询式等待是应清除的反模式。

常见问题

问:任务函数可以是带返回值的普通函数吗? 不可以,任务函数的原型是约定的:返回即视同违规,多数内核会触发错误钩子。正确写法是无限循环加阻塞点,退出走删除接口并通知他人。

问:优先级能在运行期改吗? 技术上多数内核支持,工程上慎用:运行期改优先级会推翻设计期做的全部响应时间分析,等于动态改写时间契约。确需调整(负载分档、模式切换)时,把候选优先级组预先设计并验证,运行期只在组间切换。

问:主任务与空闲任务的关系是什么? 空闲任务是内核强制存在的最低优先级任务,处理器闲下来就跑它。低功耗的空闲钩子、背景回收类工作都挂在它身上,3.3 节的切换统计里看到它占比过高,说明任务设计偏保守,处理器的真实余量比你以为的大。

问:主任务与子任务的区别是什么? 多数内核没有真正的父子关系,只有创建者与被创建者:被创建任务的存续不随创建者消亡。需要「子任务跟随主任务退出」的语义时,用事件组或删除接口显式实现——把隐式期望变成显式协议,是任务关系设计的通用原则。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U