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对齐,避免伪共享问题

你想想看,如果每次写一个字节都要加锁解锁,那性能还不如用消息队列呢。所以设计时要权衡:数据量小、频率高,用原子操作;数据量大、频率低,用信号量保护。

好了,共享内存这块就讲这么多。记住一个原则:能用简单同步就别用复杂的,能用原子操作就别用信号量。但该用锁的时候也别省,数据一致性比性能更重要。