3. 信号量基础:二值信号量、计数信号量、互斥信号量的概念与区别
好,咱们今天聊聊信号量。说实话,信号量是VxWorks里最基础、也是最常用的任务间通信手段。我刚开始接触嵌入式实时系统那会儿,总觉得信号量不就是个锁吗?后来踩了不少坑才明白——信号量远不止“锁”这么简单。
它本质上是一个资源管理计数器。你想想看,多个任务要抢着用同一个资源,比如一个串口、一块共享内存,总得有个规矩吧?信号量就是那个“交通警察”。
3.1 二值信号量:最简单的开关
二值信号量,说白了就是只有两个状态:0 和 1。0表示资源被占用了,1表示资源空闲。它就像一个简单的开关——要么开,要么关。
核心特点:
- 值只能是0或1
- 主要用于任务同步,而不是资源保护
- 没有“优先级继承”机制
我在项目中遇到过这样一个场景:一个数据采集任务,采集完数据后要通知处理任务去干活。这时候用二值信号量就特别合适。采集任务做完后semGive(),处理任务在semTake()上等着。简单、高效。
// 创建二值信号量
SEM_ID semBinary = semBCreate(SEM_Q_FIFO, SEM_EMPTY);
// 任务A:数据采集
void taskCollector(void) {
while (1) {
collectData(); // 采集数据
semGive(semBinary); // 通知处理任务
}
}
// 任务B:数据处理
void taskProcessor(void) {
while (1) {
semTake(semBinary, WAIT_FOREVER); // 等待通知
processData(); // 处理数据
}
}
我的小建议:二值信号量最适合做“事件通知”。如果你只是想告诉另一个任务“活干完了”,用它准没错。但千万别用它来保护共享资源——后面我会解释为什么。
3.2 计数信号量:资源池的管理员
计数信号量,你可以把它想象成一个停车场计数器。停车场有10个车位,每进来一辆车,计数器减1;每出去一辆车,计数器加1。当计数器为0时,后来的车就得等着。
它的值可以是0到最大值之间的任意整数。这跟二值信号量最大的区别——它能管理多个同类资源。
核心特点:
- 值可以是0到设定的最大值
- 用于管理多个相同的资源
- 同样没有优先级继承
我记得有个项目,系统里有5个DMA通道,多个任务都要用。如果不用计数信号量,你得自己写个链表来管理空闲通道,麻烦不说还容易出bug。用计数信号量,几行代码就搞定了。
// 创建计数信号量,初始有5个资源
SEM_ID semDma = semCCreate(SEM_Q_PRIORITY, 5);
// 申请DMA通道
void useDma(void) {
semTake(semDma, WAIT_FOREVER); // 拿到一个通道
// ... 使用DMA ...
semGive(semDma); // 释放通道
}
注意:计数信号量管理的资源必须是完全等价的。比如5个DMA通道,你用哪个都一样。如果资源有区别(比如通道0有特殊功能),那就不能用计数信号量了。
3.3 互斥信号量:带“优先级继承”的守护者
互斥信号量,这才是真正用来保护共享资源的。它跟二值信号量看起来很像——也是只有0和1两个状态。但它的内部机制要复杂得多。
最关键的一点:互斥信号量支持优先级继承。这是什么意思?
假设有个低优先级任务拿着锁,一个高优先级任务来抢锁,结果被阻塞了。如果没有优先级继承,中间优先级的任务就会插队,导致高优先级任务一直等——这就是经典的优先级反转问题。
互斥信号量会临时把低优先级任务的优先级提升到高优先级任务的级别,让它赶紧把锁释放掉。嗯,这个机制很巧妙。
核心特点:
- 值只能是0或1
- 支持优先级继承,防止优先级反转
- 只能由持有信号量的任务释放(谁拿谁放)
- 支持递归获取(同一个任务可以多次take)
// 创建互斥信号量
SEM_ID semMutex = semMCreate(SEM_Q_PRIORITY | SEM_INVERSION_SAFE);
// 保护共享资源
void accessSharedData(void) {
semTake(semMutex, WAIT_FOREVER);
// 操作共享数据...
semGive(semMutex); // 必须由同一个任务释放
}
我的经验:保护共享资源,永远优先用互斥信号量。我曾经在一个项目里图省事用了二值信号量保护共享内存,结果高优先级任务被堵了整整200ms。后来换成互斥信号量,问题立刻解决。优先级继承这个特性,关键时刻能救命。
3.4 三种信号量的对比
咱们来做个对比,这样你心里更有数:
| 特性 | 二值信号量 | 计数信号量 | 互斥信号量 |
|---|---|---|---|
| 取值范围 | 0 或 1 | 0 ~ 最大值 | 0 或 1 |
| 主要用途 | 任务同步、事件通知 | 管理多个同类资源 | 保护共享资源 |
| 优先级继承 | 不支持 | 不支持 | 支持 |
| 递归获取 | 不支持 | 不支持 | 支持 |
| 谁可以释放 | 任何任务 | 任何任务 | 只能由持有者释放 |
| 创建函数 | semBCreate() | semCCreate() | semMCreate() |
3.5 避坑指南
讲到这里,我得说说我踩过的几个坑:
我曾经犯过的错:
- 用二值信号量保护共享资源——结果遇到优先级反转,系统响应时间变得不可预测。记住:保护资源,用互斥信号量。
- 在中断服务程序里用semTake()——中断里不能阻塞,一用就死机。中断里只能semGive()。
- 忘记释放信号量——任务异常退出时没释放锁,其他任务永远等下去。建议用taskSafe/taskUnsafe保护临界区。
其实选哪种信号量,你只要问自己三个问题:
- 我是要通知别人干活,还是要保护数据?→ 通知用二值,保护用互斥
- 资源是不是有多个副本?→ 是就用计数信号量
- 会不会有优先级反转的风险?→ 有就用互斥信号量
想清楚这三点,基本就不会选错了。好了,信号量的基础就讲到这里。下一节咱们聊聊信号量在实际项目中的高级用法,包括死锁检测和超时处理——这些可都是实战中经常遇到的硬骨头。