3. 任务管理核心:任务创建与删除、任务优先级、任务状态切换、空闲任务机制

各位同学,咱们今天聊聊RTOS里最核心的东西——任务管理。说白了,任务就是RTOS的灵魂。你想想看,一个裸机程序,就是一个大循环在那跑,但RTOS不一样,它能把一个复杂系统拆成多个独立的小任务,让它们看起来像是“同时”在运行。

我个人习惯把任务管理比作一个公司的运作。每个任务就是一个员工,有各自的职责,有优先级高低,有上班下班(创建删除),也有摸鱼等待(状态切换)。而那个永远在后台转悠的“空闲任务”,就是公司的保洁阿姨——没人干活的时候,她总在打扫卫生。

3.1 任务创建与删除:让任务“生”与“死”

任务创建,是RTOS开发的第一步。我记得刚入行那会儿,第一次用FreeRTOS创建任务,心里还挺激动——终于不用写那个while(1)大循环了!

任务创建的核心API,在FreeRTOS里是xTaskCreate()。它的原型长这样:

BaseType_t xTaskCreate(
    TaskFunction_t pvTaskCode,      // 任务函数指针
    const char * const pcName,      // 任务名称(调试用)
    configSTACK_DEPTH_TYPE usStackDepth, // 栈深度
    void *pvParameters,             // 任务参数
    UBaseType_t uxPriority,         // 任务优先级
    TaskHandle_t *pxCreatedTask     // 任务句柄
);

这里我要特别强调一下栈深度。我在项目中遇到过好几次任务莫名其妙崩溃,查了半天,最后发现是栈给得太小了。尤其是任务里用了printf、sprintf这类函数,它们会吃掉大量栈空间。我建议你给任务分配栈时,至少留出30%的余量。

⚠️ 避坑指南: 我曾经在一个电力监控项目中,因为任务栈分配不足,导致系统运行几小时后随机死机。排查了整整两天,最后用uxTaskGetStackHighWaterMark()一查,发现某个任务的栈只剩不到10个字节。从那以后,我每个任务都会在调试阶段打印栈水位线。

任务删除相对简单,调用vTaskDelete()即可。但要注意:千万不要在中断服务函数里删除任务!这是RTOS的禁忌。另外,任务删除后,它占用的栈空间和TCB(任务控制块)会被释放回堆,但如果你用了动态内存分配,记得在任务退出前自己释放。

3.2 任务优先级:谁先跑,谁后跑?

优先级是RTOS调度器的“指挥棒”。说白了,优先级高的任务,CPU先伺候它。

在FreeRTOS中,优先级数值越小,优先级越低(0是最低)。我一般这样分配:

优先级 典型任务 说明
0 空闲任务 系统自带,不可删除
1-2 后台任务、日志记录 不紧急,可以慢慢跑
3-4 数据采集、通信处理 中等优先级,需要及时响应
5-6 控制算法、故障检测 高优先级,实时性要求高
7 紧急中断处理 最高优先级,慎用

这里有个常见的坑:优先级反转。低优先级任务拿着一个资源,高优先级任务等着用,结果中优先级任务插进来把低优先级任务抢走了CPU,高优先级任务反而被“饿死”。嗯,这个问题在电力监控里特别要命——如果故障检测任务因为优先级反转被耽误了,后果不堪设想。

💡 我的经验: 解决优先级反转,可以用优先级继承或使用互斥量(Mutex)代替二值信号量。FreeRTOS的互斥量自带优先级继承机制,能有效避免这个问题。

3.3 任务状态切换:任务在干啥?

RTOS里的任务有四种状态:运行态、就绪态、阻塞态、挂起态。你想想看,这就像一个人的四种状态:正在干活、排队等着、被堵在路上、被叫去开会了。

  • 运行态(Running):任务正在使用CPU。单核CPU上,同一时刻只有一个任务在运行。
  • 就绪态(Ready):任务准备好了,但CPU被别的任务占着,它在等调度器叫它。
  • 阻塞态(Blocked):任务在等某个事件(比如延时到期、信号量可用、队列有数据)。这是最常见的状态。
  • 挂起态(Suspended):任务被vTaskSuspend()挂起了,除非有人调用vTaskResume(),否则它永远不会被调度。

状态切换的典型场景是这样的:

void vTaskExample(void *pvParameters)
{
    for(;;)
    {
        // 任务正在运行(运行态)
        vTaskDelay(pdMS_TO_TICKS(100)); // 进入阻塞态,等100ms
        // 100ms后,任务回到就绪态,等待调度器分配CPU
        // 如果它是最高优先级的就绪任务,立刻进入运行态
    }
}

我在项目中遇到过一个问题:某个任务调用了vTaskDelay()后,再也没有被调度回来。查了半天,发现是延时时间设置成了0。嗯,vTaskDelay(0)不会让任务阻塞,它只是让出CPU给同优先级的其他任务。如果你想让任务“睡一会儿”,延时至少1个tick。

3.4 空闲任务机制:系统的“保洁阿姨”

空闲任务(Idle Task)是RTOS自动创建的,优先级为0(最低)。它的作用很简单:当没有其他任务可以运行时,CPU就跑去跑空闲任务

空闲任务到底在干啥?默认情况下,它就是一个死循环,啥也不干。但你可以通过configUSE_IDLE_HOOK这个宏,给空闲任务挂一个钩子函数(Idle Hook),在里面做一些低优先级的后台工作。

我一般会在空闲钩子里做这些事情:

  • 统计CPU利用率(计算空闲任务运行时间占比)
  • 进入低功耗模式(比如WFI指令)
  • 做一些系统健康检查
🔑 关键点: 空闲任务绝对不能阻塞!你不能在空闲钩子里调用vTaskDelay()或等待信号量。因为空闲任务一旦阻塞,系统就没有任务可跑了,调度器会直接崩溃。我曾经见过一个新手在空闲钩子里加了延时,结果系统一启动就卡死。

另外,空闲任务还有一个重要职责:清理被删除任务的资源。当你调用vTaskDelete()删除一个任务时,这个任务不会立刻被清理,而是等到空闲任务运行时,才会回收它的TCB和栈空间。所以,如果你频繁创建和删除任务,一定要确保空闲任务有足够的CPU时间来做清理工作。

好了,任务管理的核心内容就这些。说白了,任务创建和删除是“生与死”,优先级是“尊卑有序”,状态切换是“忙闲交替”,空闲任务是“兜底保障”。把这四个点吃透了,RTOS的调度机制你就算入门了。

下一章,咱们聊聊任务间的通信与同步——信号量、队列、事件组这些“传话工具”。到时候你会发现,任务之间怎么“说话”,比任务本身怎么跑还要有意思。