4. 任务管理与调度:让多个任务在单核上“同时”运行

各位同学,今天我们聊一个嵌入式开发里绕不开的话题——任务管理与调度。说白了,就是怎么让一个单核CPU看起来像在同时干好几件事。我刚开始做嵌入式那会儿,总觉得这玩意儿很玄乎,后来踩了几个坑才明白,其实核心就三件事:任务的创建与删除、任务的状态机、以及调度算法怎么选。

4.1 任务创建与删除:别让任务“生而不养”

先说说任务的创建。在RTOS里,每个任务本质上就是一个无限循环的函数,加上一个独立的栈空间。我个人习惯用 xTaskCreate() 这个API,参数看着多,但记住几个关键点就行:

// 任务创建示例(FreeRTOS风格)
xTaskCreate(
    vTaskFunction,    // 任务函数指针
    "Task1",          // 任务名字(调试用)
    256,              // 栈深度(单位:字)
    NULL,             // 传给任务的参数
    2,                // 优先级(数字越大优先级越高)
    &xHandle         // 任务句柄(用于后续操作)
);

这里有个坑,我刚开始做项目时吃过亏——栈深度给得太小。任务跑着跑着就栈溢出了,系统莫名其妙重启。你想想看,如果任务里用了局部大数组或者递归调用,栈深度至少得翻倍。我建议新手先给个512字,跑稳定了再慢慢优化。

任务的删除相对简单,但要注意:千万别在中断服务函数里直接删除任务。我曾经这么干过,结果系统直接挂掉。正确的做法是发个信号量给任务,让任务自己调用 vTaskDelete(NULL) 来结束自己。

⚠️ 避坑指南: 我曾经在项目里创建了20多个任务,结果发现内存被吃光了。后来才意识到,每个任务至少需要几百字节的栈空间。所以,任务不是越多越好,够用就行。

4.2 任务状态机:就绪、运行、阻塞、挂起

任务的状态切换,是理解调度的基础。我画了一张图,帮你理清这四种状态的关系:

就绪态 运行态 阻塞态 挂起态 调度器选择 时间片用完/被抢占 等待事件/延时 事件到达/延时结束 调用挂起API 调用恢复API

这张图我建议你存下来。实际项目中,90%的调度问题都能从状态转换里找到原因。举个例子,任务卡住了不运行,多半是它一直在阻塞态里出不来——要么是等待的事件没发生,要么是延时时间设得太长。

4.3 优先级调度算法:抢占式 vs 时间片轮转

调度算法这块,我见过不少新手纠结。其实没那么复杂,就两种主流方式:

特性 抢占式调度 时间片轮转
核心思想 高优先级任务随时打断低优先级 同优先级任务轮流执行固定时间
适用场景 实时性要求高的系统(如电机控制) 交互式系统(如人机界面)
优点 响应快,关键任务不耽误 公平,每个任务都有机会
缺点 低优先级任务可能“饿死” 实时性无法保证
典型RTOS FreeRTOS(默认)、uC/OS Linux(CFS调度器)

我个人更偏爱抢占式调度,尤其是在做工业控制项目时。为什么呢?你想想看,如果电机过流了,你希望它等100ms再响应,还是立刻停下来?抢占式调度能保证高优先级任务(比如过流保护)在1ms内得到响应。

💡 关键点: 抢占式调度里,优先级不是越高越好。我见过有人把所有任务都设成最高优先级,结果系统一直在切换任务,实际干活的时间反而少了。合理的做法是:
- 实时任务(中断响应、控制算法):高优先级
- 普通任务(数据显示、日志记录):中优先级
- 后台任务(内存整理、自检):低优先级

4.4 实际项目中的调度策略选择

说了这么多理论,咱们来点实际的。我在做一个智能家居网关项目时,遇到过这么个情况:

  • 任务A:读取温度传感器(每100ms一次)
  • 任务B:处理用户按键输入(随时可能发生)
  • 任务C:更新LCD显示(每500ms一次)
  • 任务D:通过WiFi上传数据(每5秒一次)

我当时是这样分配优先级的:

任务B(按键)→ 优先级3(最高,因为用户交互需要即时响应)
任务A(传感器)→ 优先级2(中等,100ms周期足够)
任务C(显示)→ 优先级1(较低,慢一点没关系)
任务D(上传)→ 优先级0(最低,可以等)

为什么这么分?因为按键按下后,用户期望立刻看到反馈。而WiFi上传慢个一两秒,用户根本感觉不到。这就是抢占式调度的精髓——把宝贵的CPU时间留给最紧急的任务

🔧 我的小技巧: 调试调度问题时,我习惯在任务切换时打印一条日志,记录当前任务ID和切换原因。这样能快速定位是哪个任务在“霸占”CPU,或者哪个任务一直得不到执行。

4.5 避坑总结:调度器常见的三个坑

最后,分享三个我亲身踩过的坑:

  1. 优先级反转:低优先级任务持有高优先级任务需要的资源,导致高优先级任务被阻塞。解决办法是使用优先级继承协议。
  2. 任务饿死:高优先级任务一直运行,低优先级任务永远得不到CPU。我建议给每个任务设置一个“饥饿计数器”,如果连续N次没被调度,就临时提升它的优先级。
  3. 栈溢出检测:很多RTOS提供了栈高水位标记功能。我习惯在任务创建后,先跑一遍所有功能路径,然后检查栈使用峰值,确保留出30%的余量。

嗯,任务调度这块内容就讲到这里。记住一句话:调度器的本质是资源分配,而资源分配的核心是优先级管理。你只要把任务的紧急程度和资源需求理清楚,调度问题就解决了一大半。


公众号:蓝海资料掘金营,微信deep3321