3. 共享内存实战:多任务访问共享缓冲区的设计与同步问题
共享内存,说白了就是让多个任务直接读写同一块物理内存。这是VxWorks里效率最高的通信方式——没有数据拷贝,没有消息队列的排队开销。但效率高,坑也多。我见过不少项目,共享内存用得好,系统稳如老狗;用得不好,三天两头死机。
3.1 共享内存的基本模型
在VxWorks里,共享内存的实现其实很简单。你只需要分配一块内存,然后把它的地址告诉多个任务就行。但这里有个关键问题:谁分配?谁释放?
我个人习惯的做法是:由一个初始化任务统一分配,其他任务只负责读写。这样能避免内存泄漏和重复释放的问题。
/* 共享内存结构体定义 */
typedef struct {
int data_ready; /* 数据就绪标志 */
char buffer[1024]; /* 数据缓冲区 */
int length; /* 数据长度 */
} SHARED_BUF;
/* 全局指针,所有任务都能访问 */
SHARED_BUF *pSharedBuf = NULL;
/* 初始化任务 */
void initTask(void)
{
/* 分配共享内存 */
pSharedBuf = (SHARED_BUF *)memalign(8, sizeof(SHARED_BUF));
if (pSharedBuf == NULL) {
printf("共享内存分配失败!\n");
return;
}
/* 清零初始化 */
bzero(pSharedBuf, sizeof(SHARED_BUF));
/* 创建信号量用于同步 */
semId = semBCreate(SEM_Q_FIFO, SEM_EMPTY);
}
你看,代码其实不复杂。但真正的问题在于:多个任务同时读写这块内存时,怎么保证数据的一致性?
3.2 同步问题:为什么需要保护?
先看一个典型的错误场景。假设有两个任务:一个写数据,一个读数据。没有同步机制时,会发生什么?
踩坑实录
我曾经在一个数据采集项目中,用了共享内存传递传感器数据。写任务每10ms更新一次数据,读任务每20ms读取一次。刚开始测试没问题,但一上真实环境,读到的数据经常是半截的——写任务写到一半,读任务就开始读了,结果读到的数据一部分是新的,一部分是旧的。
为什么会这样?因为共享内存本身不提供任何原子性保证。一个任务写int型变量,在底层可能是分两条指令完成的。如果读任务恰好在两条指令之间读取,那读到的就是垃圾数据。
3.3 常用同步机制
解决这个问题,VxWorks提供了几种手段。我按推荐程度排个序:
| 同步方式 | 适用场景 | 性能开销 | 复杂度 |
|---|---|---|---|
| 二进制信号量 | 一写一读 | 低 | 低 |
| 互斥信号量 | 多写多读 | 中 | 中 |
| 任务锁(taskLock) | 临界区极短 | 极低 | 低 |
| 原子操作 | 简单变量 | 最低 | 高 |
嗯,这里要注意:不是越复杂的方案越好。我见过有人用互斥信号量保护一个int变量的读写,其实用原子操作就够了。
3.4 实战:生产者-消费者模型
来,我们写一个完整的例子。这是共享内存最经典的应用场景:
/* 生产者任务 */
void producerTask(void)
{
int count = 0;
while (1) {
/* 等待缓冲区空闲 */
semTake(semEmpty, WAIT_FOREVER);
/* 写入数据 */
pSharedBuf->length = sprintf(pSharedBuf->buffer,
"数据包 #%d", count++);
pSharedBuf->data_ready = 1;
/* 通知消费者 */
semGive(semFull);
taskDelay(10); /* 模拟生产间隔 */
}
}
/* 消费者任务 */
void consumerTask(void)
{
while (1) {
/* 等待数据就绪 */
semTake(semFull, WAIT_FOREVER);
/* 读取数据 */
printf("收到: %s (长度=%d)\n",
pSharedBuf->buffer, pSharedBuf->length);
pSharedBuf->data_ready = 0;
/* 通知生产者缓冲区已空 */
semGive(semEmpty);
taskDelay(5); /* 模拟处理时间 */
}
}
我的经验
这个模型里,两个信号量semEmpty和semFull的初始值很关键。semEmpty初始化为1(缓冲区空),semFull初始化为0(无数据)。这样生产者先运行,消费者等待。如果你搞反了,程序一启动就死锁。
3.5 环形缓冲区的设计
上面的例子只能传递一个数据包。实际项目中,我们经常需要传递多个数据包。这时候环形缓冲区就派上用场了。
/* 环形缓冲区结构 */
#define BUF_SIZE 16
typedef struct {
char data[BUF_SIZE][256]; /* 数据存储区 */
int head; /* 写指针 */
int tail; /* 读指针 */
int count; /* 当前数据个数 */
SEM_ID semMutex; /* 互斥信号量 */
} RING_BUF;
RING_BUF *pRingBuf;
/* 写入环形缓冲区 */
int ringBufWrite(RING_BUF *pBuf, char *data, int len)
{
int status;
semTake(pBuf->semMutex, WAIT_FOREVER);
if (pBuf->count >= BUF_SIZE) {
/* 缓冲区满 */
status = ERROR;
} else {
/* 拷贝数据 */
bcopy(data, pBuf->data[pBuf->head], len);
pBuf->head = (pBuf->head + 1) % BUF_SIZE;
pBuf->count++;
status = OK;
}
semGive(pBuf->semMutex);
return status;
}
关键设计点
- head和tail指针用取模运算实现循环
- count变量判断缓冲区满/空
- 互斥信号量保护整个操作
- 数据拷贝要快,避免长时间占用锁
3.6 避坑指南
做共享内存开发这么多年,我总结了几条血泪教训:
我曾经踩过的坑:
- 缓存一致性问题:在MP系统上,不同CPU核有自己的缓存。一个核写了共享内存,另一个核可能读的是缓存里的旧数据。解决办法是用cacheFlush/cacheInvalidate。
- 优先级反转:低优先级任务拿了锁,被中优先级任务抢占,高优先级任务等锁等到死。解决办法是用优先级继承的互斥信号量。
- 死锁:两个任务互相等待对方释放锁。我有个项目就因为这个,系统跑半小时必死。后来用锁的获取顺序规范化解决了。
3.7 性能优化建议
共享内存虽然快,但用不好也会成为性能瓶颈。给你几个优化思路:
- 减少锁的粒度:不要锁整个缓冲区,只锁正在操作的那个元素
- 使用双缓冲:一个缓冲区在写,一个缓冲区在读,交替使用
- 批量处理:攒一批数据再通知消费者,减少上下文切换
- 内存对齐:共享内存按cache line对齐,避免伪共享问题
你想想看,如果每次写一个字节都要加锁解锁,那性能还不如用消息队列呢。所以设计时要权衡:数据量小、频率高,用原子操作;数据量大、频率低,用信号量保护。
好了,共享内存这块就讲这么多。记住一个原则:能用简单同步就别用复杂的,能用原子操作就别用信号量。但该用锁的时候也别省,数据一致性比性能更重要。