4. LiteOS-M内核基础

任务管理,说白了就是操作系统怎么管好一堆“活”。每个任务就是一个执行流,有自己的栈、优先级、状态。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) {
    // 删除失败
}
注意: 任务删除后,系统会回收它的 TCB 和栈空间。但如果你在任务里动态申请了内存(比如 malloc),删除任务前一定要自己释放掉,系统不会帮你做这件事。

4.3 任务调度算法——优先级抢占 + 时间片轮转

LiteOS-M 的调度策略,说白了就是两句话:

  • 高优先级任务来了,低优先级立马让位。(优先级抢占)
  • 同优先级任务,轮流干,每人干一个时间片。(时间片轮转)

我画了一张调度流程图,帮你理清逻辑:

LiteOS-M 任务调度流程图 任务就绪 调度器启动 检查就绪队列 有更高优先级? 抢占当前任务 同优先级轮转 任务切换执行 时间片耗尽/任务阻塞 时间片默认 10ms 可配置 LOSCFG_BASE_CORE_TSK_DEFAULT_SLICE

调度器的核心逻辑在 osTaskSchedule 函数里。每次调度时,它会做三件事:

  1. 查就绪队列:找到优先级最高的那个任务。
  2. 比优先级:如果当前运行的任务优先级不是最高,就触发任务切换。
  3. 分时间片:如果同优先级有多个任务,就轮流执行,每个任务跑一个时间片(默认 10ms)。

时间片轮转的实现,靠的是系统 Tick 中断。每次 Tick 中断,系统会把当前任务的时间片计数减 1。减到 0 时,就把任务挪到同优先级队列的末尾,换下一个任务上。

关键点: 优先级抢占是“立即生效”的。比如一个低优先级任务正在跑,突然有一个高优先级任务就绪了(比如外部中断唤醒了它),调度器会在下一个 Tick 中断里直接抢走 CPU。低优先级任务连招呼都不打,就被挂起了。

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,调度非常稳定。

小技巧: 如果两个任务优先级相同,又都需要频繁响应,可以把其中一个的优先级调高 1 级,避免它们互相抢时间片导致抖动。当然,前提是那个任务确实更紧急。

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); // 死等,方便调试
}
一句话总结: 任务管理是操作系统的“心脏”。TCB 是任务的身份,创建/删除是任务的生老病死,调度算法是任务的“交通规则”。把这三点吃透了,LiteOS-M 的任务管理你就掌握了八成。