2. LiteOS-M 内核基础:任务管理与调度

任务管理,说白了就是内核怎么管好你创建的那些“小线程”。LiteOS-M 是一个实时内核,它得知道哪个任务该跑、哪个该等、哪个该睡。我刚开始接触 LiteOS-M 时,觉得任务管理不就是创建、删除、挂起、恢复嘛,没什么难的。结果第一次做多传感器数据采集项目,就因为优先级没调好,系统直接卡死。嗯,从那以后我再也不敢小看任务调度了。

2.1 任务的生命周期:创建、删除、挂起与恢复

一个任务从出生到消亡,会经历几个状态。LiteOS-M 的任务状态机其实很清晰:就绪、运行、阻塞、挂起、终止。我们开发者能直接干预的,主要是创建、删除、挂起和恢复这四个操作。

2.1.1 任务创建

创建任务用 LOS_TaskCreate。你需要填一个任务参数结构体,告诉内核:任务入口函数是哪个、栈多大、优先级多少、要不要名字。

UINT32 ret;
TSK_INIT_PARAM_S taskParam = {0};
taskParam.pfnTaskEntry = (TSK_ENTRY_FUNC)MyTaskFunc;  // 任务入口
taskParam.uwStackSize   = 0x1000;                     // 4KB 栈空间
taskParam.usTaskPrio    = 5;                          // 优先级 5
taskParam.pcName        = "MyTask";
taskParam.uwResved      = LOS_TASK_STATUS_DETACHED;   // 分离模式

UINT32 taskID;
ret = LOS_TaskCreate(&taskID, &taskParam);
if (ret != LOS_OK) {
    // 创建失败,查错误码
}
个人习惯:我每次创建任务后,都会立即检查返回值。别偷懒,很多诡异问题都是任务创建失败但代码继续往下跑导致的。

2.1.2 任务删除

任务跑完了,或者你想主动干掉它,用 LOS_TaskDelete。注意,删除任务时内核会回收它的栈空间和 TCB,但如果你用了分离模式,得确保没有其他任务还在引用它。

ret = LOS_TaskDelete(taskID);
if (ret != LOS_OK) {
    // 删除失败,可能任务已经不存在了
}
我曾经踩过的坑:在中断服务函数里直接调用 LOS_TaskDelete 删除当前正在运行的任务。结果系统直接挂掉。记住,不要在中断里删除任务,用消息或标志位通知任务自己退出。

2.1.3 任务挂起与恢复

挂起就是让任务暂停运行,但保留它的上下文。恢复就是让它重新进入就绪队列。这两个操作在调试和低功耗场景里特别有用。

// 挂起任务
ret = LOS_TaskSuspend(taskID);

// 恢复任务
ret = LOS_TaskResume(taskID);

挂起的任务不会参与调度,直到你主动恢复它。我在做低功耗项目时,经常把不用的传感器采集任务挂起,等需要数据时再恢复,能省不少电。

2.2 任务优先级与调度策略

LiteOS-M 用的是抢占式优先级调度。什么意思?就是高优先级的任务一旦就绪,立马抢走 CPU。低优先级的任务只能等。

2.2.1 优先级范围

LiteOS-M 支持 0 到 31 级优先级,0 最高,31 最低。系统内部会占用几个最高优先级给空闲任务和软件定时器任务。你实际能用的,一般是 1 到 30。

优先级 用途 说明
0 系统保留 最高优先级,留给内核关键任务
1-5 高优先级任务 实时性要求高的任务,如中断处理、控制环路
6-20 普通任务 大部分业务逻辑放这里
21-30 低优先级任务 后台处理、日志、统计等
31 空闲任务 系统空闲时运行

2.2.2 调度策略

同优先级任务之间,LiteOS-M 使用时间片轮转。每个任务跑完一个时间片(默认 10ms),就切换到下一个同优先级任务。不同优先级之间,高优先级任务随时可以抢占。

关键点:如果高优先级任务一直不阻塞,低优先级任务永远得不到 CPU。这就是所谓的“优先级反转”和“饿死”问题。我建议你给每个任务都加一个适当的阻塞点,比如 LOS_TaskDelay 或等待信号量。

2.3 任务栈空间配置与溢出检测

栈空间是任务运行时的“临时工作台”。局部变量、函数调用、中断现场都往栈里放。栈配小了,程序跑飞;配大了,内存浪费。我见过太多因为栈溢出导致的“灵异现象”——变量莫名其妙被改、函数返回地址被覆盖、系统随机重启。

2.3.1 栈空间配置原则

怎么配栈大小?没有标准答案,但有经验法则:

  • 先估后调:先根据任务里的局部变量和函数调用深度估算一个值,比如 512 字节或 1KB。然后跑起来看实际使用量。
  • 留余量:估算值乘以 1.5 到 2 倍。中断嵌套会额外消耗栈空间。
  • 不要贪多:每个任务多 1KB,10 个任务就多 10KB。在资源受限的 MCU 上,这可能是致命的。

2.3.2 栈溢出检测

LiteOS-M 提供了两种栈溢出检测机制:

  1. 栈顶标记法:创建任务时,内核会在栈顶写入一个固定魔数(比如 0xDEADBEEF)。每次任务切换时检查这个魔数是否被破坏。如果被改了,说明栈溢出了。
  2. 栈使用量统计:通过 LOS_TaskStackUsed 接口可以查看任务实际用了多少栈。
// 获取任务栈使用情况
UINT32 usedSize = LOS_TaskStackUsed(taskID);
UINT32 totalSize = LOS_TaskStackSize(taskID);
printf("Task %s: used %d / %d bytes\n", taskName, usedSize, totalSize);
我的调试习惯:开发阶段,我会在每个任务里周期性地打印栈使用率。如果发现某个任务栈使用率超过 80%,我就给它加栈或者优化代码。等产品稳定了,再关掉这些打印。

2.4 知识体系总览

下面这张图把任务管理与调度的核心逻辑串起来了。你可以把它当作本章的“地图”。

LiteOS-M 任务管理与调度核心逻辑 任务创建 就绪状态 运行状态 阻塞/挂起 调度策略 • 抢占式:高优先级抢占低优先级 • 时间片轮转:同优先级轮流运行 栈溢出检测 • 栈顶魔数标记法 • 栈使用量统计接口 任务 = 入口函数 + 栈空间 + 优先级 + TCB 调度器 = 优先级队列 + 时间片 + 上下文切换

2.5 避坑指南与实战建议

  • 优先级别设太高:我曾经把一个轮询按键的任务设成优先级 1,结果它把 CPU 吃满了,其他任务全饿死。按键任务用中断+信号量才是正解。
  • 栈溢出检测一定要开:开发阶段打开栈溢出检测,能帮你省下大量调试时间。等产品发布前再关掉,省点性能。
  • 任务删除要谨慎:删除任务前,确保它持有的资源(信号量、互斥锁、内存)都已经释放。否则会造成资源泄漏。
  • 挂起 vs 阻塞:挂起是外部强制暂停,阻塞是任务自己等资源。能用阻塞就别用挂起,阻塞更符合事件驱动模型。
重要提醒:不要在中断服务函数里调用 LOS_TaskSuspend 或 LOS_TaskDelete 操作当前任务。中断上下文和任务上下文是两套栈,混用会导致栈损坏。如果你需要在中断里触发任务状态变化,用消息队列或事件标志组通知任务自己处理。

任务管理是 LiteOS-M 内核的基石。你把任务创建、优先级、栈配置这三个点吃透了,后面学信号量、互斥锁、消息队列就会轻松很多。记住,好的任务设计 = 合理的优先级 + 够用的栈空间 + 清晰的阻塞点。别贪多,别偷懒,每个任务只做一件事,做好一件事。