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。万一出问题,至少能恢复。
我的经验总结:
共享内存同步,选对信号量是关键。二进制信号量做通知,互斥信号量保护资源,读写锁优化读性能。记住这三条,大部分场景都能搞定。
嗯,这一章就到这里。下一章我们聊聊消息队列在核间通信中的应用。那个也很有意思。