3. 任务状态与调度:运行态、就绪态、阻塞态、挂起态;抢占式调度与时间片轮转

各位同学,咱们今天聊聊任务调度。这玩意儿说白了,就是操作系统怎么安排这些任务轮流用CPU。

我记得刚入行那会儿,总觉得任务调度是系统自动搞定的,跟我写应用没啥关系。直到有一次,我做的智能家居网关,三个传感器任务同时跑,结果系统卡死了。嗯,从那以后,我才真正开始研究任务到底是怎么被调度的。

3.1 任务的四种基本状态

一个任务从创建到结束,会经历几种不同的状态。FreeRTOS里,任务状态主要分为四种:运行态就绪态阻塞态挂起态

你想想看,这就像我们排队买奶茶。有人正在买(运行态),有人在排队等(就绪态),有人跑去上厕所了(阻塞态),还有人干脆不买了回家(挂起态)。

状态 含义 典型场景
运行态(Running) 任务正在占用CPU执行代码 当前正在执行的任务
就绪态(Ready) 任务可以运行,但CPU被别的任务占着 等待调度器分配CPU
阻塞态(Blocked) 任务在等待某个事件或资源 等待延时、等待信号量、等待队列
挂起态(Suspended) 任务被暂停,不参与调度 调试时暂停任务,或临时禁用

关键点:同一时刻,一个CPU核心上只能有一个任务处于运行态。其他任务要么排队等着,要么在睡觉(阻塞),要么被关禁闭(挂起)。

3.2 状态之间的转换

任务状态不是固定的,它们会随着系统运行不断切换。我个人习惯用一张图来理解这个转换关系:

  • 就绪态 → 运行态:调度器选中了这个任务,给它CPU使用权
  • 运行态 → 就绪态:时间片用完了,或者被更高优先级的任务抢占了
  • 运行态 → 阻塞态:任务主动调用延时函数,或者等待某个资源
  • 阻塞态 → 就绪态:等待的事件发生了,或者延时时间到了
  • 运行态/就绪态/阻塞态 → 挂起态:调用 vTaskSuspend() 函数
  • 挂起态 → 就绪态:调用 vTaskResume() 函数

避坑指南:我曾经在项目里犯过一个低级错误——在中断服务函数里直接调用了 vTaskSuspend()。结果系统直接崩溃了。记住,中断里尽量别直接操作任务状态,用信号量或消息队列通知任务自己处理。

3.3 抢占式调度

抢占式调度,说白了就是「谁优先级高谁先上」。高优先级的任务一旦就绪,立马把当前运行的低优先级任务踢下去。

我做过一个数据采集项目,采集任务优先级设得比通信任务低。结果采集任务刚读到一半数据,通信任务一触发,采集就被打断了。数据读出来全是乱的。后来我把采集任务的优先级调高,问题就解决了。

抢占式调度的特点:

  • 高优先级任务能立即获得CPU
  • 实时性好,紧急任务能及时响应
  • 但要注意优先级反转问题

注意:优先级不是越高越好。我见过有人把所有任务都设成最高优先级,结果系统一直在切换任务,实际干活的时间反而少了。优先级设计要合理,一般3-5个优先级层级就够用了。

3.4 时间片轮转

当多个任务优先级相同时,抢占式调度就不管用了。这时候需要时间片轮转——每个任务轮流用CPU,每人用一小段时间。

时间片轮转就像大家轮流用一台打印机。每人打印一页,然后换下一个人。如果某个人打印时间太长,其他人就得等着。

FreeRTOS中配置时间片轮转很简单:

// 创建两个相同优先级的任务
xTaskCreate(task1, "Task1", 2048, NULL, 2, NULL);
xTaskCreate(task2, "Task2", 2048, NULL, 2, NULL);

// 启动调度器
vTaskStartScheduler();

这两个任务优先级都是2,系统会自动分配时间片。默认每个时间片是1个系统时钟节拍(通常1ms)。

调度方式 适用场景 优点 缺点
抢占式 任务优先级差异明显 实时性好 可能导致低优先级任务饿死
时间片轮转 同优先级任务公平使用CPU 公平性高 切换开销大

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

在实际项目中,我一般这样选择调度策略:

  • 紧急任务(如报警处理、紧急停止):用高优先级,抢占式调度
  • 周期性任务(如数据采集、状态刷新):用中等优先级,配合延时阻塞
  • 后台任务(如日志记录、统计计算):用低优先级,时间片轮转

经验之谈:我建议你在设计初期就把任务的优先级和调度策略画出来。别等到代码写完了再改,那会儿改调度策略,牵一发而动全身。我曾经在一个项目里,就因为后期改了一个任务的优先级,导致三个任务都出了问题,调试了两天才搞定。

3.6 调试任务状态的小技巧

调试任务状态,我常用两个方法:

  1. 打印任务状态:用 vTaskList() 函数,可以列出所有任务的状态、优先级、堆栈使用情况
  2. 使用调试器:ESP32的调试器可以直接查看任务控制块(TCB),里面包含了任务的所有状态信息
// 打印所有任务状态
void printTaskStatus() {
    char taskList[500];
    vTaskList(taskList);
    printf("Task Name\tState\tPrio\tStack\tNum\n");
    printf("%s", taskList);
}

输出结果大概长这样:

Task Name    State    Prio    Stack    Num
Task1        R        2       128      1
Task2        B        2       128      2
IDLE         R        0       64       3

看到没?Task1是运行态(R),Task2是阻塞态(B),空闲任务是就绪态(R)。一眼就能看出系统在干什么。

小提示:调试时如果发现某个任务一直处于阻塞态,八成是它等待的资源没释放。我曾经排查过一个bug,一个任务等了三天都没等到信号量,最后发现是另一个任务忘记释放了。这种问题用 vTaskList() 一眼就能看出来。

好了,任务状态和调度就讲到这里。下节课咱们聊聊任务间通信,那又是另一个有意思的话题了。