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开销都受不了。
这时候就需要共享内存了。
共享内存说白了就是让两个进程访问同一块物理内存。数据不用拷贝,直接读写。这在音频流处理里几乎是标配。
关键步骤:
- 一个进程用
shm_open()创建共享内存对象 - 用
mmap()映射到自己的地址空间 - 另一个进程用同样的名字
shm_open(),再mmap() - 两个进程就能直接读写同一块内存了
// 进程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音频架构里各个模块是怎么协作的了。下一章我们讲音频驱动的中断处理,到时候会大量用到脉冲,你会有更深的体会。