4. 二进制信号量:创建、获取、释放,解决任务同步问题

好,咱们今天聊聊二进制信号量。

说实话,在VxWorks里做任务同步,二进制信号量是我用得最多的机制。它简单、高效,而且不容易出错。你想想看,两个任务要协调工作,一个等另一个干完活再继续,这种场景太常见了。

4.1 什么是二进制信号量?

二进制信号量,说白了就是一个只有两个状态的旗子:可用不可用。它不像计数信号量那样能记数,它只记0或1。

我个人习惯把它想象成一个「令牌」。谁拿到令牌,谁就能干活。干完了,把令牌还回去。就这么简单。

核心概念:二进制信号量用于任务间的互斥和同步。它只有两种状态:满(1)和空(0)。

4.2 创建二进制信号量

在VxWorks里,创建二进制信号量用的是 semBCreate() 函数。它的原型长这样:

SEM_ID semBCreate
    (
    int options,      /* 信号量选项 */
    SEM_B_STATE initialState  /* 初始状态:SEM_FULL 或 SEM_EMPTY */
    )

这里有两个参数需要你注意:

  • options:决定信号量的排队方式。常用的是 SEM_Q_PRIORITY(按优先级排队)和 SEM_Q_FIFO(先来后到)。
  • initialState:初始状态。如果是同步场景,一般设为 SEM_EMPTY(0);如果是保护共享资源,一般设为 SEM_FULL(1)。

我的经验:我一般用 SEM_Q_PRIORITY 做同步,用 SEM_Q_FIFO 做互斥。为什么?因为同步场景下,高优先级的任务应该先拿到信号量;而互斥场景下,大家公平排队就好,避免优先级反转。

4.3 获取信号量

获取信号量用 semTake()。任务调用这个函数时,如果信号量可用(值为1),它就拿到并继续执行;如果不可用(值为0),它就阻塞等待。

STATUS semTake
    (
    SEM_ID semId,      /* 信号量ID */
    int timeout        /* 超时时间,单位tick */
    )

timeout 参数很灵活:

  • WAIT_FOREVER:死等,直到拿到信号量。
  • NO_WAIT:不等待,拿不到就立即返回。
  • 具体数值:等待指定的 tick 数,超时后返回 ERROR。

避坑指南:我曾经在一个项目中,把 timeout 设成了 0 而不是 NO_WAIT。结果呢?任务直接返回了 ERROR,但系统没崩溃,只是行为诡异。查了两天才发现是参数传错了。记住:0 不等于 NO_WAIT,NO_WAIT 是宏定义,值通常是 0xffffffff 之类的特殊值。

4.4 释放信号量

释放信号量用 semGive()。任务干完活了,把令牌还回去,让其他等待的任务能拿到。

STATUS semGive
    (
    SEM_ID semId       /* 信号量ID */
    )

这个函数很简单,但有个细节要注意:谁拿的谁给。虽然VxWorks不强制要求,但乱给信号量会导致逻辑混乱。

4.5 实战:任务同步示例

咱们来看一个典型的同步场景。任务A负责采集数据,任务B负责处理数据。任务B必须等任务A采集完才能开始处理。

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

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

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

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

这段代码里,信号量初始为 SEM_EMPTY。任务B一启动就调用 semTake,但拿不到,于是阻塞。直到任务A采集完数据,调用 semGive,任务B才被唤醒。

关键点:同步场景下,信号量初始状态一定是 SEM_EMPTY。这样消费者任务一开始就会等待,不会空转。

4.6 常见问题与避坑

问题 现象 解决方案
信号量泄漏 任务拿了不还 检查所有路径,确保 semGive 一定会被执行
死锁 两个任务互相等 避免嵌套获取信号量,或使用超时机制
优先级反转 高优先级任务被低优先级任务阻塞 使用优先级继承或优先级天花板协议

我曾经踩过的坑:有一次,我在中断服务程序里调用了 semGive,但忘了检查返回值。结果信号量给失败了,任务B永远等不到数据。后来加了个 if (semGive(sem) != OK) 的检查,才发现是中断优先级太高,导致信号量操作被阻塞了。记住:中断里操作信号量要小心,最好用 semGive() 而不是 semTake()

4.7 二进制信号量 vs 互斥信号量

很多初学者会问:二进制信号量和互斥信号量有什么区别?

嗯,这个问题问得好。简单说:

  • 二进制信号量:适合做同步。一个任务给,另一个任务拿。没有所有权概念。
  • 互斥信号量:适合做互斥。谁拿了谁给,支持优先级继承,防止优先级反转。

我个人习惯是:做同步用二进制信号量,做互斥用互斥信号量。别混用,否则容易出问题。

4.8 小结

二进制信号量是VxWorks里最基础、最常用的同步机制。它只有两个状态,但能解决大部分任务同步问题。

记住三个函数:semBCreate 创建,semTake 获取,semGive 释放。再记住一个原则:同步用 SEM_EMPTY,互斥用 SEM_FULL

好了,这一章就到这里。下一章咱们聊聊计数信号量,看看它和二进制信号量有什么不同。