
RTOS设计与开发(4):信号量、互斥锁、消息队列:同步、互斥与通信的内核视角
在学完异常模型、PendSV 任务切换、Ready/Blocked 状态管理之后,RTOS 的骨架已经搭起来了。
但一个真正可用的 RTOS,不能只会“切任务”,还必须解决三个更实际的问题:
- 任务之间怎么同步
- 多个任务访问共享资源时怎么互斥
- 任务之间怎么传递数据
这正是:
- Semaphore
- Mutex
- Queue
要解决的三类核心问题。
所以这一部分的重点,不只是记 API 名字,而是要看明白:
它们分别在解决什么问题,任务为什么会阻塞在它们上面,内核又是如何唤醒等待者的。
一、先给三者一个总定位
1. Semaphore
解决的是:
“条件是否成立 / 资源是否可用 / 事件是否发生”
它最像“许可证”或“通知信号”。
2. Mutex
解决的是:
“某个共享资源同一时刻只能被一个任务访问”
它最像“锁”或“独占钥匙”。
3. Queue
解决的是:
“一个任务把数据交给另一个任务”
它最像“邮箱”或“消息通道”。
所以可以先记一句最简版:
Semaphore 管同步,Mutex 管互斥,Queue 管通信。
二、Semaphore:同步与资源计数
1. Semaphore 的本质
信号量本质上是一个带计数值的同步对象。
你可以把它理解成:
-
count > 0:还有可用许可 -
count == 0:当前没有许可 -
一个任务去
take,就是申请一个许可; -
一个任务或中断去
give,就是归还一个许可或发出一个通知。
2. Semaphore 最常见的两种用途
用途一:资源计数
比如系统里有 3 个可用缓冲区:
sem_init(count = 3);
每拿一个:
sem_take();
计数减一;每归还一个:
sem_give();
计数加一。
这时 Semaphore 表示的是:
可用资源的数量
用途二:事件同步
比如 ISR 完成了一次 DMA 传输后,通知任务“数据准备好了”:
- 任务先
sem_take(),等事件 - ISR 完成后
sem_give()
这时 Semaphore 表示的是:
某个事件发生了
所以 Semaphore 既能做“资源计数”,又能做“事件同步”。
3. Semaphore 的内核行为
sem_take()
任务调用:
sem_take(sem);
内核先看:
sem->count > 0 吗?
如果大于 0
说明许可可用:
count--- 当前任务继续运行
如果等于 0
说明当前拿不到:
- 当前任务从 ready list 移出
- 挂到
sem->wait_list - 进入 Blocked
- 调度别的任务运行
sem_give()
当别的任务或 ISR 调用:
sem_give(sem);
内核会先看:
sem->wait_list里有没有等待者?
如果没有等待者
count++
如果有等待者
通常会:
- 从 wait list 中取出等待任务
- 把它放回 ready list
- 必要时触发调度
所以 Semaphore 的抽象模型很清楚:
拿不到就阻塞,条件成立就唤醒。
4. Semaphore 的特点
Semaphore 强调的是:
- 有无
- 数量
- 事件通知
但它一般不强调:
- “这个对象当前属于谁”
这点和 Mutex 有本质区别。
三、Mutex:互斥与临界区保护
1. Mutex 的本质
Mutex 解决的是:
多个任务不能同时进入同一个临界区
比如几个任务都要访问:
- 全局链表
- 串口发送缓冲区
- 文件系统对象
- 某段共享变量
- 某个外设寄存器操作序列
这时候不能只靠“希望大家别冲突”,必须显式加锁。
2. Mutex 的基本语义
mutex_lock()
任务申请锁:
mutex_lock(m);
如果锁空闲
- 当前任务拿到锁
- 记录
owner = 当前任务 - 继续运行
如果锁被占用
- 当前任务拿不到锁
- 从 ready list 移出
- 挂到
m->wait_list - 阻塞等待
mutex_unlock()
当前持有锁的任务执行:
mutex_unlock(m);
内核会:
- 释放锁
- 看
wait_list是否有人等待 - 如果有,唤醒一个等待者
- 将它放回 ready list
- 必要时触发调度
3. Mutex 最关键的语义:owner
Mutex 和 Semaphore 的最本质区别之一是:
Mutex 有 owner,Semaphore 通常没有 owner
也就是说,Mutex 会明确知道:
- 当前是谁持有这把锁
于是有一个重要规则:
谁加锁,谁解锁
这和 Semaphore 很不一样。 Semaphore 更像是“资源数值变化”,而 Mutex 更像是“独占关系”。
4. 为什么 Mutex 不能简单等同于 1 值 Semaphore
binary semaphore 和 mutex 看起来很像:
- 都像只有一个资源
- 都可能阻塞等待
- 都可能唤醒别人
但它们内核语义并不一样。
Semaphore 更关心
count- 事件是否发生
- 资源是否可用
Mutex 更关心
- 当前持有者是谁
- 共享资源是否被独占
- 临界区是否被正确保护
所以:
binary semaphore 可以做出“类似互斥”的效果,但它不是严格意义上的 mutex。
5. Mutex 为什么更专业:优先级继承
这就是 Mutex 在 RTOS 里最“像样”的地方。
优先级反转问题
假设:
- L:低优先级任务
- M:中优先级任务
- H:高优先级任务
流程如下:
- L 先拿到了 mutex
- H 来申请这个 mutex,拿不到,被阻塞
- M 恰好一直在运行
这时会出现一个很糟糕的现象:
- H 虽然优先级最高
- 但它在等 L 释放锁
- L 又因为 M 抢占而迟迟运行不到解锁那一步
于是:
高优先级任务 H 被中优先级任务 M 间接拖住了
这就是经典的 priority inversion(优先级反转)。
优先级继承怎么解决
RTOS 的 Mutex 往往支持:
Priority Inheritance(优先级继承)
也就是:
- H 在等 L 手里的锁
- 内核临时把 L 的优先级提升到 H 附近
- 让 L 先把临界区跑完
- 尽快 unlock
- 然后恢复 L 原本优先级
所以:
优先级继承几乎是 Mutex 的标志性能力,而不是普通 Semaphore 的典型能力。
四、Queue:通信与同步的结合体
1. Queue 的本质
Queue 不只是“通知一下”,而是:
把一份数据从发送者交给接收者
比如:
- 采集任务把传感器数据发给处理任务
- ISR 把接收到的数据包交给协议栈任务
- 按键任务把事件发给 UI 任务
Queue 解决的是:
任务之间如何安全、有序地传递消息
2. Queue 为什么比 Semaphore 更复杂
因为 Queue 同时要管理两类东西:
1)数据缓冲区
队列里真的要存放消息内容。
2)等待任务
有人在等着:
- 发
- 收
所以 Queue 通常不止一个等待队列,而是至少两个:
send wait listrecv wait list
3. queue_receive()
任务执行:
queue_receive(q, &msg);
如果队列非空
- 直接取出消息
- 当前任务继续运行
如果队列为空
- 当前任务拿不到消息
- 从 ready list 移出
- 挂到
q->recv_wait_list - 进入 Blocked
所以:
队列空时,接收者阻塞
4. queue_send()
任务执行:
queue_send(q, msg);
如果队列未满
- 消息成功入队
- 当前任务继续运行
如果队列已满
- 当前任务无法再发送
- 从 ready list 移出
- 挂到
q->send_wait_list - 进入 Blocked
所以:
队列满时,发送者阻塞
5. Queue 最重要的唤醒逻辑
场景一:有人在等接收
如果某个任务已经因为 queue_receive() 阻塞在 recv_wait_list 上,这时别的任务 queue_send() 成功送入消息,内核最关键的动作就是:
- 把等待接收的任务从
recv_wait_list中移出 - 放回 ready list
- 必要时重新调度
也就是说:
消息一到,就尽快唤醒接收者
场景二:有人在等发送
如果某个任务已经因为队列满而阻塞在 send_wait_list 上,这时别的任务 queue_receive() 取走了一条消息,队列腾出空间,内核通常会:
- 从
send_wait_list中取出一个等待发送者 - 把它放回 ready list
- 让它有机会继续发送
所以 Queue 的内核逻辑比 Semaphore 更完整:
它要同时管理“消息是否存在”和“空间是否可用”。
五、三者的核心区别总结
现在把三者横着看,会清楚很多。
1. Semaphore:同步 / 通知 / 资源计数
它回答的问题是:
- 事件到了吗?
- 资源可用吗?
- 还有几个许可?
它强调的是:
- 数量
- 条件
- 通知
2. Mutex:互斥 / 独占 / 临界区保护
它回答的问题是:
- 这把锁现在归谁?
- 共享资源现在能不能进?
- 谁在临界区里?
它强调的是:
- owner
- 独占
- 优先级继承
3. Queue:带数据的通信
它回答的问题是:
- 有没有消息?
- 消息内容是什么?
- 发送方和接收方如何同步?
它强调的是:
- 数据传递
- 有序缓存
- 发送/接收双方阻塞与唤醒
4. 一句最简对比
Semaphore 传“有没有”,Mutex 管“归谁用”,Queue 传“是什么”。
六、从 Blocked/Ready 角度看三者
1. 等 Semaphore
任务在等:
许可 / 资源 / 事件
挂到:
sem->wait_list
2. 等 Mutex
任务在等:
锁释放
挂到:
mutex->wait_list
3. 等 Queue
任务可能在等两种条件:
等收
- 队列里来消息
- 挂到
recv_wait_list
等发
- 队列里腾出空间
- 挂到
send_wait_list
所以 Queue 比 Semaphore / Mutex 更复杂的一个直观原因就是:
它天然就可能有两类等待者。
七、为什么被唤醒后不一定立刻运行
这个点在同步原语上也必须继续保持清醒。
当某个任务因为:
sem_give()mutex_unlock()queue_send()queue_receive()
而被唤醒时,它并不是直接 Running,而是:
- 从 wait list 中移出
- 回到 ready list
- 再由调度器比较优先级
所以:
唤醒只是恢复运行资格,不是直接拿到 CPU
如果被唤醒任务优先级更高,就可能触发 PendSV; 如果不更高,就先在 Ready 中排队。
八、ISR 场景下三者怎么理解
这个问题很常见,也很适合你现在这个阶段理解。
1. ISR 通知任务“完成了”
更像:
- Semaphore
- 或 event flag
因为本质是“事件同步”,而不是保护临界区。
2. ISR 不仅通知“完成了”,还要把数据交给任务
更像:
- Queue
因为已经不是简单通知,而是:
通知 + 传数据
3. Mutex 一般不适合拿来做 ISR 通知
因为 Mutex 强调:
- owner
- 谁 lock 谁 unlock
- 临界区保护
而 ISR 做的往往是:
- 投递事件
- 投递数据
- 唤醒等待任务
所以这类语义更像 Semaphore / Queue,而不是 Mutex。
九、什么时候用谁
1. 只想通知“条件成立了”
比如:
- DMA 完成
- 按键中断到了
- 某硬件事件发生
优先想:
- Semaphore
2. 要保护共享变量、共享链表、共享设备
比如:
- 多任务访问同一个 UART 输出
- 多任务改同一个全局结构
- 多任务访问同一外设驱动状态
优先想:
- Mutex
3. 要把数据从 A 交给 B
比如:
- 采样数据
- 按键消息
- 网络包
- 日志消息
优先想:
- Queue
4. 一个简单记忆
通知用 Semaphore,保护用 Mutex,传数据用 Queue。
十、把三者放到一条总线上理解
现在可以把它们和前面的调度、阻塞、唤醒完整串起来了:
1. 任务执行某个获取操作
比如:
sem_takemutex_lockqueue_receive
2. 内核先判断条件是否满足
如果满足:
- 直接成功
- 继续运行
如果不满足:
- 从 Ready 中移出
- 挂到对象的 wait list
- 进入 Blocked
3. 某个释放/投递操作发生
比如:
sem_givemutex_unlockqueue_send
4. 内核唤醒等待者
- 从 wait list 中移出等待任务
- 放回 ready list
- 比较优先级
- 必要时触发调度与 PendSV
所以这一部分和第三部分不是分开的,而是完美衔接的:
同步原语决定“任务在等什么”,调度器决定“谁该先跑”,PendSV 决定“如何真正切过去”。