第1章:二进制信号量——创建与删除、获取与释放、典型应用场景(任务同步)

各位同学,咱们今天聊聊二进制信号量。说实话,这是VxWorks里最基础、最常用的同步机制之一。我刚开始接触VxWorks时,第一个深入研究的对象就是它。为什么?因为任务同步是实时系统的命脉,而二进制信号量就是那个把命脉打通的关键工具。

1.1 什么是二进制信号量?

说白了,二进制信号量就是一个只有两个状态的变量:0和1。你可以把它想象成一个停车场的空位指示牌——有车位(1)或者没车位(0)。任务要进去,就得先看看这个牌子。

我个人习惯把二进制信号量比作一把钥匙。谁拿到钥匙,谁就能进入临界区。用完了,把钥匙还回去。就这么简单。

核心要点:二进制信号量本质上是一个布尔标志,用于实现任务间的互斥或同步。它只有两种状态:可用(1)和不可用(0)。

1.2 创建与删除

在VxWorks里,创建二进制信号量用的是semBCreate()函数。这个函数有两个参数:

  • options:信号量的队列选项,决定等待任务按什么顺序排队
  • initialState:初始状态,SEM_FULL(1)或SEM_EMPTY(0)
// 创建一个初始为可用的二进制信号量
SEM_ID semMutex;
semMutex = semBCreate(SEM_Q_PRIORITY, SEM_FULL);

// 创建一个初始为不可用的二进制信号量(用于同步)
SEM_ID semSync;
semSync = semBCreate(SEM_Q_PRIORITY, SEM_EMPTY);

嗯,这里要注意一点。我在项目中遇到过一个问题:创建信号量时忘了检查返回值。结果系统跑着跑着就莫名其妙地卡死了。后来排查了半天,才发现是内存不足导致信号量创建失败。

避坑指南:我曾经因为没检查semBCreate的返回值,导致一个产线设备在运行48小时后突然死机。从那以后,我每次创建信号量都会加一句:

if (semMutex == NULL) {
    logMsg("Failed to create semaphore!\n", 0,0,0,0,0);
    return ERROR;
}

删除信号量用semDelete()。这个函数很简单,但有个坑:如果有任务正在等待这个信号量,删除操作会把这些任务全部唤醒,并返回ERROR。你想想看,这会造成什么后果?

// 删除信号量
status = semDelete(semMutex);
if (status != OK) {
    logMsg("semDelete failed - tasks may be waiting!\n", 0,0,0,0,0);
}

1.3 获取与释放

获取信号量用semTake(),释放用semGive()。这两个函数是成对出现的,就像锁和钥匙的关系。

// 获取信号量(带超时)
if (semTake(semMutex, WAIT_FOREVER) == OK) {
    // 进入临界区,访问共享资源
    // ...
    
    // 释放信号量
    semGive(semMutex);
}

这里有个细节我想强调一下:semTake()的第二个参数是超时时间。WAIT_FOREVER表示一直等,直到拿到信号量。NO_WAIT表示立即返回,拿不到就算了。我个人建议,除非你非常确定资源很快就能拿到,否则尽量用WAIT_FOREVER或者一个合理的超时值。

实战技巧:我在做数据采集系统时,发现一个任务经常超时。后来用semTake(sem, 100)(100个tick超时)代替WAIT_FOREVER,配合错误处理,系统稳定性提升了不少。超时机制是你的朋友,别怕用它。

1.4 典型应用场景:任务同步

二进制信号量最经典的应用就是任务同步。什么叫任务同步?就是一个任务等另一个任务干完某件事,才能继续往下走。

举个例子:假设你有两个任务,TaskA负责采集数据,TaskB负责处理数据。TaskB必须等TaskA采集完才能开始处理。用二进制信号量实现这个逻辑,代码非常简洁:

/* 全局信号量 */
SEM_ID semDataReady;

/* TaskA - 数据采集任务 */
void taskA(void)
{
    while (1) {
        // 采集数据
        collectData();
        
        // 通知TaskB:数据准备好了
        semGive(semDataReady);
        
        // 继续采集下一批数据
        taskDelay(100);
    }
}

/* TaskB - 数据处理任务 */
void taskB(void)
{
    while (1) {
        // 等待数据就绪
        semTake(semDataReady, WAIT_FOREVER);
        
        // 处理数据
        processData();
    }
}

/* 初始化 */
void init(void)
{
    // 创建信号量,初始为不可用
    semDataReady = semBCreate(SEM_Q_PRIORITY, SEM_EMPTY);
    
    // 创建任务
    taskSpawn("tTaskA", 100, 0, 4096, taskA, 0,0,0,0,0,0,0,0,0,0);
    taskSpawn("tTaskB", 100, 0, 4096, taskB, 0,0,0,0,0,0,0,0,0,0);
}

你看,这个模式多清晰。TaskB在semTake()那里等着,直到TaskA调用semGive()。这就是典型的「生产者-消费者」模型。

关键理解:二进制信号量用于同步时,初始状态通常是SEM_EMPTY。这样,等待的任务会立即阻塞,直到其他任务释放信号量。这和用于互斥时(初始为SEM_FULL)正好相反。

1.5 我踩过的坑

说到二进制信号量,我不得不提一个让我印象深刻的bug。有一次,我在一个多任务系统中用了二进制信号量做同步,结果系统跑着跑着就死锁了。排查了整整两天,最后发现是某个任务在临界区里调用了semGive()两次。

为什么会这样?因为二进制信号量不像计数信号量,它只能被释放一次。连续两次semGive(),第二次调用实际上不会改变信号量的状态。但问题在于,代码逻辑上以为释放了两次,导致另一个任务永远等不到信号量。

血的教训:我曾经在一个项目中,因为一个任务里不小心调了两次semGive(),导致整个系统崩溃。从那以后,我养成了一个习惯:每次semGive()之后,立即把信号量指针置为NULL,防止误操作。

1.6 小结

二进制信号量是VxWorks同步机制的基础。记住几个要点:

  • 创建时选对初始状态:互斥用SEM_FULL,同步用SEM_EMPTY
  • 获取和释放必须成对出现,别多也别少
  • 超时机制是你的保护伞,别总用WAIT_FOREVER
  • 任务同步是二进制信号量的拿手好戏,生产者-消费者模型是经典案例

下一章咱们聊计数信号量,那个东西在处理资源池场景时特别好用。到时候我会分享一个我在网络协议栈开发中遇到的真实案例,保证让你有收获。

课后思考:如果两个任务同时调用semTake()等待同一个二进制信号量,会发生什么?哪个任务会先拿到?答案就在semBCreate()的options参数里。试试看用SEM_Q_PRIORITY和SEM_Q_FIFO分别创建,观察行为差异。