二、任务管理与调度:任务状态机、任务创建与删除、优先级调度策略、任务切换原理

好,咱们进入第二章。任务管理与调度,说白了就是操作系统的“心脏”。你想想看,一个工控机里跑着几十上百个任务,谁先执行、谁后执行、谁被暂停、谁被干掉——这些都得有个规矩。这个规矩,就是任务管理与调度。

我个人习惯把任务管理比作一个“交通指挥系统”。任务就是路上的车,调度器就是红绿灯和交警。没有它,系统就乱套了。我在项目中遇到过好几次,任务优先级设错了,结果关键的控制任务被低优先级的日志打印任务给堵死了,现场差点出事故。嗯,咱们今天就把这块彻底讲透。

2.1 任务状态机:任务的一生

每个任务从创建到销毁,会经历几个状态。实时操作系统里,任务状态一般分为四种:就绪态运行态阻塞态挂起态。有些RTOS还会细分,但核心就是这四个。

状态 说明 典型场景
就绪态 任务已准备好,只等CPU 任务创建后、延时到期后
运行态 正在占用CPU执行 调度器选中该任务
阻塞态 等待某个事件或资源 等待信号量、消息队列、延时
挂起态 被主动暂停,不参与调度 调试、低功耗模式

状态之间的转换,我画个简单的图给你看:

创建 → 就绪态 → 运行态 → 阻塞态
                ↑        ↓
                ← 就绪态 ←
                
挂起态 ←→ 就绪态/阻塞态(通过API控制)

这里有个关键点:只有就绪态的任务才能被调度器选中。阻塞态的任务,哪怕优先级再高,也不会占用CPU。我曾经调试一个系统,发现一个高优先级任务一直不执行,查了半天才发现它卡在等待一个永远不会来的信号量上——这就是典型的“优先级反转”的前兆。

避坑指南:千万不要让高优先级任务长时间阻塞在低优先级任务释放的资源上。否则系统实时性会大打折扣。我曾经在一个电机控制项目里,就因为一个互斥信号量没处理好,导致电机响应延迟了200ms,差点把机械臂撞坏。

2.2 任务创建与删除:生与死的艺术

任务创建,在RTOS里一般用类似 xTaskCreate() 的API。参数包括:任务函数指针、任务名称、栈大小、参数、优先级、任务句柄。

我一般这样写:

// 创建一个控制任务
TaskHandle_t xControlTaskHandle = NULL;

void vControlTask(void *pvParameters)
{
    // 任务初始化
    while(1)
    {
        // 读取传感器
        // 执行控制算法
        // 输出控制信号
        vTaskDelay(pdMS_TO_TICKS(10)); // 10ms周期
    }
}

void main(void)
{
    // 创建任务
    xTaskCreate(
        vControlTask,       // 任务函数
        "ControlTask",      // 任务名称(调试用)
        1024,               // 栈大小(单位:字)
        NULL,               // 参数
        5,                  // 优先级(数值越大优先级越高)
        &xControlTaskHandle // 任务句柄
    );
    
    // 启动调度器
    vTaskStartScheduler();
}

任务删除呢?一般用 vTaskDelete()。但要注意:删除任务前,一定要确保它释放了所有占用的资源。比如动态分配的内存、打开的硬件外设等。否则会造成资源泄漏。

我的经验:任务创建时,栈大小别拍脑袋定。我习惯先给一个较大的值(比如2048字),跑起来后用RTOS自带的栈使用统计API看一下实际峰值,再调整到合适大小。这样既省内存,又不会栈溢出。

2.3 优先级调度策略:谁先谁后?

实时操作系统最常用的调度策略是基于优先级的抢占式调度。说白了就是:高优先级的任务一旦就绪,立刻抢占低优先级任务的CPU。

优先级怎么设?我有个原则:

  • 硬实时任务(比如电机控制、中断响应)→ 最高优先级
  • 软实时任务(比如数据采集、通信处理)→ 中等优先级
  • 非实时任务(比如日志打印、界面刷新)→ 最低优先级

但这里有个坑——优先级反转。举个例子:

  1. 任务A(高优先级)等待任务C(低优先级)释放的资源
  2. 任务B(中优先级)开始运行,抢占了任务C的CPU
  3. 任务A只能干等,直到任务B运行完,任务C才能释放资源

结果就是:高优先级任务被中优先级任务间接阻塞了。这在实际工控系统中非常危险。

解决办法?用优先级继承协议优先级天花板协议。大部分RTOS(比如FreeRTOS)的互斥量已经内置了优先级继承机制。所以,能用互斥量就别用二值信号量,这是我在项目里摔过跟头后学到的教训。

2.4 任务切换原理:上下文切换的秘密

任务切换,也叫上下文切换。每次切换,CPU都要做三件事:

  1. 保存当前任务的上下文(寄存器、栈指针、状态字等)
  2. 找到下一个要运行的任务(调度器决定)
  3. 恢复新任务的上下文(加载寄存器、栈指针等)

这个过程通常由PendSV异常(在ARM Cortex-M上)或类似的软中断来完成。为什么不用普通函数调用?因为上下文切换必须在特权模式下、原子性地完成,不能被其他中断打断。

我画个简化的切换流程:

当前任务运行中
    ↓
触发PendSV异常
    ↓
保存R4-R11、LR、PC、xPSR到当前任务栈
    ↓
更新当前任务TCB中的栈指针
    ↓
调度器选择下一个任务
    ↓
从新任务TCB中恢复栈指针
    ↓
从新任务栈中恢复R4-R11、LR、PC、xPSR
    ↓
跳转到新任务继续执行

这里有个性能指标:上下文切换时间。一般在几微秒到几十微秒之间。我测过一个Cortex-M4跑168MHz的系统,一次切换大约1.2μs。如果系统里任务切换太频繁(比如每1ms切换一次),那切换开销就占了1.2%的CPU时间——还算可以接受。但如果任务数太多、切换太频繁,这个开销就会显著影响实时性。

关键点:任务切换的触发时机有三个:
  • 当前任务主动阻塞(调用delay、等待信号量等)
  • 高优先级任务就绪(比如中断中释放了信号量)
  • 时间片轮转(同优先级任务轮流执行)

嗯,任务管理与调度这块,说白了就是“谁该干活、谁该等着、怎么换人”。掌握了这些,你就掌握了RTOS的核心。下一章咱们聊聊同步与通信——任务之间怎么“说话”,那可是另一门学问了。