3. 进程与线程管理:进程创建与销毁、线程调度策略、优先级管理、线程同步原语
好,咱们进入第三章。这一章可以说是QNX系统里最核心、最接地气的内容了。进程与线程管理,说白了就是操作系统怎么“管人”的。你想想看,一个系统里跑着几十上百个任务,谁先执行、谁后执行、谁跟谁抢资源,这些都得有个规矩。QNX的规矩,我个人觉得,设计得相当精巧。
3.1 进程创建与销毁
在QNX里创建进程,最常用的就是 spawn() 和 fork()。不过我得说,我实际项目中几乎不用 fork(),原因很简单——QNX是微内核,fork() 的语义在资源受限的嵌入式环境里容易出问题。
核心函数:spawn()
spawn() 是QNX推荐的进程创建方式。它直接加载一个新程序,不会复制父进程的地址空间。这在嵌入式系统里特别重要——你想想看,一个监控进程可能占了几十兆内存,如果每次创建子进程都复制一遍,内存很快就爆了。
// 一个典型的 spawn 调用
#include <spawn.h>
#include <sys/neutrino.h>
pid_t pid;
char *argv[] = {"/usr/bin/sensor_daemon", "-c", "/etc/sensor.conf", NULL};
struct inheritance inherit;
// 设置继承属性
inherit.flags = SPAWN_EXEC;
inherit.pgroup = 0; // 使用父进程的进程组
pid = spawn("/usr/bin/sensor_daemon", 0, NULL, &inherit, argv, NULL);
if (pid == -1) {
// 我曾经在这里踩过坑——忘记检查 errno
fprintf(stderr, "spawn failed: %s\n", strerror(errno));
}
嗯,这里要注意一点:spawn() 的第三个参数是 environ,传 NULL 表示继承父进程的环境变量。我建议你显式传递环境变量,避免子进程拿到一些不该有的东西。
3.2 线程调度策略
QNX支持三种调度策略:FIFO、RR(Round-Robin)和SPORADIC。这三种策略,说白了就是CPU时间怎么分的问题。
| 调度策略 | 特点 | 适用场景 |
|---|---|---|
| FIFO | 先到先服务,不抢占 | 短任务、紧急任务 |
| RR | 时间片轮转,同优先级轮流执行 | 计算密集型、需要公平调度的任务 |
| SPORADIC | 偶发调度,限制执行时间 | 实时系统中的偶发任务 |
3.2.1 FIFO 调度
FIFO,就是先进先出。一个线程拿到CPU后,除非它自己主动让出(比如调用 sched_yield() 或者阻塞),否则会一直运行到结束。我在做工业控制器时遇到过一个问题:一个FIFO线程里有个死循环,结果整个系统都卡死了。嗯,从那以后我规定:FIFO线程里绝对不能有长循环。
3.2.2 RR 调度
RR调度给每个线程分配一个时间片。时间片用完了,就轮到下一个同优先级的线程。这个策略适合那些需要公平调度的场景。我个人习惯把后台任务设为RR,前台实时任务用FIFO。
// 设置线程为RR调度
struct sched_param param;
param.sched_priority = 10;
param.sched_curpriority = 10;
param.sched_ss_low_priority = 0;
param.sched_ss_repl_period = {0, 0};
param.sched_ss_init_budget = {0, 0};
param.sched_ss_max_repl = 0;
pthread_setschedparam(pthread_self(), SCHED_RR, ¶m);
3.2.3 SPORADIC 调度
SPORADIC调度是QNX的特色。它专门为那些“平时不干活,偶尔忙一阵”的任务设计。比如一个温度监控线程,平时大部分时间都在休眠,只有温度超标时才需要紧急处理。
避坑指南:我曾经在SPORADIC调度上栽过跟头。当时没设置好 sched_ss_repl_period(补充周期),导致线程在紧急任务完成后,CPU时间没有及时补充。结果下一次紧急事件来了,线程却因为没有预算而无法执行。记住:补充周期一定要小于或等于紧急事件的最小间隔。
3.3 优先级管理
QNX的优先级范围是0到255,其中255最高,0最低。嗯,这里有个容易混淆的地方:数字越大优先级越高,跟某些RTOS正好相反。
优先级管理最核心的原则是:优先级反转。你想想看,一个高优先级线程等一个低优先级线程释放锁,而低优先级线程又被中等优先级线程抢占——这不就乱套了吗?
注意:QNX提供了优先级继承协议(Priority Inheritance Protocol)来避免优先级反转。使用 pthread_mutexattr_setprotocol() 设置 PTHREAD_PRIO_INHERIT 即可。我建议所有共享资源的互斥锁都开启这个选项,别省这点性能开销。
// 开启优先级继承
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &attr);
3.4 线程同步原语
线程同步,说白了就是让多个线程别打架。QNX提供了丰富的同步原语:互斥锁、条件变量、信号量、屏障等。
3.4.1 互斥锁(Mutex)
互斥锁是最基本的同步工具。但我得说,很多人用错了。最常见的错误是:在中断服务程序里使用互斥锁。记住:中断上下文不能阻塞,所以不能用互斥锁。
3.4.2 条件变量(Condition Variable)
条件变量用于“等待某个条件成立”。比如一个生产者-消费者模型:消费者等待队列非空,生产者等待队列非满。
// 典型的生产者-消费者模式
pthread_mutex_lock(&mutex);
while (queue_is_empty()) {
pthread_cond_wait(&cond, &mutex); // 等待条件
}
item = queue_pop();
pthread_mutex_unlock(&mutex);
注意那个 while 循环。为什么不用 if?因为 pthread_cond_wait() 可能被虚假唤醒(spurious wakeup)。我刚开始写多线程程序时就吃过这个亏——用 if 判断,结果队列还是空的就往下走了。
3.4.3 信号量(Semaphore)
信号量适合“计数”场景。比如控制对有限资源的访问:有5个网络连接,信号量初始值设为5,每用掉一个就减1,释放一个就加1。
个人经验:信号量在QNX里实现得很轻量,但要注意:信号量的等待操作(sem_wait())可能会阻塞。如果你在实时线程里用信号量,一定要考虑阻塞时间对实时性的影响。
3.4.4 屏障(Barrier)
屏障用于“所有线程都到达某一点后再继续”。比如一个并行计算任务,需要所有线程都完成第一阶段计算后,才能开始第二阶段。
// 初始化屏障,等待4个线程
pthread_barrier_t barrier;
pthread_barrier_init(&barrier, NULL, 4);
// 每个线程中
pthread_barrier_wait(&barrier); // 等待所有线程到达
嗯,这里要注意:屏障的计数必须和实际线程数一致。我曾经因为线程创建失败,导致屏障永远等不齐——那叫一个惨。
3.5 实战建议
最后,我总结几条实战经验:
- 线程优先级不要设太高:255是最高,但一般用到200就差不多了。留点余量给系统线程。
- 避免优先级反转:能用优先级继承就用,别省。
- 线程同步要小心:死锁、活锁、优先级反转,这些都是多线程编程的“三大坑”。
- 测试要全面:多线程问题往往在压力测试时才暴露。我建议用QNX的
Instrumented Kernel来检测死锁和竞争条件。
好了,这一章的内容就到这里。进程与线程管理是QNX实时性的基石,理解透了,后面的章节就好办了。下一章我们讲内存管理,到时候再聊。