4. 任务间同步与通信:信号量、消息队列、管道与套接字、事件机制

各位同学,咱们今天聊点实在的。任务间同步与通信,说白了就是让多个任务能好好“说话”,别打架。我在VxWorks上摸爬滚打这么多年,见过太多因为同步没做好导致的“血案”——系统死锁、数据错乱、任务饿死……嗯,今天咱们就把这些坑一个个填上。

4.1 信号量:最基础的同步工具

信号量,我习惯叫它“交通信号灯”。它控制着任务对共享资源的访问权。VxWorks里主要有三种:二进制、计数、互斥。

4.1.1 二进制信号量

二进制信号量只有两个状态:0(不可用)和1(可用)。它最适合做“事件通知”。

典型场景:一个任务等待某个硬件中断发生,另一个任务处理中断数据。

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

// 任务A:等待中断
taskWait() {
    semTake(semBinary, WAIT_FOREVER);  // 阻塞等待
    // 处理数据...
}

// ISR:通知任务
isrHandler() {
    semGive(semBinary);  // 释放信号量
}
我的经验:在ISR里千万别用semTake()!ISR不能阻塞。我曾经有个同事在中断里调了semTake(),结果系统直接挂掉。记住:ISR只能semGive()。

4.1.2 计数信号量

计数信号量维护一个计数器,允许多个任务同时访问有限数量的资源。比如一个连接池有5个连接,最多允许5个任务同时使用。

// 创建计数信号量,初始值5,最大值5
SEM_ID semCount = semCCreate(SEM_Q_FIFO, 5);

// 任务获取连接
taskUseConnection() {
    semTake(semCount, WAIT_FOREVER);
    // 使用连接...
    semGive(semCount);  // 归还
}

你想想看,如果没有计数信号量,5个任务同时抢一个连接,那不乱套了?

4.1.3 互斥信号量

互斥信号量是二进制信号量的“升级版”。它解决了优先级反转问题——这是实时系统里的大坑。

避坑指南:我曾经在一个雷达数据处理系统里,因为用了普通二进制信号量,导致高优先级任务被低优先级任务阻塞了整整200ms。这在实时系统里是致命的!后来换成互斥信号量,问题立刻解决。

互斥信号量支持优先级继承:当低优先级任务持有锁时,它会临时“继承”等待该锁的最高优先级任务的优先级,从而不被中间优先级的任务抢占。

// 创建互斥信号量(带优先级继承)
SEM_ID semMutex = semMCreate(SEM_Q_PRIORITY | SEM_INVERSION_SAFE);
类型 适用场景 注意事项
二进制信号量 事件通知、简单互斥 不支持优先级继承
计数信号量 资源池管理 注意最大计数值
互斥信号量 保护共享资源 支持优先级继承,但开销稍大

4.2 消息队列:任务间的“快递员”

消息队列(Message Queue)是VxWorks里最常用的通信方式。它允许任务之间发送和接收消息,消息可以是任意长度的数据。

我个人习惯用消息队列做“生产者-消费者”模式。比如一个任务采集传感器数据,另一个任务处理数据并写入日志。

// 创建消息队列,最多10条消息,每条最大100字节
MSG_Q_ID msgQ = msgQCreate(10, 100, MSG_Q_FIFO);

// 生产者任务
taskProducer() {
    char buffer[100];
    while(1) {
        // 采集数据...
        msgQSend(msgQ, buffer, 100, WAIT_FOREVER, MSG_PRI_NORMAL);
    }
}

// 消费者任务
taskConsumer() {
    char buffer[100];
    while(1) {
        msgQReceive(msgQ, buffer, 100, WAIT_FOREVER);
        // 处理数据...
    }
}
关键点:消息队列支持紧急消息(MSG_PRI_URGENT),它会插队到队列最前面。我在一个飞行控制系统中用过这个特性——当检测到异常时,紧急消息能立刻被处理,而不是排队等。

4.3 管道与套接字:跨任务/跨系统通信

4.3.1 管道(Pipe)

管道本质上是一个特殊的消息队列,但它使用文件描述符接口。这意味着你可以用read()/write()来操作它,就像操作文件一样。

为什么要有管道?说白了,就是为了兼容POSIX接口。如果你想把Linux上的代码移植到VxWorks,管道能省不少事。

// 创建管道
int fds[2];
pipe(fds);

// 任务A写入
write(fds[1], "hello", 5);

// 任务B读取
char buf[10];
read(fds[0], buf, 5);

4.3.2 套接字(Socket)

套接字用于网络通信。VxWorks支持标准的BSD套接字接口。我在一个分布式控制系统中用过——多个VxWorks节点通过UDP广播同步状态。

// UDP接收端
int sock = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.s_addr = INADDR_ANY;
bind(sock, (struct sockaddr*)&addr, sizeof(addr));

char buf[1024];
recvfrom(sock, buf, 1024, 0, NULL, NULL);
建议:在VxWorks里用套接字,记得检查网络协议栈的配置。我遇到过因为缓冲区太小导致丢包的情况,调大sbuf和rbuf就解决了。

4.4 事件(Event)机制:轻量级同步

事件机制是VxWorks的特色之一。它允许任务等待一组事件中的任意一个或多个发生。每个任务有32个事件位(0-31)。

事件机制比信号量更灵活。比如一个任务需要等待“数据就绪”或“超时”或“错误”中的任意一个发生,用事件就非常方便。

// 任务等待事件
UINT32 events;
eventReceive(0x07, EVENTS_WAIT_ANY, WAIT_FOREVER, &events);

if (events & 0x01) {
    // 数据就绪
} else if (events & 0x02) {
    // 超时
} else if (events & 0x04) {
    // 错误
}

为什么会用事件而不是信号量?你想想看,如果用信号量实现“等待多个条件”,你得创建多个信号量,然后一个个去取,代码会变得很啰嗦。事件机制一个调用就搞定了。

注意:事件是任务级别的,不是全局的。任务A发送事件给任务B,任务B必须明确调用eventReceive()才能收到。我曾经见过有人误以为事件是广播的,结果任务没收到事件,排查了半天。

好了,这一章的内容就这些。信号量、消息队列、管道、套接字、事件——每个工具都有自己的用武之地。选对工具,你的系统才能跑得稳、跑得快。下一章咱们聊聊内存管理,那又是另一番天地了。