3. 进程与线程管理:进程创建与销毁、线程调度策略、优先级管理、同步与互斥
好,咱们今天聊聊QNX里最核心的东西——进程和线程管理。说实话,这部分内容我每次给团队新人培训时都会反复讲。因为搞不懂这个,你在QNX上写驱动就是瞎忙活。
QNX是个真正的实时操作系统。它的进程和线程管理,跟Linux那套有本质区别。我个人习惯把QNX的进程模型想象成一个「微内核的细胞结构」——每个进程都是独立的细胞,线程就是细胞里的工作单元。嗯,这个比喻虽然不完美,但能帮你快速建立直觉。
3.1 进程创建与销毁
在QNX里创建进程,最常用的就是 spawn() 和 posix_spawn()。我刚开始用QNX时,总想着用 fork(),结果发现QNX的 fork() 跟Linux不太一样——它没有写时复制(COW)机制。
fork() 会完整复制父进程的地址空间。这在嵌入式系统里是很大的开销。我建议你尽量用 posix_spawn() 来创建新进程。
来看个实际例子:
#include <spawn.h>
#include <sys/neutrino.h>
pid_t pid;
char *argv[] = {"/usr/bin/my_driver", "-d", "/dev/servo", NULL};
struct inheritance inherit;
// 设置继承属性
inherit.flags = SPAWN_SETGROUP;
inherit.pgroup = 0;
inherit.sched = SCHED_RR; // 指定调度策略
inherit.sched_param.sched_priority = 20;
// 创建进程
int ret = posix_spawn(&pid, "/usr/bin/my_driver", NULL, NULL, argv, NULL);
if (ret != EOK) {
// 我曾经在这里踩过坑——忘记检查返回值
fprintf(stderr, "spawn failed: %s\n", strerror(ret));
return -1;
}
// 等待进程结束
int status;
waitpid(pid, &status, 0);
销毁进程呢?说白了就是发个信号。QNX里用 SignalKill() 或者 kill()。但要注意,直接 kill -9 可能会造成资源泄漏。我建议的做法是:
- 先发
SIGTERM,给进程一个清理的机会 - 等几秒钟,如果还没退出,再发
SIGKILL
atexit() 处理函数,专门用来释放中断向量、取消消息通道。你想想看,如果进程被强制杀掉,这些资源没释放,下次启动时就会出问题。
3.2 线程调度策略
QNX支持三种调度策略:SCHED_FIFO、SCHED_RR 和 SCHED_SPORADIC。嗯,这里要重点说说。
| 策略 | 特点 | 适用场景 |
|---|---|---|
| SCHED_FIFO | 先入先出,不抢占同优先级 | 短任务、中断处理 |
| SCHED_RR | 时间片轮转,同优先级轮流执行 | 计算密集型任务 |
| SCHED_SPORADIC | 偶发调度,适合周期性任务 | 多媒体、控制环路 |
我个人最常用的是 SCHED_RR。为什么?因为 SCHED_FIFO 有个坑——如果一个高优先级线程死循环了,所有同优先级和低优先级的线程都别想跑。我曾经在一个电机控制项目里就遇到过这事,一个线程的while循环忘了加延时,结果整个系统都卡死了。
// 设置线程调度策略
struct sched_param param;
param.sched_priority = 30;
pthread_t thread;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setschedpolicy(&attr, SCHED_RR);
pthread_attr_setschedparam(&attr, ¶m);
pthread_create(&thread, &attr, my_thread_func, NULL);
3.3 优先级管理
QNX的优先级范围是0到255。0是最低,255是最高。但说实话,你很少会用到255——那是给内核保留的。用户空间线程一般用1到63。
我建议你遵循这几个规则:
- 中断处理线程:优先级50-63
- 控制环路:优先级40-49
- 数据采集:优先级30-39
- 日志/监控:优先级10-19
- 后台任务:优先级1-9
为什么会这样分?你想想看,中断处理必须最快响应,控制环路次之,日志慢一点没关系。我曾经见过一个项目,把所有线程都设成优先级50,结果系统响应时间完全不可控——说白了就是优先级没拉开差距。
3.4 同步与互斥
这部分是驱动开发的重灾区。QNX提供了多种同步机制:
- 互斥锁(Mutex):最常用,配合条件变量使用
- 信号量(Semaphore):适合资源计数场景
- 屏障(Barrier):多线程同步点
- 消息传递(Message Passing):QNX的特色,进程间通信首选
我重点说说互斥锁。在QNX里,我强烈建议你用 pthread_mutex_t 而不是自己用原子操作实现锁。为什么?因为QNX的mutex实现了优先级继承,能有效避免优先级反转。
pthread_mutex_t lock;
pthread_mutexattr_t attr;
// 设置优先级继承属性
pthread_mutexattr_init(&attr);
pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);
pthread_mutex_init(&lock, &attr);
// 使用
pthread_mutex_lock(&lock);
// 临界区代码
pthread_mutex_unlock(&lock);
pthread_mutex_trylock() 配合while循环来实现「非阻塞等待」。结果发现CPU占用率飙升到100%。后来改用 pthread_mutex_timedlock() 才解决问题。记住,轮询是实时系统的大敌。
消息传递是QXN的独门绝技。它比共享内存更安全,因为消息传递是同步的——发送方会阻塞直到接收方处理完。这天然解决了同步问题。
// 发送消息
int chid = ChannelCreate(0);
int coid = ConnectAttach(0, pid, chid, 0, 0);
// 发送并等待回复
struct my_msg msg = {.type = READ_SENSOR, .data = 0};
int ret = MsgSend(coid, &msg, sizeof(msg), &reply, sizeof(reply));
嗯,这里要注意:消息传递虽然好用,但性能不如共享内存。如果你需要高频数据交换(比如每秒几千次),还是用共享内存加互斥锁更合适。
最后说一句,同步机制的选择没有银弹。我一般遵循这个原则:
- 进程内线程同步:用互斥锁+条件变量
- 进程间同步:用消息传递
- 高频数据交换:用共享内存+信号量
- 中断与线程同步:用脉冲(Pulse)
好了,进程线程管理这部分就讲到这里。记住,QNX的实时性不是靠运气,而是靠你对这些机制的深入理解。下一节我们聊聊内存管理,那又是另一个有意思的话题。