4. 任务管理机制:任务控制块(TCB)结构、任务创建与删除、任务调度算法(优先级抢占+时间片轮转)
任务管理,说白了就是操作系统怎么管好一堆“活”。每个任务就是一个执行流,有自己的栈、优先级、状态。LiteOS-M 的任务管理,我做了这么多年嵌入式,觉得它设计得挺清爽——不花哨,但够用。
4.1 任务控制块(TCB)——任务的“身份证”
每个任务在系统里都有一个对应的结构体,叫任务控制块(Task Control Block,TCB)。你可以把它理解成任务的档案袋。系统调度任务时,全靠翻这个档案袋里的信息。
LiteOS-M 的 TCB 定义在 los_task.h 里,核心字段我列一下:
| 字段名 | 类型 | 说明 |
|---|---|---|
taskID |
UINT32 | 任务唯一标识,系统分配 |
taskStatus |
UINT16 | 任务状态(就绪、阻塞、挂起等) |
priority |
UINT16 | 任务优先级,数值越小优先级越高 |
stackSize |
UINT32 | 任务栈大小(字节) |
topOfStack |
UINT32 * | 栈顶指针,任务切换时保存现场 |
taskEntry |
TSK_ENTRY_FUNC | 任务入口函数指针 |
taskName |
CHAR * | 任务名称,调试用 |
timeSlice |
UINT16 | 时间片轮转剩余时间(Tick) |
sortList |
LOS_DL_LIST | 就绪队列或阻塞队列的链表节点 |
taskName 起得长一点,比如 "Sensor_Read_Task",这样在系统异常时打印任务列表,一眼就能看出哪个任务卡住了。
TCB 在系统初始化时,会预分配一个数组,叫 g_taskCBArray。默认大小是 LOSCFG_BASE_CORE_TSK_LIMIT,一般是 64 或 128。任务创建时,就从数组里取一个空闲的 TCB 出来用。
4.2 任务创建与删除——让任务“生”与“死”
任务创建,我用得最多的就是 LOS_TaskCreate 这个 API。它的原型长这样:
UINT32 LOS_TaskCreate(UINT32 *taskID, TSK_INIT_PARAM_S *taskInitParam);
参数 taskInitParam 是个结构体,你得把任务名、优先级、栈大小、入口函数都填进去。我贴一个实际项目里用过的例子:
UINT32 g_sensorTaskID;
TSK_INIT_PARAM_S taskParam = {0};
taskParam.pfnTaskEntry = (TSK_ENTRY_FUNC)SensorTaskEntry;
taskParam.uwStackSize = 0x1000; // 4KB 栈
taskParam.usTaskPrio = 5; // 优先级 5
taskParam.pcName = "Sensor_Task";
taskParam.uwResved = LOS_TASK_STATUS_DETACHED; // 分离模式
UINT32 ret = LOS_TaskCreate(&g_sensorTaskID, &taskParam);
if (ret != LOS_OK) {
// 创建失败处理
printf("Task create failed, ret = 0x%x\n", ret);
}
这里有个坑,我踩过好几次——栈大小。你想想看,如果任务里调用了 printf、sprintf 这类函数,它们会吃不少栈。我曾经在 STM32F4 上给一个任务只配了 512 字节栈,结果跑着跑着就 HardFault。后来查出来是栈溢出。我的建议是:初期至少给 2KB,调试稳定后再慢慢往下压。
删除任务用 LOS_TaskDelete:
ret = LOS_TaskDelete(g_sensorTaskID);
if (ret != LOS_OK) {
// 删除失败
}
4.3 任务调度算法——优先级抢占 + 时间片轮转
LiteOS-M 的调度策略,说白了就是两句话:
- 高优先级任务来了,低优先级立马让位。(优先级抢占)
- 同优先级任务,轮流干,每人干一个时间片。(时间片轮转)
我画了一张调度流程图,帮你理清逻辑:
调度器的核心逻辑在 osTaskSchedule 函数里。每次调度时,它会做三件事:
- 查就绪队列:找到优先级最高的那个任务。
- 比优先级:如果当前运行的任务优先级不是最高,就触发任务切换。
- 分时间片:如果同优先级有多个任务,就轮流执行,每个任务跑一个时间片(默认 10ms)。
时间片轮转的实现,靠的是系统 Tick 中断。每次 Tick 中断,系统会把当前任务的时间片计数减 1。减到 0 时,就把任务挪到同优先级队列的末尾,换下一个任务上。
4.4 任务状态与状态迁移
任务在生命周期里会经历几种状态。我整理了一张表:
| 状态 | 宏定义 | 说明 |
|---|---|---|
| 就绪 | OS_TASK_STATUS_READY | 任务已准备好,等待调度器分配 CPU |
| 运行 | OS_TASK_STATUS_RUNNING | 任务正在占用 CPU 执行 |
| 阻塞 | OS_TASK_STATUS_PEND | 任务等待某个事件(信号量、队列、延时等) |
| 挂起 | OS_TASK_STATUS_SUSPEND | 任务被主动挂起,不参与调度 |
| 删除 | OS_TASK_STATUS_DELETE | 任务已被删除,TCB 等待回收 |
状态迁移的典型路径是这样的:
- 创建 → 就绪:任务创建成功后,自动进入就绪队列。
- 就绪 → 运行:调度器选中它,开始执行。
- 运行 → 阻塞:任务调用了
LOS_TaskDelay或等待信号量。 - 阻塞 → 就绪:等待的条件满足了(延时到、信号量释放)。
- 运行 → 挂起:其他任务调用了
LOS_TaskSuspend。 - 挂起 → 就绪:调用了
LOS_TaskResume。
LOS_TaskDelay。这是不允许的!中断上下文不能做任务切换。正确的做法是在中断里只发信号量,让任务自己去等。否则系统会直接 panic。
4.5 任务优先级设计建议
优先级怎么分配?我一般遵循这几个原则:
- 实时性要求高的任务(比如电机控制、传感器读取)给高优先级,0~5 之间。
- 通信类任务(比如 UART 收发、SPI 通信)给中等优先级,6~10。
- 后台处理任务(比如日志记录、状态上报)给低优先级,11~15。
- 空闲任务(Idle Task)优先级最低,系统默认是 31。
LiteOS-M 支持 32 级优先级(0~31),0 最高,31 最低。这个范围对大多数 IoT 设备来说足够了。我做过一个项目,用了 8 个任务,优先级从 3 到 20,调度非常稳定。
4.6 任务栈溢出检测
栈溢出是嵌入式开发的“隐形杀手”。LiteOS-M 提供了栈溢出检测机制,在创建任务时,系统会在栈顶和栈底各写一个“魔数”(0xCCCCCCCC)。每次任务切换时,检查魔数是否被覆盖。如果被覆盖,说明栈溢出了。
开启方法:在 target_config.h 里定义 LOSCFG_BASE_CORE_TSK_MONITOR 为 1。
#define LOSCFG_BASE_CORE_TSK_MONITOR 1
检测到溢出时,系统会调用 OsStackOverflowHook,你可以在这里加打印或者进入断言。我一般会打印任务名和当前栈使用量:
VOID OsStackOverflowHook(UINT32 taskID)
{
TskCB *taskCB = OS_TCB_FROM_TASKID(taskID);
printf("Stack overflow! Task: %s, ID: %u\n",
taskCB->taskName, taskID);
while(1); // 死等,方便调试
}