第三讲:二进制信号量——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 避坑指南

我这些年踩过的坑,分享给你们:

  1. 忘记释放:最常见的问题。特别是在有多个return路径的函数里。
  2. 中断里用semGive:可以,但中断里不能用semTake(会阻塞)。
  3. 任务删除前没释放:任务被删除时,持有的信号量不会自动释放。
  4. 优先级反转:低优先级任务拿着锁,高优先级任务等锁,中间优先级的任务抢了CPU。嗯,这个问题后面会专门讲。

我的经验: 写代码时,先写semGive再写semTake。这样就不会忘记释放了。你试试看,这个习惯帮我避免了很多bug。

3.6 实战建议

最后,给几个实际项目中的建议:

  • 能用NO_WAIT就别用WAIT_FOREVER
  • 临界区代码要短,别在锁里做耗时操作
  • 信号量命名要有意义,别用sem1、sem2这种
  • 调试时可以用semShow()查看信号量状态

好了,二进制信号量就讲这么多。下一节我们聊互斥锁,看看它和二进制信号量到底有什么区别。