4、QNX IPC通信机制:消息传递、共享内存、脉冲

好,咱们进入第四章。这一章聊的是QNX的IPC,也就是进程间通信。

说实话,做嵌入式音频系统,IPC是绕不开的坎。你想想看,音频驱动要跟音频服务通信,服务要跟应用层通信,底层硬件中断要通知上层处理——这些全靠IPC撑着。

QNX的IPC机制主要有三种:消息传递、共享内存、脉冲。我一个个说。

4.1 消息传递(Message Passing)

消息传递是QNX最核心的IPC方式。它跟Linux的信号、管道不一样,QNX的消息传递是同步的、带阻塞的。

什么意思?就是发送方发完消息后,会阻塞住,直到接收方处理完并回复。反过来,接收方调用接收函数时,如果没有消息,也会阻塞。

这种设计的好处是——天然同步,不需要额外的锁机制。坏处嘛,就是如果处理不当,容易死锁。

核心API:

  • Send() — 发送消息并等待回复
  • Receive() — 接收消息
  • Reply() — 回复消息

我举个例子。音频驱动里,应用层想设置采样率,就会发一个消息给驱动。驱动收到后处理,然后回复。这期间应用层是阻塞的,不会乱发指令。

// 发送端(应用层)
int msg = SET_SAMPLERATE;
int reply;
Send( driver_chid, &msg, &reply, sizeof(msg), sizeof(reply) );

// 接收端(驱动层)
int msg;
int rcvid = Receive( chid, &msg, sizeof(msg) );
// 处理消息...
Reply( rcvid, &reply, sizeof(reply) );

嗯,这里要注意:Send()的阻塞特性,意味着你不能在中断上下文里调用它。我在项目中遇到过有人想在ISR里发消息,结果系统直接挂掉——因为中断里不能阻塞。

我的习惯:在音频驱动的初始化阶段,我会先创建好通道(Channel),然后让音频服务线程一直阻塞在Receive()上。这样应用层随时可以发消息过来,驱动都能及时响应。

4.2 共享内存(Shared Memory)

消息传递虽然好用,但有个问题——数据拷贝。你想想看,如果音频数据每次都要从应用层拷贝到驱动层,那延迟和CPU开销都受不了。

这时候就需要共享内存了。

共享内存说白了就是让两个进程访问同一块物理内存。数据不用拷贝,直接读写。这在音频流处理里几乎是标配。

关键步骤:

  1. 一个进程用shm_open()创建共享内存对象
  2. mmap()映射到自己的地址空间
  3. 另一个进程用同样的名字shm_open(),再mmap()
  4. 两个进程就能直接读写同一块内存了
// 进程A(音频服务)
int fd = shm_open( "/audio_buffer", O_RDWR | O_CREAT, 0666 );
ftruncate( fd, BUFFER_SIZE );
void *buf = mmap( NULL, BUFFER_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0 );

// 进程B(音频驱动)
int fd = shm_open( "/audio_buffer", O_RDWR, 0666 );
void *buf = mmap( NULL, BUFFER_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0 );

但是,共享内存有个大坑——同步问题。两个进程同时写同一块内存,数据就乱了。

我曾经踩过的坑:在做一个多通道音频采集项目时,驱动往共享内存写数据,应用层读数据。没有加同步机制,结果音频数据里时不时出现爆音。查了两天才发现是读写冲突。

解决方案:用QNX的pthread_mutex配合pthread_cond做同步,或者用原子操作。我个人更推荐用消息传递来传递“数据就绪”的信号,共享内存只负责存数据本身。

4.3 脉冲(Pulse)

脉冲是QNX里一种轻量级的消息。它只有40字节,而且发送方不会阻塞。

你想想看,什么时候需要这种机制?中断处理!

硬件中断来了,ISR里不能阻塞,不能调用Send()。这时候脉冲就派上用场了。ISR里发一个脉冲给驱动线程,驱动线程收到后就知道“硬件有数据了”,然后去处理。

脉冲的特点:

  • 固定大小(40字节)
  • 发送方不阻塞(非阻塞)
  • 可以带8字节的附带数据
  • 常用于中断通知、状态变化通知
// 发送脉冲(ISR中)
struct sigevent event;
event.sigev_notify = SIGEV_PULSE;
event.sigev_coid = driver_coid;  // 连接ID
event.sigev_priority = 255;
event.sigev_code = AUDIO_IRQ_PULSE;
MsgSendPulse( driver_coid, 255, AUDIO_IRQ_PULSE, 0 );

// 接收脉冲(驱动线程)
struct _msg_info info;
int rcvid = Receive( chid, &msg, sizeof(msg) );
if( rcvid == 0 ) {
    // 这是一个脉冲
    int code = msg.pulse.code;
    if( code == AUDIO_IRQ_PULSE ) {
        // 处理音频中断
    }
}

嗯,这里有个细节:Receive()返回0表示收到的是脉冲,返回正数表示收到的是普通消息。这个判断很重要,我见过有人把脉冲当消息处理,结果数据全乱了。

我的建议:在音频驱动里,我会把脉冲优先级设高(比如255),确保中断通知能尽快被处理。普通消息的优先级可以低一些。这样紧急的音频中断不会被普通配置消息阻塞。

4.4 三种机制的配合使用

实际项目中,这三种IPC不是孤立用的。我通常这样搭配:

场景 IPC方式 原因
控制命令(设置采样率、启停) 消息传递 需要同步确认,保证命令执行完成
音频数据流 共享内存 大数据量,避免拷贝
硬件中断通知 脉冲 ISR中不能阻塞,需要轻量通知
数据就绪信号 脉冲 + 共享内存 脉冲通知“数据写好了”,共享内存存数据

举个例子。音频采集时,硬件产生中断,ISR发脉冲给驱动线程。驱动线程收到脉冲后,从DMA缓冲区把数据拷贝到共享内存。然后驱动再发一个脉冲给音频服务,告诉它“数据准备好了”。音频服务从共享内存读数据,处理完后发消息告诉驱动“可以继续采了”。

你看,三种IPC各司其职,配合得很默契。

注意:共享内存虽然快,但不要滥用。我见过有人把所有通信都改成共享内存,结果同步问题搞得一团糟。记住一个原则:控制走消息,数据走共享,中断走脉冲。

好了,这一章就到这里。IPC是QNX的基石,搞懂了这三种机制,你就能理解QNX音频架构里各个模块是怎么协作的了。下一章我们讲音频驱动的中断处理,到时候会大量用到脉冲,你会有更深的体会。