第2章:QNX内核基础

好,咱们直接进入正题。QNX 最核心的东西,就是它的微内核架构。很多人一听到「微内核」就觉得高大上,其实说白了,就是把操作系统拆成最小的一坨,只留最必要的东西在内核里跑。

2.1 微内核架构详解

传统 Linux 那种叫宏内核,什么文件系统、网络协议栈、驱动,全塞进内核里。QNX 不一样,它只保留:进程调度、线程管理、IPC(进程间通信)、中断处理。就这四样,别的全扔到用户态去。

微内核 vs 宏内核(我个人的理解)

宏内核像一个大公司,所有部门都在一栋楼里,沟通快但一出事全完蛋。微内核像外包公司,核心只做调度和通信,其他功能都交给外部进程。出错了?重启那个进程就行,内核纹丝不动。

我在项目中遇到过一件事:某次客户现场,一个第三方驱动崩溃了。如果是 Linux,直接 panic 给你看。但在 QNX 上,我只需要 slay 掉那个驱动进程,再重新启动它,系统照常运行。嗯,这就是微内核的魅力。

2.2 进程与线程管理

QNX 里进程和线程的概念,跟 POSIX 标准基本一致。但有几个关键点,我建议你记牢。

2.2.1 进程

进程是资源分配的最小单位。每个进程有独立的地址空间。创建进程用 spawn()fork()。我个人习惯用 spawn(),因为它更灵活,可以直接指定路径和参数。

// 创建一个子进程
pid_t pid = spawn("my_driver", 0, NULL, NULL, &argv, NULL);
if (pid == -1) {
    perror("spawn failed");
}

2.2.2 线程

线程是 CPU 调度的最小单位。同一个进程内的线程共享地址空间。QNX 的线程调度策略有 FIFO、RR(轮转)、SPORADIC(零星调度)。

我的经验: 实时性要求高的任务,用 FIFO 策略。需要公平分时的,用 RR。SPORADIC 我很少用,除非是那种偶尔爆发一下的任务。

// 创建一个实时线程
pthread_t thread;
pthread_attr_t attr;
struct sched_param param;

pthread_attr_init(&attr);
pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
param.sched_priority = 50;
pthread_attr_setschedparam(&attr, &param);

pthread_create(&thread, &attr, my_thread_func, NULL);

2.3 进程间通信(IPC)概述

QNX 的 IPC 是它的灵魂。因为微内核把功能都拆到用户态了,所以进程之间必须频繁通信。QNX 提供了好几种 IPC 机制,我按使用频率排个序:

  1. 消息传递(最常用,也是 QNX 的招牌)
  2. 信号(轻量级通知)
  3. 共享内存(大数据量传输)
  4. 脉冲(Pulse,轻量级消息)
  5. 事件(Event,高级用法)

你想想看,为什么 QNX 这么强调 IPC?因为它的设计哲学就是「让进程之间好好说话」。我曾经调试过一个音频驱动,问题就出在 IPC 超时上。嗯,那会儿我花了整整两天才定位到是消息队列满了。

2.4 消息传递

消息传递是 QNX 最核心的 IPC 机制。它采用 同步阻塞 模式:发送方调用 MsgSend() 后会阻塞,直到接收方调用 MsgReceive() 并处理完,再调用 MsgReply() 回复。

避坑指南: 我曾经犯过一个错误——在中断服务程序里调用 MsgSend()。结果系统直接死锁。记住:中断上下文里绝对不能做阻塞 IPC 调用!

// 服务端代码
int chid = ChannelCreate(0);  // 创建通道
int rcvid = MsgReceive(chid, &msg, sizeof(msg), NULL);
// 处理消息...
MsgReply(rcvid, EOK, &reply, sizeof(reply));

// 客户端代码
int coid = ConnectAttach(0, pid, chid, 0, 0);
MsgSend(coid, &msg, sizeof(msg), &reply, sizeof(reply));

这里有个细节:ChannelCreate()ConnectAttach() 是一对。通道是服务端创建的,连接是客户端建立的。我刚开始学的时候老搞混,后来记住一句话:「服务端开通道,客户端连通道」。

2.5 信号

信号是一种异步通知机制。QNX 支持标准的 POSIX 信号,比如 SIGINT、SIGTERM。但说实话,在驱动开发里,我很少用信号。为什么?因为信号不可靠,容易丢失,而且处理起来有诸多限制。

我个人更推荐用 脉冲(Pulse) 来代替信号。脉冲是 QNX 特有的轻量级 IPC,只有 4 字节数据,但不会丢失,而且可以在中断中使用。

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

// 接收脉冲(在 MsgReceive 中处理)
if (msg->type == _PULSE_CODE) {
    // 处理脉冲
}

小技巧: 如果你需要在驱动里通知应用层「数据准备好了」,用脉冲比用信号靠谱得多。我所有的生产项目里,信号几乎只用来处理 Ctrl+C 退出。

2.6 共享内存

共享内存是最高效的 IPC 方式。数据不用拷贝,直接读写同一块物理内存。适合传输大量数据,比如摄像头图像、音频流。

使用步骤很简单:

  1. 一个进程创建共享内存对象(shm_open() + mmap()
  2. 其他进程通过名字打开同一个对象(shm_open() + mmap()
  3. 用信号量或互斥锁做同步
// 创建共享内存
int fd = shm_open("/my_shared_mem", 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("/my_shared_mem", O_RDWR, 0666);
void *ptr = mmap(0, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

注意: 共享内存本身不提供同步机制。你必须自己加锁。我曾经见过一个项目,两个进程同时写共享内存,结果数据全乱了。后来加了 pthread_mutex_lock() 才解决。

好了,这一章的内容就这些。微内核架构、进程线程、IPC 三大件——消息、信号、共享内存。这些都是 QNX 驱动开发的基石。下一章我们会讲中断处理和时间管理,那才是驱动开发真正硬核的地方。

记住一句话:在 QNX 里,一切皆 IPC。你写的驱动,本质上就是一个不断收发消息的进程。把这个想通了,后面就顺了。