2.1 任务的概念、创建与生命周期 API


2.1 任务的概念、创建与生命周期 API

本节摘要:一个任务由三件东西构成——一段永不返回的函数、一块专属的栈、一个记录全部管理信息的控制块。本节拆开这三件套看内部结构,然后过一遍任务的完整生命周期 API:动态与静态两种创建方式的行为差异与返回值检查,延时、挂起、优先级调整、删除与自删各自的语义、代价与陷阱。读完你能解释"创建一个任务到底发生了什么",并按场景选对创建与终结方式。

行话说,FreeRTOS 的任务是"虚拟的小处理器":每个任务都以为自己独占一颗核,从自己的函数第一行执行到天荒地老。制造这个幻觉的道具就是本节的三件套。上一章的基线工程已经创建过任务,本节把那行创建代码拆到底——这是理解后面状态机、调度与栈溢出排障的前提。

一、拆开一个任务:控制块、栈、函数体

任务函数本身没有神秘之处:普通的 C 函数,带一个空指针参数,永不返回。神秘感来自另外两件配套。是任务的私有工作台:局部变量、函数调用链的返回地址、被保存的现场寄存器全压在这里;两个任务的局部变量互不干扰,靠的就是各自的栈。在 Cortex-M 上栈向低地址生长,这点对第 4 节的溢出演练至关重要。

控制块是内核的登记簿,简化后的骨架长这样:

typedef struct tskTaskControlBlock { volatile StackType_t *pxTopOfStack; /* 栈顶指针,上下文切换第一字段 */ ListItem_t xStateListItem; /* 挂到就绪、阻塞、挂起链表 */ ListItem_t xEventListItem; /* 挂到队列、信号量的等待链表 */ UBaseType_t uxPriority; /* 当前优先级,可被继承临时抬高 */ StackType_t *pxStack; /* 栈起始地址,溢出检测用 */ char pcTaskName[16]; /* 名字,钩子函数报错时救命 */ /* 其余字段:基优先级、通知值、线程本地存储指针等 */ } tskTCB;

四个字段的分工值得逐个记牢。栈顶指针放在结构体第一个字段不是随意的——上下文切换的汇编代码要从控制块地址直接取它,放在首位省一次偏移计算。状态链表项让任务可以随时挂进"就绪链表""阻塞链表"或"挂起链表",调度器挑任务就是遍历就绪链表。事件链表项独立于状态链表项,任务阻塞在某个队列上时,事件链表项挂到该队列的等待链表、状态链表项挂到阻塞链表——一鱼两吃,第 4 章讲队列内部时会看到这套机关。优先级字段配合基优先级字段,实现互斥量的优先级继承时临时抬升、释放后还原。

栈和控制块的组合方式有讲究:动态创建时两者都从堆里来,静态创建时都来自你声明的全局数组。而任务函数本身呢?它只是只读代码镜像里的一段指令,所有任务实例共享——这就引出一个非常实用的推论:同一个函数可以创建出多个任务实例,各弹各的栈。

一个任务的内存布局

一个任务的内存布局

二、创建:动态一条龙,静态全托管

动态创建是基线工程里用的方式:一句创建,内核从堆里领内存给栈和控制块、初始化各字段、把任务挂进就绪链表,一切就绪。它的便利是真的,风险也是真的——堆是一个共享资源,分配可能失败(必须检查返回值),长期运行还可能碎片化(第 5 章专题)。静态创建把两块内存都换成你声明的全局数组与控制块变量,创建函数只做初始化,绝无失败可能,代价是栈大小在编译期定死、数组常驻不释放。安全认证类项目基本都走静态路线。

/* 静态创建:内存全部来自全局区,永不失败 */ static StackType_t xBlinkStack[128]; static StaticTask_t xBlinkTCB; void create_blink(void) { xTaskCreateStatic(vTaskBlink, "blink", 128, NULL, tskIDLE_PRIORITY + 2, xBlinkStack, &xBlinkTCB); /* 返回任务句柄;注意此时调度器可能尚未启动,任务只是就绪 */ }

多实例模式是任务函数共享性的经典应用:四个传感器各跑一个采集任务,函数只写一份,创建时把索引或配置结构指针作为参数传入:

typedef struct { uint8_t channel; uint16_t period_ms; } SensorCfg_t; static void vSensorTask(void *arg) { SensorCfg_t cfg = *(SensorCfg_t *)arg; /* 拷贝一份再用自己的栈 */ for (;;) { sample(cfg.channel); vTaskDelay(pdMS_TO_TICKS(cfg.period_ms)); } } /* 创建:参数指向各自的配置;配置若是局部变量必须先拷贝再创建后离开 */

这里埋着新手高频坑:传给任务的指针如果指向创建者的局部变量,创建者函数一返回,那块栈就被回收改写,任务读到的配置面目全非。稳妥做法是要么传全局或静态配置的指针,要么在任务开头先把内容拷贝进任务自己的栈。

对比项 动态创建 静态创建
栈与控制块来源 内核堆 全局或静态数组
失败可能 有,须查返回值
生命周期 可创建可删除,灵活 一般常驻
碎片风险 有(第 5 章)
栈大小调整 运行期改堆总量间接影响 编译期定死
适用 原型、动态负载 认证、长期运行、极端资源

三、生命周期 API 面板:行为、代价、陷阱

延时。相对延时从"现在"睡固定时长,写起来最省心;绝对延时以"上次唤醒时刻"为基准,周期任务的误差不累积。两者在 2.2 节的实验里对照演示,这里先给结论:所有"每 N 毫秒做一次"的需求都用后者。另外注意延时参数为特殊值零时等价于主动让出一次处理器——它不睡觉,只排队。

挂起与恢复。挂起是"人为按停":任务被移出一切链表、不参与调度,直到恢复调用到来。它和阻塞的本质区别是阻塞有期限(超时参数),挂起无期限且不挂任何事件。实践中挂起多用于调试与故障处置(比如把可疑任务按停观察系统是否恢复),不宜当常规流程控制用——被挂起的任务若持有互斥量,恢复之前没人能拿到,系统可能整体卡住,这类死锁模式第 4 章有完整分析。恢复接口有中断安全版本,带"是否唤醒了更高优先级任务"的返回值,中断里调用后要据此触发切换。

优先级调整。运行中改优先级是合法操作:任务可以给自己升降级(比如干完重活后自降,等事件再升回),内核会立即按新优先级重排调度。配合互斥量的优先级继承,内核内部也会临时抬高持锁任务——这就是控制块里"基优先级"与"当前优先级"两个字段并存的理由。

删除与自删。删除分"删自己"与"删别人":删自己是任务完成使命的标准退场方式;删别人则要三思。删除的内部机制分两步走:任务立即停止并进入终结链表,而它的栈与控制块要等空闲任务来回收——所以删除不是瞬间释放内存的,忙到空闲任务都没空跑的系统,删除积压会持续吃内存。两条铁律:绝不能删空闲任务(回收机制本身停摆);删除前必须确认它持有的资源(队列、互斥量、分配的缓冲)已有归属,内核只回收任务的栈和控制块,不管你任务里创建过什么。

⚠️ 自删的经典事故:任务里创建了队列与缓冲,末尾一句删除自己扬长而去——队列与缓冲成了没人认领的僵尸,内存只减不增。正确姿势是删除前把创建过的资源逐个释放,或者干脆不删:把"做完就退出"的任务改成"做完就阻塞等下一批"的长驻任务,是更省心的工程选择。

查询类接口也属于生命周期面板:查当前滴答数、按名查任务句柄、查任务优先级、查任务状态。它们是第 7 章运行时统计的地基。

本节要点回顾

  • 三件套分工:函数是共享代码,栈是私有工作台,控制块是内核登记簿——栈顶指针居控制块首位是切换汇编的取巧;
  • 两条链表项一身二职:状态链表项挂调度链表,事件链表项挂等待对象的链表,阻塞任务两边同时在册;
  • 动态创建必须查返回值,静态创建永不失败但栈定死;认证项目首选静态;
  • 传参坑:参数指向创建者局部变量等于埋雷,全局配置或任务内先拷贝是解法;
  • 挂起无期限、阻塞有期限,挂起持锁任务是死锁配方;
  • 删除是两段式且依赖空闲任务回收,删前清点资源,删空闲任务是系统级自杀。

下一节把镜头对准状态机:这些 API 每一次调用,本质都是在状态图上搬动任务——看懂迁移规则,调度器就不再神秘。


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