4、任务间通信:信号量、消息队列、管道、事件、共享内存
任务间通信,说白了就是让多个任务能「好好说话」。
在 VxWorks 里,任务都是独立运行的,但现实世界哪有那么多「老死不相往来」?数据要共享、资源要互斥、事件要通知。我刚开始做 VxWorks 开发时,就吃过通信方式选错的亏——系统跑着跑着就死锁了,查了两天才发现是信号量用错了类型。
今天咱们就把这五种通信方式捋清楚。每种都有它的脾气,用对了事半功倍。
4.1 信号量:最基础的同步工具
信号量是 VxWorks 里最常用的通信机制。我个人习惯把它分成三类:二进制、计数、互斥。
4.1.1 二进制信号量
二进制信号量只有两个状态:0 和 1。说白了就是个「开关」。
它最适合做任务同步。比如一个任务等待某个事件发生,另一个任务去触发这个事件。
典型场景: 一个任务等待数据就绪,另一个任务负责采集数据。
// 创建二进制信号量
SEM_ID semBinary = semBCreate(SEM_Q_FIFO, SEM_EMPTY);
// 任务A:等待数据
taskA()
{
semTake(semBinary, WAIT_FOREVER); // 等!没数据就挂起
// 数据就绪,开始处理
}
// 任务B:通知数据就绪
taskB()
{
// 采集数据...
semGive(semBinary); // 好了,通知任务A
}
我在项目中遇到过一个问题:两个任务都用二进制信号量做同步,结果一个给了信号量,另一个没及时取,信号量就丢了。嗯,这里要注意——二进制信号量没有「计数」能力,给一次只能被取一次。
4.1.2 计数信号量
计数信号量可以记录「有多少个资源可用」。它内部维护一个计数器,每次 give 加一,每次 take 减一。
你想想看,如果缓冲区有 10 个位置,生产者放一个,消费者取一个。用二进制信号量?不行,因为缓冲区可能同时有多个数据。这时候计数信号量就派上用场了。
// 创建计数信号量,初始值为 5
SEM_ID semCount = semCCreate(SEM_Q_FIFO, 5);
// 生产者
producer()
{
// 生产数据...
semGive(semCount); // 资源数+1
}
// 消费者
consumer()
{
semTake(semCount, WAIT_FOREVER); // 资源数-1,不够就等
// 消费数据...
}
我曾经用计数信号量管理一个内存池的 8 个块。每个任务申请时 take,释放时 give。简单又高效。
4.1.3 互斥信号量
互斥信号量是二进制信号量的「升级版」。它专门用来保护共享资源,防止多个任务同时访问。
为什么不用二进制信号量代替?因为互斥信号量有几个关键特性:
- 优先级继承:防止优先级反转。这是个大坑,我后面会讲。
- 递归访问:同一个任务可以多次 take 同一个互斥信号量,不会死锁。
- 删除安全:任务在持有信号量时被删除,系统会自动释放。
避坑指南: 我曾经在一个项目中用二进制信号量保护共享内存,结果高优先级任务被低优先级任务阻塞了整整 200ms。后来换成互斥信号量,问题立刻解决。优先级反转不是闹着玩的。
// 创建互斥信号量
SEM_ID semMutex = semMCreate(SEM_Q_PRIORITY);
// 访问共享资源
taskAccess()
{
semTake(semMutex, WAIT_FOREVER);
// 操作共享数据...
semGive(semMutex);
}
4.2 消息队列:任务间的「快递员」
信号量只能传递「有/没有」这种简单信息。如果你要传递具体的数据,比如一个结构体、一段字符串,那就得用消息队列。
消息队列本质上是一个 FIFO 缓冲区。一个任务往里放消息,另一个任务往外取。VxWorks 的消息队列支持多种消息长度,也支持紧急消息(插队到队首)。
// 创建消息队列,最多10条消息,每条最大100字节
MSG_Q_ID msgQ = msgQCreate(10, 100, MSG_Q_FIFO);
// 发送任务
sender()
{
char buffer[] = "Hello, VxWorks!";
msgQSend(msgQ, buffer, strlen(buffer) + 1, WAIT_FOREVER, MSG_PRI_NORMAL);
}
// 接收任务
receiver()
{
char buffer[100];
msgQReceive(msgQ, buffer, 100, WAIT_FOREVER);
printf("收到消息: %s\n", buffer);
}
我个人习惯用消息队列做任务间的「解耦」。发送方不用关心接收方是谁,接收方也不用关心发送方是谁。只要约定好消息格式,两边各干各的。
小技巧: 消息队列的长度不要设太大,够用就行。我见过有人设了 1000 条,结果内存爆了。一般 10~50 条就够用。
4.3 管道:类 Unix 的通信方式
管道是 VxWorks 模仿 Unix 管道机制实现的。它本质上是一个虚拟的 I/O 设备,任务可以像读写文件一样读写管道。
为什么要有管道?因为有些场景下,任务需要和驱动、或者和文件系统交互。用消息队列?不行,消息队列只能任务间通信。管道可以打通任务和 I/O 系统之间的壁垒。
// 创建管道
int fd = pipeCreate("/pipe/myPipe", 10, 100);
// 任务A:写入管道
taskA()
{
write(fd, "data", 4);
}
// 任务B:读取管道
taskB()
{
char buf[100];
read(fd, buf, 100);
}
我记得有一次调试一个网络驱动,数据从网卡到应用层需要经过好几个模块。用管道把各个模块串起来,每个模块只管读写管道,调试起来特别方便。
注意: 管道是阻塞 I/O。如果读管道时没有数据,任务会挂起。如果写管道时缓冲区满了,任务也会挂起。设计时一定要考虑超时机制。
4.4 事件:一对多的通知机制
事件是一种「广播式」的通信方式。一个任务可以等待多个事件,另一个任务可以触发一个或多个事件。
你想想看,如果系统里有 5 个任务都在等同一个条件,用信号量?你得给每个任务发一个信号量,太麻烦。用事件?一个事件触发,所有等待的任务都能收到通知。
// 创建事件
EVENT_ID eventId = eventCreate();
// 任务A:等待事件0x01
taskA()
{
eventReceive(eventId, 0x01, EVENTS_WAIT_ANY, WAIT_FOREVER, NULL);
// 事件0x01发生了
}
// 任务B:触发事件0x01
taskB()
{
eventSend(eventId, 0x01);
}
事件支持「等待任意一个」和「等待全部」两种模式。我个人习惯用事件做状态机的驱动——每个状态对应一个事件位,状态切换时触发对应事件。
实战经验: 事件最多支持 32 个独立事件位(0x00000001 ~ 0x80000000)。别贪多,一般 8~16 个就够用了。
4.5 共享内存:最快的通信方式
共享内存,说白了就是多个任务直接访问同一块物理内存。没有拷贝、没有队列、没有中间环节——速度最快。
但快是有代价的。共享内存需要配合信号量或互斥锁使用,否则多个任务同时读写,数据就乱套了。
// 分配共享内存
char *sharedMem = (char *)memalign(4, 1024);
// 任务A:写入数据
taskA()
{
semTake(semMutex, WAIT_FOREVER);
strcpy(sharedMem, "共享数据");
semGive(semMutex);
}
// 任务B:读取数据
taskB()
{
semTake(semMutex, WAIT_FOREVER);
printf("读取: %s\n", sharedMem);
semGive(semMutex);
}
我在一个视频处理项目里用过共享内存。摄像头采集的数据量很大,用消息队列拷贝一次就要几毫秒,根本扛不住。后来改成共享内存,延迟降到微秒级。
避坑指南: 共享内存一定要用互斥信号量保护。我曾经图省事没用锁,结果两个任务同时写同一个地址,数据错乱,查了三天才发现是共享内存的竞争问题。
4.6 如何选择?一张表说清楚
| 通信方式 | 传递内容 | 速度 | 典型场景 |
|---|---|---|---|
| 二进制信号量 | 状态(0/1) | 极快 | 任务同步、中断通知 |
| 计数信号量 | 资源数量 | 极快 | 资源池管理、生产者-消费者 |
| 互斥信号量 | 锁状态 | 快 | 共享资源保护、临界区 |
| 消息队列 | 数据块 | 中等 | 任务间数据传递、解耦 |
| 管道 | 字节流 | 中等 | 任务与I/O系统交互 |
| 事件 | 事件位 | 快 | 一对多通知、状态机驱动 |
| 共享内存 | 任意数据 | 最快 | 大数据量传输、低延迟场景 |
嗯,总结一下:
- 只要同步,用信号量。
- 要传数据,用消息队列。
- 要打通 I/O,用管道。
- 要广播通知,用事件。
- 要极致速度,用共享内存(别忘了加锁)。
这五种通信方式,我在实际项目中都用过。没有最好的,只有最合适的。选对了,系统跑得稳;选错了,调试到崩溃。希望今天的分享能帮你少走弯路。