4. IPC机制:消息传递、共享内存、脉冲、信号量、事件通知

QNX 的 IPC,说白了就是进程间通信的「高速公路网」。

我刚开始接触 QNX 时,最震撼的就是它的微内核设计——内核只做最基本的事,其他都交给进程。那进程之间怎么协作?全靠 IPC。今天咱们就把这套机制掰开揉碎了讲清楚。

4.1 消息传递:QNX 的「灵魂」

消息传递是 QNX IPC 的核心。它不像 Linux 那种「发完就不管了」,而是同步的、带确认的。

你想想看,一个进程发消息给另一个,发送方会阻塞,直到接收方处理完并回复。这叫什么?这叫「同步消息传递」。我习惯叫它「打电话」——你拨号,对方接,说完,挂断。

核心概念: 消息传递是同步的、带确认的。发送方和接收方在消息交换期间是耦合的。

代码示例很简单,但背后逻辑很深:

// 发送端
int chid = ConnectAttach(0, pid, chid, 0, 0);
MsgSend(chid, &smsg, sizeof(smsg), &rmsg, sizeof(rmsg));

// 接收端
int rcvid = MsgReceive(chid, &msg, sizeof(msg), NULL);
MsgReply(rcvid, EOK, &reply, sizeof(reply));

我在项目中遇到过一个问题:两个进程频繁交换小数据,结果性能上不去。后来发现是消息拷贝次数太多。QNX 的消息传递默认会做两次拷贝——从发送方到内核,再从内核到接收方。对于大数据,这开销不小。

我的建议: 如果数据量超过 4KB,就别用消息传递了。考虑共享内存吧。

4.2 共享内存:最快的 IPC

共享内存,说白了就是两个进程直接读写同一块物理内存。没有拷贝,没有内核介入,速度最快。

但快是有代价的——你得自己处理同步。否则一个进程在写,另一个在读,数据就乱套了。

// 创建共享内存
int fd = shm_open("/myshm", O_RDWR | O_CREAT, 0666);
ftruncate(fd, 4096);
void *ptr = mmap(0, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

// 另一个进程
int fd = shm_open("/myshm", O_RDWR, 0666);
void *ptr = mmap(0, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

嗯,这里要注意:共享内存本身不提供同步机制。你需要配合信号量或互斥锁使用。我习惯用 QNX 的 sync_* 系列函数,它们是为实时系统优化的。

避坑指南: 我曾经在共享内存里放了一个结构体,里面包含指针。结果两个进程的地址空间不同,指针完全无效。记住:共享内存里只能放「扁平数据」,不能放指针。

4.3 脉冲:轻量级通知

脉冲(Pulse)是 QNX 特有的一种 IPC。它很小——只有 40 字节左右。而且它是非阻塞的,发送方发完就继续跑,不用等接收方处理。

我把它比作「敲门」。你敲完门就走,屋里的人有空了再来开门。

// 发送脉冲
struct _pulse pulse;
pulse.code = _PULSE_CODE_MINAVAIL + 1;
pulse.value.sival_int = 42;
MsgSendPulse(coid, -1, pulse.code, pulse.value.sival_int);

// 接收脉冲(和消息共用 MsgReceive)
rcvid = MsgReceive(chid, &msg, sizeof(msg), NULL);
if (rcvid == 0) {
    // 收到的是脉冲
    struct _pulse *pulse = (struct _pulse *)&msg;
    // 处理脉冲
}

脉冲最适合做什么?中断通知、状态变化通知、超时处理。我在做设备驱动时,经常用脉冲通知用户态进程「硬件中断来了」。

小技巧: 脉冲的优先级比普通消息高。如果你希望某个通知被优先处理,用脉冲准没错。

4.4 信号量:资源计数器

信号量,说白了就是一个计数器。它用来控制对共享资源的访问数量。

QNX 的信号量分两种:命名信号量和无名信号量。命名信号量可以在不同进程间共享,无名信号量通常用于线程间同步。

// 创建命名信号量
sem_t *sem = sem_open("/mysem", O_CREAT, 0666, 1);

// 等待(P操作)
sem_wait(sem);

// 释放(V操作)
sem_post(sem);

// 关闭
sem_close(sem);

我习惯用信号量来保护共享内存。比如一个进程写数据,另一个进程读数据。写之前 sem_wait,写完后 sem_post。读操作也一样。

注意: 信号量的初始值很关键。设为 1 就是互斥锁,设为 N 就是资源池。我曾经把初始值设错了,结果多个进程同时访问共享资源,数据全乱了。

4.5 事件通知:异步回调

事件通知是 QNX 提供的一种异步机制。它允许一个进程注册「我对某个事件感兴趣」,当事件发生时,内核会通知你。

这有点像 Linux 的 epoll,但更轻量。事件通知可以绑定到消息通道、脉冲、甚至信号量上。

// 创建事件
struct sigevent event;
SIGEV_SIGNAL_INIT(&event, SIGUSR1);

// 注册事件
int fd = open("/dev/some_device", O_RDWR);
devctl(fd, DCMD_EVENT_REGISTER, &event, sizeof(event), NULL);

// 等待事件
int sig;
sigwait(&set, &sig);

事件通知最适合做什么?异步 I/O 完成通知、硬件状态变化通知。我在做音频驱动时,用事件通知告诉应用层「音频数据已经准备好了」。

我的经验: 事件通知和脉冲很像,但事件通知更灵活。你可以把事件绑定到任意文件描述符上,而脉冲只能通过消息通道传递。

4.6 如何选择?一张表说清楚

IPC 机制 速度 数据量 同步/异步 适用场景
消息传递 中等 小(<4KB) 同步 请求-响应模式
共享内存 最快 大(任意) 异步(需同步) 大数据交换
脉冲 极小(40B) 异步 轻量通知
信号量 无数据 同步 资源计数、互斥
事件通知 中等 无数据 异步 异步回调、I/O 通知

我个人习惯这样选:

  • 需要确认的请求-响应 → 消息传递
  • 大数据量交换 → 共享内存 + 信号量
  • 轻量级通知 → 脉冲
  • 资源计数或互斥 → 信号量
  • 异步 I/O 通知 → 事件通知

你想想看,QNX 的 IPC 机制其实就这五种。但组合起来,能解决几乎所有进程间通信的问题。我在实际项目中,经常把消息传递和共享内存混用——用消息传递传递控制命令,用共享内存传递数据。这样既保证了控制命令的可靠性,又保证了数据的高速传输。

嗯,最后说一句:IPC 的选择没有银弹。你得根据实际场景、数据量、实时性要求来权衡。多试试,多踩坑,慢慢就有感觉了。