LK 博客
RTOS设计与开发(3):阻塞态与就绪态管理:谁该运行,谁该等待,谁该被唤醒
嵌入式
约 1 分钟阅读 0 赞 0 条评论 鸿蒙黑体

RTOS设计与开发(3):阻塞态与就绪态管理:谁该运行,谁该等待,谁该被唤醒

Yukikaze
Yukikaze @Yukikaze
累计点赞 0 登录后每个账号只能点一次
内容长度 0 正文词元数
正文
目录会跟随阅读位置移动。
阅读进度

在学完 Cortex-M 异常模型、PendSV 任务切换之后,很容易产生一种错觉:

RTOS 不就是“保存一下现场,再恢复另一个任务的现场”吗?

其实不是。

上下文切换只是执行层面的动作,RTOS 更本质的能力是:管理任务状态。

也就是说,内核必须始终回答这些问题:

  • 当前哪些任务可以运行?
  • 哪些任务暂时不能运行?
  • 它们分别在等什么?
  • tick 到来时谁该苏醒?
  • 资源可用时谁该先被唤醒?
  • 被唤醒的任务是否应该立刻抢占当前任务?

所以,真正让 RTOS 成为 RTOS 的,不只是 PendSV,而是:

任务状态流转 + 内核数据结构管理 + 调度决策

这一部分的核心,就是 Ready(就绪态)Blocked(阻塞态) 的组织和转换。

一、任务状态的核心含义

先抓住三个最重要的状态:

1. Running

任务当前正在 CPU 上运行。

2. Ready

任务具备运行资格,但当前还没轮到它。

3. Blocked

任务当前不能运行,因为它正在等待某个条件成立。

这里最关键的理解是:

Ready = 能跑,但暂时没上 CPU Blocked = 暂时不能跑,因为还在等条件

所以:

  • Ready 任务之间竞争 CPU
  • Blocked 任务不参与 CPU 竞争

二、RTOS 的真正核心:不是“会切换”,而是“会管理”

PendSV 解决的是:

怎么切

而调度与状态管理解决的是:

为什么切、切给谁

因此三者分工应该这样理解:

  • 状态管理:维护任务处于 Ready 还是 Blocked
  • 调度器:从 Ready 任务里选出下一任务
  • PendSV:执行真正的上下文切换

可以压缩成一句话:

状态管理决定谁有资格运行,调度器决定该轮到谁,PendSV 负责真正把 CPU 切过去。

三、为什么 Ready 往往统一管理,而 Blocked 往往分散管理

1. Ready 的共同点非常明确

所有 Ready 任务都满足同一个条件:

它们现在都可以运行

所以它们可以统一放进 ready 结构里,由调度器按优先级和时间片统一处理。

2. Blocked 的共同点只到“暂时不能运行”为止

虽然 Blocked 任务都不能运行,但它们等待的条件完全不同:

  • 有的在等时间到
  • 有的在等信号量
  • 有的在等消息到来
  • 有的在等队列空出空间
  • 有的在等互斥锁释放

所以更本质的区别是:

Blocked 任务的唤醒条件不同

因此它们通常不会统一塞进一个总 blocked 队列,而是按等待原因分散挂到不同结构里。

例如:

  • 等时间 → delay list
  • 等信号量 → semaphore wait list
  • 等消息 → queue recv wait list
  • 等队列可写 → queue send wait list

所以这句话非常值得记:

Ready 按“能不能运行”统一管理;Blocked 按“在等什么”分散管理。

四、Ready 区的组织:优先级 + 同优先级轮转

一个成熟 RTOS 的 ready 管理,通常不是“一张总链表”,而是:

  • 每个优先级一条 ready list
  • 再配一个 ready bitmap

也就是:

  • ready_list[prio]
  • ready_bitmap

1. 为什么每个优先级一条链表

因为任务优先级不同,调度器首先要解决的问题是:

当前最高优先级的 Ready 任务是谁?

如果所有任务混在一张链表里,就得不断遍历比较,效率很差。 按优先级拆开后,组织会更清晰。

2. 为什么还要位图

如果只有“每个优先级一条链表”,调度器仍然要一档一档检查:

  • 最高优先级链表是不是空?
  • 这一档是不是空?
  • 下一档是不是空?

而 ready bitmap 的作用就是:

快速标记哪些优先级当前非空

这样调度器就可以快速定位当前最高的非空优先级,而不用一条条链表挨个试。

所以:

  • ready list:组织同优先级任务
  • ready bitmap:快速定位最高优先级的就绪组

3. 同优先级任务怎么排

同优先级任务通常采用:

  • FIFO 组织
  • 配合 时间片轮转(Round-Robin)

经典行为是:

  • 新进入 Ready 的任务 → 插到队尾
  • 当前应该运行的任务 → 取队头
  • 时间片到了但未阻塞 → 挪到队尾

比如优先级 2 的 ready list:

A -> B -> C

如果 A 时间片到了但没有阻塞,则变成:

B -> C -> A

所以可以总结为:

不同优先级之间按优先级竞争,同优先级之间按 FIFO/时间片轮转共享 CPU。

五、Blocked 的第一类:时间阻塞(delay)

最典型的阻塞原因之一是:

  • delay(10)
  • sleep(10 ticks)

这类阻塞不是在等资源,而是在等“时间到”。

1. 调用 delay 时,内核做什么

当任务执行 delay(10) 时,内核最关键的动作有两个:

  • 从 ready 结构移出
  • 挂到 delay list,并记录唤醒时刻

例如:

当前系统 tick 为:

system_tick = 100;

任务调用:

delay(10);

那么内核会计算:

wake_tick = 110;

然后:

  • 从 ready list 中删除该任务
  • 在 TCB 中记录 wake_tick
  • 挂入 delay list

2. SysTick 如何参与唤醒

以后每来一次 SysTick,中断处理里会:

system_tick++;

然后检查 delay list 中是否有任务到期:

  • wake_tick <= system_tick
  • 说明该任务该醒了

于是:

  • 从 delay list 中移出
  • 放回 ready list

注意:

被唤醒,不等于立刻运行

它只是先回到 Ready,随后还要由调度器决定是否立刻切换过去。

3. delay list 为什么常按唤醒时刻排序

如果 delay list 是无序的,那么每次 tick 到来都可能要扫描整张表。 这样 tick 中断开销会随着阻塞任务数量增加而变大。

因此更成熟的设计通常会让 delay list:

按唤醒时刻排序

这样每次 tick 只需重点看表头:

  • 表头到了 → 唤醒
  • 表头没到 → 后面更不可能到,可以停止检查

这样 tick 处理更高效,也更符合 RTOS “快进快出”的中断处理要求。

六、Blocked 的第二类:等待资源或事件

除了 delay,任务更常见的阻塞来源是:

  • 等信号量
  • 等互斥锁
  • 等消息队列
  • 等事件标志

这类任务的共同点是:

它们不是在等时间,而是在等“某个条件成立”

所以它们通常挂到“对象自己的 wait list”上。

七、信号量:同步与资源计数

1. take 成功的情况

如果任务执行:

sem_take(sem);

而此时:

sem->count > 0

那么内核直接:

  • count--
  • 任务继续运行

2. take 失败的情况

如果:

sem->count == 0

那任务就拿不到资源,必须阻塞。内核会:

  • 从 ready list 移出任务
  • 挂到 sem->wait_list
  • 调度其他 Ready 任务运行

3. give 时做什么

当别的任务或中断执行:

sem_give(sem);

内核会先看:

  • 有没有任务正在 sem->wait_list 上等

如果有,通常优先做的不是单纯 count++,而是:

  • 从 wait list 中取出等待者
  • 将其放回 ready list
  • 必要时触发调度

所以 semaphore 的内核逻辑可以概括为:

拿不到就阻塞,资源一来就唤醒等待者。

八、消息队列:通信 + 同步

Queue 比 semaphore 更复杂,因为它不只管理“有/没有”,还管理“数据内容”。

1. queue_receive

如果队列非空:

  • 直接取消息
  • 任务继续运行

如果队列为空:

  • 当前任务阻塞
  • 挂到 recv wait list

2. queue_send

如果队列未满:

  • 消息入队
  • 发送任务继续运行

如果队列已满:

  • 发送任务阻塞
  • 挂到 send wait list

所以 queue 通常有两类等待者:

  • 接收等待队列:队列空,收的人等
  • 发送等待队列:队列满,发的人等

这就是为什么说:

Queue 一头连着数据存储,一头连着任务同步。

3. 为什么消息到来时会唤醒接收者

如果某个任务正在 recv wait list 里等消息,这时别的任务执行 queue_send() 成功送入消息,内核最关键的动作就是:

把等待 queue_receive() 的任务从 recv wait list 中移出,放回 ready list

之后如果这个被唤醒任务优先级更高,就可能立刻触发新一轮调度。

九、唤醒 ≠ 立刻运行

这是第三部分特别容易混淆的一点。

很多初学者会把:

  • 从 wait list 中出来
  • 立即切到该任务运行

看成一件事

其实不是。

正确的两步是:

1. 唤醒

说明任务:

  • 不再 blocked
  • 恢复了运行资格
  • 被放回 ready list

2. 调度

说明调度器重新比较优先级,决定:

  • 是否立刻切过去
  • 还是先让当前任务继续跑

所以要牢牢记住:

  • Blocked → Ready 是状态变化
  • Ready → Running 是调度决策

例如:

  • 高优先级任务被唤醒 → 可能马上抢占
  • 低优先级任务被唤醒 → 先进入 Ready 排队

十、wait list 为什么也常按优先级组织

很多 RTOS 不仅 ready list 按优先级管理,连 wait list 也常按优先级组织。

原因是:

如果 wait list 只按 FIFO,而不看优先级,就会出现这种问题:

  • 低优先级任务先等待
  • 高优先级任务后等待
  • 资源可用时却先唤醒低优先级任务

这会削弱 RTOS 的实时性,因为:

高优先级任务无法优先获得服务

所以 wait list 常见策略是:

优先级优先,同优先级再按 FIFO

也就是:

  • 先唤醒最高优先级等待者
  • 若多个等待者优先级相同,再按先来先到处理

十一、mutex 为什么不能简单等同于 1 值 semaphore

表面上看,binary semaphore 和 mutex 很像:

  • 都可以“拿”
  • 都可以“等”
  • 都可以“释放”

但内核语义并不相同。

1. Mutex 有 owner

lock,谁就是锁的持有者。 以后必须由这个持有者自己 unlock

也就是说:

谁加锁,谁解锁

2. Semaphore 关注的是计数与同步

Semaphore 更强调:

  • 资源数值变化
  • 事件通知
  • 计数许可

它通常不强调“这个对象现在归谁”。

3. Mutex 能处理优先级反转

在 RTOS 中,mutex 常带有:

优先级继承(Priority Inheritance)

当高优先级任务等待一个由低优先级任务持有的 mutex 时,内核会临时提升低优先级任务的优先级,让它尽快释放锁。

这正是 mutex 比 binary semaphore 更专业的地方。

所以:

  • Semaphore 用于同步与资源计数;Mutex 用于互斥与临界区保护。
  • 二值信号量虽然表面像锁,但并不等同于 mutex。

十二、一个完整流程

现在可以把第三部分整个流程用一条线串起来了:

场景:任务 A 等待资源,任务 B 释放资源

  • 任务 A 正在运行
  • A 申请某个资源失败
  • A 从 ready list 移出
  • A 挂到对应对象的 wait list
  • 调度器选择别的 Ready 任务运行
  • 后来任务 B 释放资源 / 发送消息 / give 信号量
  • 内核从 wait list 中取出等待任务 A
  • A 回到 ready list
  • 调度器重新比较优先级
  • 如果 A 优先级更高,则触发一次 PendSV
  • PendSV 保存当前任务上下文,恢复 A 的上下文
  • A 开始继续运行

所以这一整套真正体现的是:

资源事件驱动任务状态流转,调度器依据优先级决定运行权,PendSV 执行最终切换。

十三、和前面异常模型、PendSV 任务切换的关系

现在你可以把前三块知识完整串起来了:

第一部分:异常模型

解决的是:

CPU 如何从 Thread mode 进入 Handler mode 如何自动压栈、异常返回

第二部分:PendSV 任务切换

解决的是:

任务上下文如何保存与恢复 TCB 中的 PSP 如何参与任务切换

第三部分:阻塞态与就绪态管理

解决的是:

  • 谁现在能运行
  • 谁必须等待
  • 谁什么时候苏醒
  • 苏醒后是否立刻获得 CPU

所以说:

异常模型提供“进入内核的机制”,PendSV 提供“切换任务的机制”,而 Ready/Blocked 管理提供“调度成立的前提”。

作者名片

Yukikaze
Yukikaze
@Yukikaze

私にできることなら何でもするから

评论区
文章作者和管理员都可以管理这里的评论。
0 条评论
登录后即可参与评论。 去登录
还没有评论,欢迎留下第一条交流内容。