2. RTOS基础回顾

好,咱们正式开始聊任务。很多初学者觉得任务调度很玄乎,其实说白了,任务就是一段可以独立运行的代码。但RTOS怎么管理这段代码?靠的就是任务控制块(TCB)。

我个人习惯把TCB想象成任务的「身份证」。系统不关心你的函数里写了什么,它只关心这张身份证上的信息——你叫什么(任务名)、你排第几(优先级)、你现在什么状态(就绪/运行/阻塞/挂起)、你的私人物品放哪(栈指针)。

核心观点:没有TCB,任务就是一段裸代码;有了TCB,任务才成为RTOS中的一等公民。

2.1 任务控制块(TCB)——任务的身份证

每个任务在创建时,内核都会分配一个TCB。TCB里存了什么?我列个表,你一看就明白。

字段 含义 我见过的坑
任务栈指针 指向任务私有栈顶 栈溢出时这个指针会乱飞
任务状态 就绪/运行/阻塞/挂起 状态切换漏了,任务就「死」了
优先级 0~255,数值越小优先级越高 优先级反转?我吃过亏
任务入口函数 任务启动时执行的函数地址 别把函数地址写错了
任务名称 调试用的字符串 命名规范很重要
延时/超时计数 用于阻塞状态计时 时间片轮转时容易算错

你看,TCB其实就是个结构体。我最早用uC/OS-II时,它的TCB大概有30多个字段。当时觉得太多了,后来自己写迷你内核才发现——每个字段都有用,少一个就出bug。

我的习惯:在项目初期就把TCB结构体定义好,哪怕有些字段暂时不用。预留总比后面加字段要安全,因为TCB是全局数据结构,改一次要重新编译所有任务代码。

2.2 任务状态机——四种状态与切换

任务不是一直在跑的。它会在四种状态之间跳来跳去:就绪、运行、阻塞、挂起。

为什么会这样?你想想看,单核CPU同一时刻只能跑一个任务。其他任务要么等着(就绪),要么在等某个事件(阻塞),要么被人为暂停(挂起)。

任务状态迁移图 就绪 (Ready) 运行 (Running) 阻塞 (Blocked) 挂起 (Suspended) 调度器选择 时间片用完/被抢占 等待事件/延时 事件到达/超时 主动/被动挂起 恢复挂起 说明: 挂起状态只能由其他任务或中断主动调用恢复,不能自动回到就绪。阻塞状态在事件到达后自动回到就绪。

这张图我建议你多看几遍。我曾经在一个项目中,把阻塞和挂起搞混了——任务在等串口数据,我用了挂起而不是阻塞。结果数据来了,任务醒不过来。排查了两天才找到原因。

避坑指南:阻塞是任务主动等资源,资源到了自动恢复;挂起是被别人暂停,必须由别人恢复。这两个状态千万别搞混。

2.3 任务优先级——谁先跑谁后跑

优先级是RTOS调度的心脏。数值越小优先级越高,这是大多数RTOS的约定(FreeRTOS反过来,数值越大优先级越高,但原理一样)。

优先级怎么定?我一般遵循几个原则:

  • 硬实时任务(比如电机控制)给最高优先级
  • 软实时任务(比如按键扫描)给中等优先级
  • 后台任务(比如日志输出)给最低优先级

但这里有个经典问题——优先级反转。低优先级任务占着资源不放,高优先级任务等资源,中优先级任务趁机抢跑。结果高优先级任务被中优先级任务「踩」了。

我记得第一次遇到这个问题是在一个工业控制器项目里。一个高优先级的报警任务被一个中优先级的UI刷新任务堵住了,报警延迟了200ms。客户差点退货。

解决方案:使用优先级继承协议或优先级天花板协议。FreeRTOS的互斥量自带优先级继承,这也是我推荐用互斥量而不是二值信号量的原因之一。

2.4 任务栈空间分配——给任务一个「私人房间」

每个任务都有自己的栈。栈里存什么?局部变量、函数调用返回地址、中断现场。如果栈不够大,任务就会踩到别人的内存——这叫栈溢出。

栈溢出是嵌入式开发中最隐蔽的bug之一。它不会立刻崩溃,而是跑着跑着突然死机,或者出现诡异的数据错误。

栈大小 适用场景 我的建议
64~128字节 极简任务,无局部数组 太危险,不推荐
256~512字节 简单控制任务 适合按键扫描、LED控制
1KB~2KB 中等复杂度任务 适合通信协议解析
4KB+ 复杂任务,有递归或大数组 注意内存总量,别超了

栈大小怎么估算?我有个笨办法:先把栈设大一点(比如1KB),跑一段时间后用内核提供的栈使用统计API查看峰值。然后根据峰值加30%余量。

经验之谈:我曾经在一个项目里把栈设成刚刚好,结果产品发布后客户现场偶发死机。最后发现是某个极端路径下多嵌套了两层函数调用。从那以后,我每个任务至少留50%的栈余量。

2.5 代码示例:创建一个任务

说了这么多,咱们看个实际代码。以FreeRTOS为例:

// 任务函数
void vTask1(void *pvParameters)
{
    while(1)
    {
        // 任务逻辑
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

// 任务句柄(本质是指向TCB的指针)
TaskHandle_t xTask1Handle = NULL;

// 创建任务
BaseType_t xReturn = xTaskCreate(
    vTask1,           // 任务函数
    "Task 1",         // 任务名称(调试用)
    256,              // 栈大小,单位是字(4字节)
    NULL,             // 任务参数
    2,                // 优先级,数值越大优先级越高(FreeRTOS约定)
    &xTask1Handle     // 返回的任务句柄
);

if (xReturn == pdPASS)
{
    // 创建成功
    // 可以在这里用xTask1Handle操作任务
}
else
{
    // 创建失败,通常是内存不足
    // 检查堆大小或栈大小
}

注意看,xTaskCreate内部会做三件事:分配TCB、分配栈空间、把任务加入就绪列表。如果堆不够,返回errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY。

常见错误:栈大小单位是「字」不是「字节」。在32位处理器上,256字=1024字节。我见过有人把栈设成256字节,结果任务一跑就崩。

2.6 本章小结

任务基础这块,说白了就四个东西:TCB是身份证,状态是当前处境,优先级是排队规则,栈是私人空间。这四个概念贯穿整个RTOS学习过程,后面讲调度算法、内存管理、任务间通信,全都离不开它们。

嗯,我建议你动手写一个最简单的任务,然后用调试器看看TCB里的字段值。亲眼看到状态从就绪变成运行,再变成阻塞,比看十遍书都管用。