4. 共享内存同步:信号量机制

多核共享内存,最怕什么?

怕数据打架。

两个核同时写一个变量,结果谁都不知道最终值是什么。我在项目中遇到过好几次这种问题,排查起来特别头疼。所以,共享内存必须配同步机制。VxWorks 里最常用的就是信号量。

4.1 二进制信号量与互斥信号量

很多人搞不清这两个的区别。我刚开始用 VxWorks 时也犯过糊涂。

二进制信号量,说白了就是一个开关。0 表示资源被占,1 表示资源可用。它没有所有权概念。谁都能给,谁都能取。

互斥信号量呢?它带所有权。哪个任务拿了,就必须由哪个任务释放。而且它支持优先级继承,能防止优先级反转。

核心区别一句话:

  • 二进制信号量:适合事件通知、任务同步
  • 互斥信号量:适合保护共享资源、临界区

举个例子。我在一个飞控项目里,两个核共享一个传感器数据缓冲区。一开始我用二进制信号量保护,结果发现高优先级任务老是被阻塞。后来换成互斥信号量,问题就解决了。嗯,这就是优先级继承在起作用。

4.1.1 二进制信号量实战

/* 创建二进制信号量 */
SEM_ID semBinary = semBCreate(SEM_Q_FIFO, SEM_FULL);

/* 任务A:生产者 */
void taskProducer(void)
{
    while (1)
    {
        /* 写入共享数据 */
        writeSharedData();
        
        /* 释放信号量,通知任务B */
        semGive(semBinary);
        
        taskDelay(10);
    }
}

/* 任务B:消费者 */
void taskConsumer(void)
{
    while (1)
    {
        /* 等待信号量 */
        semTake(semBinary, WAIT_FOREVER);
        
        /* 读取共享数据 */
        readSharedData();
    }
}

你看,这个模式特别适合一个核生产、一个核消费的场景。我建议你注意一下 semTake 的超时参数。WAIT_FOREVER 虽然方便,但万一生产者挂了,消费者就永远卡死了。我在项目中一般设个超时,比如 1000 个 tick。

4.1.2 互斥信号量实战

/* 创建互斥信号量 */
SEM_ID semMutex = semMCreate(SEM_Q_PRIORITY | SEM_INVERSION_SAFE);

/* 保护共享资源 */
void accessSharedMemory(void)
{
    /* 获取互斥锁 */
    semTake(semMutex, WAIT_FOREVER);
    
    /* 临界区操作 */
    sharedVar1 = computeValue();
    sharedVar2 = sharedVar1 * 2;
    
    /* 释放互斥锁 */
    semGive(semMutex);
}

注意:互斥信号量不能在中断服务程序中使用。因为 ISR 里不能阻塞等待。如果你需要在中断里做同步,请用二进制信号量。

我曾经犯过一个低级错误。在中断里调了 semTake,结果系统直接挂掉。排查了半天才发现问题。你想想看,中断上下文哪能等锁呢?

4.2 读写锁在共享内存中的应用

读写锁是个好东西。它允许多个读者同时访问,但写者必须独占。

为什么需要它?

你想啊,共享内存里大部分操作是读。如果每次读都要拿互斥锁,那性能就浪费了。读写锁正好解决这个问题。

4.2.1 VxWorks 读写锁实现

/* 读写锁结构 */
typedef struct {
    SEM_ID semMutex;      /* 保护内部状态的互斥锁 */
    SEM_ID semRead;       /* 读者信号量 */
    int    readerCount;   /* 当前读者数量 */
} RWLock;

/* 初始化读写锁 */
void rwLockInit(RWLock *lock)
{
    lock->semMutex = semMCreate(SEM_Q_PRIORITY | SEM_INVERSION_SAFE);
    lock->semRead = semBCreate(SEM_Q_FIFO, SEM_FULL);
    lock->readerCount = 0;
}

/* 获取读锁 */
void rwLockRead(RWLock *lock)
{
    semTake(lock->semMutex, WAIT_FOREVER);
    
    if (lock->readerCount == 0)
    {
        /* 第一个读者,阻止写者 */
        semTake(lock->semRead, WAIT_FOREVER);
    }
    lock->readerCount++;
    
    semGive(lock->semMutex);
}

/* 释放读锁 */
void rwUnlockRead(RWLock *lock)
{
    semTake(lock->semMutex, WAIT_FOREVER);
    
    lock->readerCount--;
    if (lock->readerCount == 0)
    {
        /* 最后一个读者,允许写者 */
        semGive(lock->semRead);
    }
    
    semGive(lock->semMutex);
}

/* 获取写锁 */
void rwLockWrite(RWLock *lock)
{
    semTake(lock->semRead, WAIT_FOREVER);
}

/* 释放写锁 */
void rwUnlockWrite(RWLock *lock)
{
    semGive(lock->semRead);
}

使用建议:

  • 读多写少的场景,读写锁性能提升明显
  • 写操作频繁时,读写锁可能不如互斥锁
  • 注意读者优先可能导致写者饥饿

我在一个数据采集系统里用过读写锁。四个核同时读传感器数据,一个核负责更新校准参数。用互斥锁时,读操作经常被阻塞。换成读写锁后,读性能提升了将近 3 倍。效果很明显。

4.3 信号量选择决策表

场景 推荐信号量 原因
任务间事件通知 二进制信号量 轻量、快速、无所有权
中断与任务同步 二进制信号量 ISR 中可用
保护共享资源 互斥信号量 支持优先级继承
读多写少的数据 读写锁 并发读性能好
递归锁需求 互斥信号量 支持递归获取

4.4 避坑指南

我踩过的坑,你最好别踩。

  • 死锁:我曾经在两个任务里互相等待对方的信号量,结果系统卡死。解决办法是统一锁的顺序,或者用 tryLock 机制。
  • 优先级反转:低优先级任务拿着锁,高优先级任务等锁,中优先级任务抢 CPU。互斥信号量的优先级继承能解决这个问题。
  • 信号量泄漏:创建了信号量忘记删除。在长时间运行的系统里,这会导致资源耗尽。我习惯在创建时记录日志,方便排查。
  • 超时设置:WAIT_FOREVER 要慎用。我建议大部分场景设一个合理的超时值,比如 500ms。万一出问题,至少能恢复。

我的经验总结:

共享内存同步,选对信号量是关键。二进制信号量做通知,互斥信号量保护资源,读写锁优化读性能。记住这三条,大部分场景都能搞定。

嗯,这一章就到这里。下一章我们聊聊消息队列在核间通信中的应用。那个也很有意思。