第三讲:二进制信号量——semBCreate、semTake、semGive 核心API
各位同学,今天我们聊点实在的——二进制信号量。
说实话,我在VxWorks上摸爬滚打这么多年,用得最多的同步机制就是它。别看它结构简单,就两个状态:满和空。但用好它,能解决80%以上的任务同步问题。
3.1 什么是二进制信号量?
二进制信号量,说白了就是一个只有0和1的计数器。
- 值为1:信号量可用(满状态)
- 值为0:信号量被占用(空状态)
它不像计数信号量那样可以累加。它只有两种状态。嗯,你可以把它想象成一个停车位——要么有车,要么没车。
核心要点: 二进制信号量主要用于任务间的互斥和同步,不是用来计数的。
3.2 创建二进制信号量:semBCreate
先看API原型:
SEM_ID semBCreate
(
int options, /* 信号量选项 */
SEM_B_STATE initialState /* 初始状态 */
);
两个参数,我一个个说。
options 参数
这里有两个常用选项:
| 选项 | 含义 | 适用场景 |
|---|---|---|
| SEM_Q_FIFO | 先进先出排队 | 一般任务同步 |
| SEM_Q_PRIORITY | 按优先级排队 | 高优先级任务优先获取 |
我个人习惯用SEM_Q_FIFO。为什么?因为公平。你想想看,如果高优先级任务一直抢信号量,低优先级任务可能永远拿不到——这就是优先级反转的雏形。
initialState 参数
就两个值:
- SEM_FULL:初始为满(值为1)
- SEM_EMPTY:初始为空(值为0)
怎么选?看用途:
- 做互斥锁:初始化为SEM_FULL
- 做任务同步:初始化为SEM_EMPTY
举个例子:
/* 创建一个互斥用的二进制信号量 */
SEM_ID semMutex = semBCreate(SEM_Q_FIFO, SEM_FULL);
/* 创建一个同步用的二进制信号量 */
SEM_ID semSync = semBCreate(SEM_Q_FIFO, SEM_EMPTY);
小技巧: 创建失败返回NULL。我建议每次创建后都检查返回值,别偷懒。我曾经在一个项目中没检查,结果系统跑着跑着就挂了,查了两天才发现是信号量没创建成功。
3.3 获取信号量:semTake
API原型:
STATUS semTake
(
SEM_ID semId, /* 信号量ID */
int timeout /* 超时时间(tick数) */
);
timeout参数有三种玩法:
- WAIT_FOREVER:死等,直到拿到信号量
- NO_WAIT:不等待,拿不到就返回
- 具体tick数:等一段时间
我一般这样用:
/* 方式1:死等(慎用) */
semTake(semMutex, WAIT_FOREVER);
/* 方式2:尝试获取,拿不到就干别的 */
if (semTake(semMutex, NO_WAIT) == OK) {
/* 拿到锁了,干活 */
} else {
/* 没拿到,做其他事 */
}
/* 方式3:等100个tick */
if (semTake(semMutex, 100) == OK) {
/* 100 tick内拿到了 */
} else {
/* 超时了 */
}
警告: WAIT_FOREVER要小心用。如果持有信号量的任务挂了,其他任务会永远等下去。这就是死锁。我曾经在一个项目中,一个任务异常退出前没释放信号量,结果所有依赖它的任务全部卡死。排查起来特别痛苦。
3.4 释放信号量:semGive
API原型:
STATUS semGive
(
SEM_ID semId /* 信号量ID */
);
就这么简单,一个参数。
但简单不代表可以乱用。有几个原则:
- 谁拿谁放:哪个任务semTake,就由哪个任务semGive
- 成对出现:每个semTake都要有对应的semGive
- 异常处理:在错误路径上也要释放
看个完整例子:
void taskEntry(void)
{
SEM_ID semMutex;
/* 创建信号量 */
semMutex = semBCreate(SEM_Q_FIFO, SEM_FULL);
if (semMutex == NULL) {
printf("创建信号量失败\n");
return;
}
while (1) {
/* 获取信号量 */
if (semTake(semMutex, WAIT_FOREVER) != OK) {
continue;
}
/* 临界区代码 */
printf("进入临界区\n");
/* ... 干活 ... */
printf("离开临界区\n");
/* 释放信号量 */
semGive(semMutex);
}
}
3.5 避坑指南
我这些年踩过的坑,分享给你们:
- 忘记释放:最常见的问题。特别是在有多个return路径的函数里。
- 中断里用semGive:可以,但中断里不能用semTake(会阻塞)。
- 任务删除前没释放:任务被删除时,持有的信号量不会自动释放。
- 优先级反转:低优先级任务拿着锁,高优先级任务等锁,中间优先级的任务抢了CPU。嗯,这个问题后面会专门讲。
我的经验: 写代码时,先写semGive再写semTake。这样就不会忘记释放了。你试试看,这个习惯帮我避免了很多bug。
3.6 实战建议
最后,给几个实际项目中的建议:
- 能用NO_WAIT就别用WAIT_FOREVER
- 临界区代码要短,别在锁里做耗时操作
- 信号量命名要有意义,别用sem1、sem2这种
- 调试时可以用semShow()查看信号量状态
好了,二进制信号量就讲这么多。下一节我们聊互斥锁,看看它和二进制信号量到底有什么区别。