4、二值信号量详解:创建、获取、释放、典型应用(任务同步)
好,咱们今天来聊聊二值信号量。说实话,这是VxWorks里最基础、也最常用的同步机制之一。很多刚入行的朋友容易把它跟互斥信号量搞混,其实它们俩的用途完全不同。二值信号量,说白了就是一个“开关”,要么是满的(可用),要么是空的(不可用)。它不像计数信号量那样能数数,也不像互斥信号量那样有优先级继承。
我个人习惯把二值信号量想象成一个“令牌”。谁拿到了令牌,谁就能干活。干完了,把令牌还回来。就这么简单。
4.1 二值信号量的本质
二值信号量在VxWorks内部其实是一个结构体,包含了一个计数值和一个等待任务队列。计数值只有两种状态:0和1。0表示信号量被占用,1表示信号量可用。
我在项目中遇到过一个问题:一个任务在等待信号量时,另一个高优先级任务突然插进来,导致低优先级任务一直拿不到信号量。这就是典型的优先级反转。不过二值信号量本身不解决这个问题,那是互斥信号量的活儿。
核心要点:二值信号量只用于同步,不用于保护共享资源。如果你需要保护临界区,请用互斥信号量。
4.2 创建二值信号量
创建二值信号量用 semBCreate() 函数。它的原型长这样:
SEM_ID semBCreate(
int options, // 信号量选项
SEM_B_STATE initialState // 初始状态:SEM_FULL 或 SEM_EMPTY
);
options 参数有两个常用值:
SEM_Q_FIFO:等待任务按先进先出顺序排队SEM_Q_PRIORITY:等待任务按优先级排队
嗯,这里要注意。如果你用 SEM_Q_PRIORITY,高优先级任务会插队。这在某些实时场景下是必要的,但也会带来不确定性。我建议除非你有明确的优先级需求,否则用 SEM_Q_FIFO 更稳妥。
initialState 参数:
SEM_FULL:初始可用(计数值为1)SEM_EMPTY:初始不可用(计数值为0)
举个例子:
SEM_ID mySem;
/* 创建一个初始可用的二值信号量,FIFO排队 */
mySem = semBCreate(SEM_Q_FIFO, SEM_FULL);
if (mySem == NULL) {
printf("创建信号量失败!\n");
return ERROR;
}
我曾经犯过一个低级错误:创建信号量时忘了检查返回值。结果程序跑起来后,任务一直卡在等待信号量的地方,排查了半天才发现是信号量没创建成功。所以,创建完一定要检查返回值,这是血的教训。
4.3 获取二值信号量
获取信号量用 semTake() 函数:
STATUS semTake(
SEM_ID semId, // 信号量ID
int timeout // 超时时间,单位:tick
);
timeout 参数:
WAIT_FOREVER:一直等,直到拿到信号量NO_WAIT:不等待,立即返回- 其他正数:等待指定的 tick 数
看个实际用法:
/* 等待信号量,最多等100个tick */
if (semTake(mySem, 100) == OK) {
/* 拿到信号量了,开始干活 */
printf("任务A开始执行...\n");
taskDelay(10);
/* 干完活,释放信号量 */
semGive(mySem);
} else {
printf("等待信号量超时!\n");
}
小技巧:在中断服务程序中,绝对不能调用 semTake() 并设置 WAIT_FOREVER。中断上下文不能阻塞。如果你需要在ISR中获取信号量,只能用 NO_WAIT。
4.4 释放二值信号量
释放信号量用 semGive() 函数:
STATUS semGive(
SEM_ID semId // 信号量ID
);
这个函数很简单,就是把信号量的计数值从0变成1。如果有任务在等待这个信号量,VxWorks会唤醒等待队列中的第一个任务。
释放信号量可以在任务中调用,也可以在中断服务程序中调用。这是二值信号量的一大优势——它可以在ISR和任务之间传递同步信息。
举个例子:
/* 中断服务程序 */
void isrHandler(void)
{
/* 做一些紧急处理 */
...
/* 释放信号量,通知任务来处理剩余工作 */
semGive(mySem);
}
/* 任务 */
void taskEntry(void)
{
while (1) {
/* 等待中断通知 */
semTake(mySem, WAIT_FOREVER);
/* 处理中断遗留的工作 */
printf("处理中断数据...\n");
}
}
这种模式在嵌入式系统中非常常见。中断只做最紧急的事,然后把繁重的处理工作交给任务。你想想看,如果中断里做太多事,会影响系统的实时性。
4.5 典型应用:任务同步
二值信号量最经典的应用就是任务同步。我给大家画个场景:
假设有两个任务:任务A负责采集数据,任务B负责处理数据。任务A采集完数据后,必须通知任务B开始处理。这就是典型的“生产者-消费者”模型。
SEM_ID dataSem;
/* 任务A:数据采集 */
void taskA(void)
{
while (1) {
/* 采集数据 */
collectData();
/* 通知任务B:数据准备好了 */
semGive(dataSem);
/* 继续采集下一批数据 */
taskDelay(100);
}
}
/* 任务B:数据处理 */
void taskB(void)
{
while (1) {
/* 等待任务A的通知 */
semTake(dataSem, WAIT_FOREVER);
/* 处理数据 */
processData();
}
}
void main(void)
{
/* 创建信号量,初始为不可用 */
dataSem = semBCreate(SEM_Q_FIFO, SEM_EMPTY);
/* 创建任务 */
taskSpawn("tA", 100, 0, 2000, (FUNCPTR)taskA, 0,0,0,0,0,0,0,0,0,0);
taskSpawn("tB", 100, 0, 2000, (FUNCPTR)taskB, 0,0,0,0,0,0,0,0,0,0);
}
警告:千万不要在多个任务之间用二值信号量来保护共享资源。比如两个任务同时访问一个全局变量,用二值信号量来“加锁解锁”。这会导致优先级反转、死锁等问题。保护共享资源请用互斥信号量。
4.6 避坑指南
我总结几个常见的坑,都是我曾经踩过的:
- 忘记初始化信号量状态:创建时
SEM_FULL和SEM_EMPTY选错了,导致任务一开始就卡住或者意外执行。 - 在ISR中调用
semTake()并阻塞:中断上下文不能阻塞,否则系统会崩溃。 - 多次释放同一个信号量:二值信号量释放后计数值变成1,再释放一次还是1,不会变成2。但如果你不小心在多个地方调用了
semGive(),可能会导致逻辑错误。 - 忘记检查返回值:创建、获取、释放都要检查返回值,尤其是
semTake()超时的情况。
我记得有一次,一个同事在ISR里调用了 semGive(),然后在任务里用 semTake(WAIT_FOREVER) 等待。看起来没问题,但他忘了考虑一种情况:如果ISR连续触发了两次,而任务只处理了一次,第二次的 semGive() 就白给了。因为二值信号量只能记住一次状态。这就是为什么有些场景需要用计数信号量。
4.7 小结
二值信号量,说白了就是一个同步开关。它最适合的场景是:一个任务等待另一个任务或中断的“信号”。记住三点:
- 创建时选对初始状态
- 获取时注意超时设置
- 释放时别在ISR里阻塞
掌握了这些,二值信号量这块基本就稳了。下一节咱们聊聊计数信号量,它跟二值信号量很像,但能处理更复杂的场景。