
RTOS设计与开发(3):阻塞态与就绪态管理:谁该运行,谁该等待,谁该被唤醒
在学完 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 管理提供“调度成立的前提”。