4、IPC通信机制:消息传递(MsgSend/MsgReceive)、共享内存(shm_open)、脉冲(Pulse)使用技巧
QNX 的 IPC,说白了就是进程间聊天的几种方式。有人喜欢打电话(消息传递),有人喜欢贴纸条(脉冲),还有人干脆把笔记本撕几页扔给对方(共享内存)。这三种方式我都在项目里用过,各有各的脾气。
4.1 消息传递:最正统的 IPC 方式
消息传递是 QNX 的看家本领。它的核心就两个函数:MsgSend() 和 MsgReceive()。一个发,一个收,同步阻塞。
我刚开始用 QNX 时,总觉得这玩意儿太死板。发消息必须等对方收完才能继续跑,多耽误事啊?后来才明白,这种设计恰恰是为了确定性——你发出去的消息,对方一定处理完了,你才往下走。
核心要点:消息传递是同步的、阻塞的、带数据拷贝的。发送方在 MsgSend 调用后会阻塞,直到接收方调用 MsgReply 或 MsgError 回复。
来看个典型例子:
// 服务端
int chid = ChannelCreate(0);
// 等待客户端连接
int rcvid = MsgReceive(chid, &msg, sizeof(msg), NULL);
// 处理消息
msg.result = msg.a + msg.b;
// 回复客户端
MsgReply(rcvid, EOK, &msg, sizeof(msg));
// 客户端
int coid = ConnectAttach(0, 0, chid, _NTO_SIDE_CHANNEL, 0);
MsgSend(coid, &msg, sizeof(msg), &reply, sizeof(reply));
嗯,这里要注意:ChannelCreate 和 ConnectAttach 的权限参数别乱填。我在项目中遇到过一个问题——服务端创建通道时用了 _NTO_CHF_FIXED_PRIORITY,结果客户端优先级一高,直接把服务端线程饿死了。后来改成默认参数才解决。
4.2 消息传递的避坑指南
我曾经在一个车载音频项目里踩过大坑。当时多个客户端同时向服务端发消息,服务端用 MsgReceive 一个个处理。结果有个客户端发了超大消息(几 MB 的音频数据),导致其他客户端等了好久。
解决方案其实不复杂:
- 控制消息大小:超过 64KB 的消息,考虑用共享内存传数据,消息只传指针
- 使用优先级通道:紧急消息走高优先级通道
- 异步处理:用
MsgSendPulse代替MsgSend,服务端收到脉冲后再去取数据
我的习惯:消息体结构里永远留一个 uint32_t type 字段。这样服务端收到消息后,先判断类型再处理,代码清晰多了。
4.3 脉冲:轻量级的通知机制
脉冲(Pulse)是 QNX 里最轻的 IPC 方式。它只有 4 字节的数据负载,但发送方不需要等待回复。说白了就是「喊一嗓子,爱听不听」。
脉冲最适合做事件通知。比如按键按下、定时器超时、数据就绪这些场景。
// 发送脉冲
struct _pulse pulse;
pulse.code = _PULSE_CODE_MINAVAIL + 1; // 自定义脉冲码
pulse.value.sival_int = 42; // 附带数据
MsgSendPulse(coid, -1, pulse.code, pulse.value.sival_int);
// 接收脉冲
struct _msg_info info;
int rcvid = MsgReceive(chid, &pulse, sizeof(pulse), &info);
if (rcvid == 0) {
// 收到的是脉冲
if (pulse.code == _PULSE_CODE_MINAVAIL + 1) {
// 处理脉冲
}
}
你想想看,脉冲的接收方怎么知道来的是脉冲还是普通消息?看 MsgReceive 的返回值:返回 0 就是脉冲,返回正数就是普通消息的 rcvid。
注意:脉冲的负载只有 4 字节。别想着用它传大数据。我见过有人把结构体塞进脉冲的 value 字段里,结果数据被截断了,查了半天 bug。
4.4 共享内存:大数据传输的利器
消息传递和脉冲都适合小数据。但如果你要传一帧摄像头图像(几 MB),再用消息传递就太蠢了——数据拷贝一次,CPU 就白忙活半天。
共享内存就不一样了。大家直接读写同一块物理内存,零拷贝。QNX 里用 shm_open() + mmap() 来搞。
// 创建共享内存
int fd = shm_open("/my_shm", O_RDWR | O_CREAT, 0666);
ftruncate(fd, SHM_SIZE);
void *ptr = mmap(0, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// 另一个进程打开同一块共享内存
int fd = shm_open("/my_shm", O_RDWR, 0666);
void *ptr = mmap(0, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
共享内存的名字以 / 开头,这是 POSIX 标准要求的。我习惯用 /com_company_project_shm_xxx 这种命名方式,避免冲突。
4.5 共享内存的同步问题
共享内存最大的坑是什么?同步。两个进程同时写同一块内存,数据就乱了。
QNX 提供了几种同步方式:
| 同步方式 | 适用场景 | 性能 |
|---|---|---|
| 互斥锁(pthread_mutex) | 进程间互斥访问 | 中等 |
| 信号量(sem_t) | 生产者-消费者 | 较高 |
| 原子操作(atomic_*) | 简单计数器、标志位 | 最高 |
| 脉冲 + 共享内存 | 数据就绪通知 | 灵活 |
我个人最喜欢「脉冲 + 共享内存」的组合。生产者写完数据后,发一个脉冲通知消费者。消费者收到脉冲后,再去共享内存里读数据。这样既避免了轮询,又实现了零拷贝。
避坑指南:共享内存的 shm_unlink() 要在所有进程都 munmap() 之后调用。我曾经在进程退出时先 unlink 了,结果另一个进程还在用,直接段错误。
4.6 三种 IPC 的选型建议
说了这么多,到底该用哪种?我总结了个简单的判断逻辑:
- 数据小于 64KB,需要同步确认:用消息传递。比如控制命令、配置参数。
- 数据小于 64KB,不需要回复:用脉冲。比如事件通知、状态变更。
- 数据大于 64KB,或者需要频繁读写:用共享内存。比如音视频数据、大块传感器数据。
- 既要传大数据,又要同步:共享内存 + 脉冲。数据走共享内存,同步走脉冲。
嗯,其实还有一个隐藏技巧:消息传递也可以传文件描述符。用 MsgSendv() 配合 IOV 结构体,可以把共享内存的 fd 传给另一个进程。这样两个进程就能共享同一块内存了,而且不需要 shm_open。
我在一个车载仪表盘项目里就用过这招。主控进程分配一块共享内存,把 fd 通过消息传给显示进程。显示进程 mmap 后直接读写,省去了 shm_open 的命名和权限管理。效果不错。
最后说一句:别把 IPC 想得太复杂。消息传递就是打电话,脉冲就是发微信,共享内存就是一起看黑板。选对工具,事半功倍。