2. 任务与任务状态:任务定义、任务状态模型、状态转换图

大家好,我是你们的嵌入式系统讲师。今天我们来聊聊实时操作系统里最核心的概念——任务与任务状态。说实话,我做了十几年嵌入式开发,见过太多因为任务状态管理不当导致的系统崩溃。嗯,咱们今天就把这块彻底讲透。

2.1 任务到底是什么?

先说说任务的定义。在RTOS里,任务(Task)说白了就是一个独立的执行线程。它有自己的栈空间、优先级、任务控制块(TCB)。我习惯把任务想象成一个“独立的小人”——它有自己的工作台(栈)、自己的编号(任务ID)、自己的紧急程度(优先级)。

每个任务本质上就是一个无限循环的函数。你想想看,嵌入式系统要一直运行,任务当然不能执行完就退出。代码结构通常是这样的:

void myTask(void *parameter)
{
    // 初始化工作
    while(1)
    {
        // 任务主体代码
        // 等待事件、处理数据、输出结果
        vTaskDelay(10); // 让出CPU
    }
}

我在项目中遇到过一个问题:有个同事把任务写成了有限循环,结果任务执行完就“消失”了,系统直接卡死。所以记住:任务函数绝对不能返回

2.2 任务状态模型

任务在运行过程中,会在不同状态之间切换。RTOS里主要有四种状态:就绪、运行、阻塞、挂起。我刚开始学的时候总觉得状态太多记不住,后来发现其实很好理解——你就想象成一个人一天的状态:

  • 就绪(Ready):活都准备好了,就等CPU来干。好比你在工位前坐着,手头有活,就等老板(调度器)分配任务。
  • 运行(Running):正在使用CPU。就是老板说“你干这个”,然后你开始干活。
  • 阻塞(Blocked):在等某个事件。比如等串口数据、等信号量、等延时结束。就像你等快递——活干不了,只能等着。
  • 挂起(Suspended):被强制暂停了。好比老板说“你先停一下”,你就得停。

这里有个关键点:任何时刻,只有一个任务处于运行状态。单核CPU嘛,一次只能干一件事。

2.3 状态转换图

状态之间的转换是有规则的。我画个简单的转换关系:

创建 → 就绪 ←→ 运行 ←→ 阻塞
         ↑         ↓
         └── 挂起 ←─┘

具体转换条件是这样的:

当前状态 触发条件 下一状态
就绪 调度器选中该任务 运行
运行 时间片用完或被更高优先级任务抢占 就绪
运行 等待事件(延时、信号量、消息队列) 阻塞
阻塞 等待的事件发生 就绪
运行/就绪 调用挂起函数 挂起
挂起 调用恢复函数 就绪

核心要点:阻塞状态只能从运行状态进入,不能从就绪状态直接进入阻塞。为什么?因为任务只有正在运行时,才能知道自己要等什么事件。

2.4 实际项目中的状态管理

我曾经在一个车载ECU项目里踩过坑。当时有个传感器采集任务,设计时没考虑好状态转换,结果任务在阻塞和就绪之间频繁切换,CPU利用率飙升到90%以上。后来排查发现,是信号量释放时机不对,导致任务反复被唤醒又阻塞。

这里给大家几个实用建议:

  • 合理使用阻塞:不要用空循环等待,一定要用阻塞API。比如用vTaskDelay()而不是for(i=0;i<100000;i++)
  • 注意优先级反转:高优先级任务被低优先级任务间接阻塞,这是经典问题。我一般用优先级继承协议来解决。
  • 挂起状态要慎用:挂起任务容易导致资源泄漏。我曾经有个同事挂起了一个持有互斥锁的任务,结果其他任务全卡死了。

小技巧:调试时可以用uxTaskGetNumberOfTasks()vTaskList()查看当前所有任务的状态。FreeRTOS里这两个函数特别好用,能帮你快速定位问题。

2.5 状态转换的代码实现

以FreeRTOS为例,状态转换是通过调度器内部的vTaskSuspend()vTaskResume()vTaskDelay()等API触发的。调度器会维护一个就绪列表,每个优先级对应一个链表。任务状态变化时,调度器会把它从当前链表移到目标链表。

// 任务挂起示例
void vTaskSuspend(TaskHandle_t xTaskToSuspend)
{
    TaskHandle_t pxTCB;
    
    // 获取任务控制块
    pxTCB = xTaskToSuspend;
    
    // 从就绪/阻塞列表中移除
    uxListRemove(&(pxTCB->xStateListItem));
    
    // 添加到挂起列表
    vListInsertEnd(&xSuspendedTaskList, &(pxTCB->xStateListItem));
    
    // 如果挂起的是当前任务,触发调度
    if(pxTCB == pxCurrentTCB)
    {
        taskYIELD();
    }
}

警告:不要在中断服务函数里调用vTaskSuspend()!中断上下文不能阻塞,否则会导致系统崩溃。如果非要在中断里操作任务状态,用xTaskResumeFromISR()这种专门的中断安全版本。

好了,关于任务与任务状态,今天就讲这么多。记住一句话:状态管理是RTOS调度的基石。你想想看,如果连任务在干什么都不知道,调度器怎么合理分配CPU?下一章我们会讲任务优先级和调度策略,到时候这些状态转换会更有用武之地。