2、任务管理基础:任务的概念、任务状态(就绪、运行、阻塞、挂起)、任务优先级

好,咱们今天聊聊任务管理。这是RTOS的核心中的核心。你想想看,一个裸机程序,就是一个大while循环,所有事情排队做。但RTOS不一样,它能把一个复杂系统拆成多个独立的任务,让它们看起来像是「同时」在运行。

2.1 任务到底是什么?

任务,说白了就是一段独立的程序执行流。每个任务都有自己的栈空间、自己的优先级、自己的状态。我习惯把任务想象成一个「独立的小人」——每个小人都有自己的工作要做,RTOS内核就是那个调度员,决定哪个小人什么时候干活。

在代码层面,任务通常是一个无限循环的函数:

void task_led(void *param)
{
    while(1)
    {
        // 点亮LED
        HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
        vTaskDelay(500);  // 阻塞500ms
        // 熄灭LED
        HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
        vTaskDelay(500);
    }
}

嗯,这里要注意:任务函数绝对不能返回!一旦返回,这个任务就「死」了。我在项目中遇到过新手把任务写成普通函数,执行完就return了,结果系统直接崩溃。所以记住,任务函数必须是无限循环,或者用vTaskDelete()主动删除自己。

2.2 任务的四种状态

一个任务在它的生命周期里,会经历四种状态。我刚开始学RTOS时,总觉得状态切换很抽象。后来我画了个图,一下子就明白了。

状态 含义 通俗理解
就绪(Ready) 任务已准备好运行,等待调度器分配CPU 「我准备好了,随时可以干活」
运行(Running) 任务正在占用CPU执行代码 「我正在干活,别打扰我」
阻塞(Blocked) 任务在等待某个事件(延时、信号量、队列等) 「我在等快递,到了叫我」
挂起(Suspended) 任务被暂停,调度器不会分配CPU给它 「我休假了,别找我」

核心要点:任何时刻,一个CPU核心上只能有一个任务处于运行状态。其他任务要么就绪、要么阻塞、要么挂起。

为什么会这样?因为CPU只有一个嘛。但通过快速切换,我们感觉所有任务都在同时运行。这就是「并发」的本质。

2.3 状态切换的典型场景

咱们用实际代码看看状态怎么切换。假设有三个任务:TaskA(高优先级)、TaskB(中优先级)、TaskC(低优先级)。

void TaskA(void *param)
{
    while(1)
    {
        // TaskA正在运行(Running)
        vTaskDelay(100);  // 进入阻塞(Blocked),等待100ms
        // 100ms后,TaskA回到就绪(Ready)
    }
}

void TaskB(void *param)
{
    while(1)
    {
        // TaskB正在运行(Running)
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);  // 进入阻塞,等待通知
        // 收到通知后,回到就绪
    }
}

void TaskC(void *param)
{
    while(1)
    {
        // TaskC正在运行(Running)
        vTaskSuspend(NULL);  // 把自己挂起(Suspended)
        // 只有其他任务调用vTaskResume()才能恢复
    }
}

我曾经在一个电力保护装置项目中,遇到一个诡异的问题:某个任务偶尔会「卡死」。排查了两天,最后发现是任务在等待信号量时,超时时间设成了0。结果信号量没来,任务直接进入阻塞状态,再也没人唤醒它。嗯,从那以后,我每次写阻塞调用都会检查超时参数。

2.4 任务优先级——谁先干活?

优先级决定了调度器「先让谁运行」。数值越小优先级越高?还是越大越高?这取决于RTOS的实现。FreeRTOS里,数值越大优先级越高。uC/OS则相反。我个人习惯用FreeRTOS,所以咱们以它为例。

我的建议:优先级不要超过32级。太多优先级反而增加调度开销,而且容易把人搞晕。我一般只用5-8个优先级,够用就好。

优先级的使用原则:

  • 实时性要求高的任务(如中断响应、保护逻辑)→ 高优先级
  • 周期性任务(如数据采集、状态刷新)→ 中等优先级
  • 后台任务(如日志记录、界面刷新)→ 低优先级

你想想看,如果保护逻辑任务和界面刷新任务优先级一样,会发生什么?调度器会轮流执行它们。万一保护逻辑需要立即响应,却被界面刷新任务抢了CPU,那后果不堪设想。所以,保护装置里,保护任务的优先级一定要最高。

2.5 优先级反转——一个经典坑

说到优先级,就不得不提优先级反转。这是个经典问题。简单说就是:高优先级任务在等一个资源,而这个资源被低优先级任务占着,中优先级任务又抢了低优先级任务的CPU,导致高优先级任务迟迟拿不到资源。

我曾经踩过的坑:在一个继电保护装置中,通信任务(高优先级)和采样任务(中优先级)共享一个数据缓冲区。采样任务拿到缓冲区后,被一个后台日志任务(低优先级)打断了。结果通信任务等了整整200ms才拿到缓冲区,差点导致保护误动作。后来我用了互斥量(带优先级继承),才彻底解决这个问题。

解决方案通常有三种:

  1. 优先级继承——低优先级任务临时提升到高优先级,尽快释放资源
  2. 优先级天花板——访问共享资源的任务,优先级统一设为一个固定高值
  3. 关中断保护——短时间关中断,但会影响实时性

我个人推荐用优先级继承,FreeRTOS的互斥量默认就支持这个特性。省心又可靠。

2.6 任务设计的一些经验

最后,分享几个我在项目中总结的经验:

  • 任务不要太多——每个任务都有独立的栈空间,任务多了内存吃紧。我一般控制在10个以内。
  • 任务栈大小要合理——太小会栈溢出,太大浪费内存。可以用uxTaskGetStackHighWaterMark()查看栈使用峰值。
  • 避免任务饿死——低优先级任务可能永远得不到CPU。可以用「时间片轮转」或「优先级动态调整」来缓解。
  • 阻塞调用是好事——任务阻塞时,CPU可以给其他任务用。所以尽量用阻塞等待,别用忙等待(while循环轮询)。

我记得有一次,一个同事把所有任务都设成相同优先级,结果系统响应特别慢。我一看,所有任务都在抢CPU,调度器频繁切换,效率极低。后来我根据任务的实时性要求重新分配了优先级,系统一下子就流畅了。所以说,优先级分配是个技术活,得多琢磨。

好了,任务管理的基础就这些。下一节咱们聊聊任务间通信——信号量、队列、事件组这些东西。到时候你会发现,任务之间怎么「说话」,比任务本身更有意思。