4、进程与线程管理:进程创建与销毁、线程调度策略与优先级反转

好,咱们进入第四章。这一章我打算聊聊 QNX 里最核心的东西——进程和线程的管理。说实话,我早年刚接触 QNX 时,觉得它跟 Linux 差不多,后来踩了几个坑才明白,这俩在实时性上的设计哲学完全不同。你想想看,一个跑在嵌入式设备上的系统,如果进程创建慢了,或者线程调度出了岔子,那可不是蓝屏重启那么简单,搞不好就是设备失灵、产线停摆。

4.1 进程的创建与销毁:从 fork() 到 procmgr_*

在 QNX 里,进程创建不像 Linux 那样用 fork()。QNX 是微内核,进程管理走的是 POSIX 的 spawn 族函数。我个人习惯用 posix_spawn(),它比 fork() 更轻量,也更能体现 QNX 的设计思路——你不需要复制整个地址空间,直接指定新进程的路径、参数和环境就行。

核心函数:

  • posix_spawn() / posix_spawnp():创建新进程,指定可执行文件路径
  • procmgr_spawn():QNX 原生接口,更底层,支持更多进程属性控制
  • fork() + exec():虽然能用,但我不推荐,尤其在资源受限的嵌入式环境

举个例子,我在一个车载项目中需要动态启动一个传感器采集进程,代码大概是这样:

#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_SETGROUP;
inherit.pgroup = 0;

int ret = posix_spawn(&pid, "/usr/bin/sensor_daemon", NULL, NULL, argv, NULL);
if (ret != 0) {
    // 我曾经在这里吃过亏——没检查 errno,结果进程没起来,系统也没报错
    fprintf(stderr, "spawn failed: %s\n", strerror(errno));
}

销毁进程呢?QNX 里用 kill() 发 SIGTERM 或 SIGKILL。但要注意,直接 SIGKILL 可能造成资源泄漏。我建议先发 SIGTERM,给进程一个清理的机会。如果它 3 秒内没反应,再上 SIGKILL。嗯,这里有个小技巧:用 TimerTimeout() 配合 kill() 实现优雅超时。

避坑指南:我曾经在调试一个多进程通信的系统时,发现子进程退出后,父进程还在傻等。后来才意识到,QNX 里子进程退出会发 SIGCHLD,但如果你没注册信号处理函数,或者没调用 waitpid(),子进程就会变成僵尸。记住:waitpid() 一定要调,或者用 WNOHANG 非阻塞轮询。

4.2 线程调度策略:FIFO、RR 与 SPORADIC

QNX 的线程调度是它的看家本领。它支持三种实时调度策略:FIFO、RR(轮转)和 SPORADIC(零星调度)。你想想看,一个系统里可能有几十个线程在跑,谁先跑、跑多久、跑完怎么办,这些策略就是用来回答这些问题的。

策略 特点 适用场景
SCHED_FIFO 先到先服务,直到主动让出或被更高优先级抢占 短任务、高优先级中断处理
SCHED_RR 时间片轮转,同优先级线程轮流执行 多个同等重要的周期性任务
SCHED_SPORADIC 允许低优先级线程在预算内临时提升优先级 偶发高负载、软实时任务

FIFO 策略:说白了就是排队。高优先级的线程先跑,跑完了或者自己阻塞了,才轮到下一个。我在一个工业控制器项目中用过 FIFO,当时有个电机控制线程必须抢占一切资源,我就给它设了最高优先级 + FIFO。效果很好,但要注意:如果这个线程不主动让出 CPU,其他同优先级线程就永远没机会跑。

RR 策略:每个线程分一个时间片,时间到了就换下一个。适合那些“大家轮流干活”的场景。比如一个数据采集系统,三个传感器线程优先级相同,用 RR 就能保证每个传感器都能定期被采样到。

SPORADIC 策略:这个比较有意思。它允许一个低优先级线程在“预算”内临时提升优先级。我记得有一次做音频处理,有个后台任务偶尔需要高优先级处理突发数据,但平时不能影响主线程。用 SPORADIC 就完美解决了——给它设一个预算(比如 5ms 的高优先级时间),用完了就降回去。

个人经验:设置调度策略时,别忘了调用 pthread_attr_setschedpolicy()pthread_attr_setschedparam()。我见过有人只设了策略没设优先级,结果线程跑在默认优先级上,跟普通线程抢资源,调试了半天才发现。

#include <pthread.h>
#include <sched.h>

pthread_attr_t attr;
struct sched_param param;

pthread_attr_init(&attr);
pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
param.sched_priority = 30;  // 优先级范围 1-255,数字越大优先级越高
pthread_attr_setschedparam(&attr, &param);

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

4.3 优先级反转:一个经典问题

优先级反转,说白了就是一个高优先级线程被一个低优先级线程“卡住”了。为什么会这样?因为低优先级线程拿着高优先级线程需要的资源(比如互斥锁)。更糟的是,如果中间还有个中优先级线程在跑,低优先级线程就永远没机会释放锁,高优先级线程就只能干等。

我在一个机器人项目中遇到过这个坑。一个高优先级的运动控制线程要访问共享内存,但低优先级的日志线程正拿着锁写日志。结果一个中优先级的网络线程抢占了 CPU,日志线程跑不了,锁释放不了,运动控制线程直接超时,机器人差点撞墙。

4.4 解决方案:优先级继承与优先级天花板

QNX 提供了两种机制来解决优先级反转:

  • 优先级继承(Priority Inheritance):当低优先级线程持有高优先级线程需要的锁时,低优先级线程临时“继承”高优先级线程的优先级,直到释放锁。这样中优先级线程就抢不过它了。
  • 优先级天花板(Priority Ceiling):每个锁都有一个“天花板优先级”,任何线程拿到锁后,优先级自动提升到天花板级别。这能防止更复杂的嵌套锁场景。

在 QNX 里,用 pthread_mutexattr_setprotocol() 来设置:

pthread_mutexattr_t mattr;
pthread_mutexattr_init(&mattr);
// 启用优先级继承
pthread_mutexattr_setprotocol(&mattr, PTHREAD_PRIO_INHERIT);
// 或者启用优先级天花板
// pthread_mutexattr_setprotocol(&mattr, PTHREAD_PRIO_PROTECT);
// pthread_mutexattr_setprioceiling(&mattr, 40);

pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &mattr);

关键点:

  • 优先级继承是动态的,只在冲突发生时生效,开销较小
  • 优先级天花板是静态的,需要你提前知道每个锁的最高使用优先级
  • 我建议在大多数嵌入式场景中用优先级继承,因为它更灵活,不容易出错

避坑指南:我曾经在一个多核系统上用过优先级继承,结果发现它只在单核上有效。多核环境下,两个线程可能跑在不同核上,优先级继承就管不了了。这时候你需要用 pthread_spinlock_t 或者 QNX 的 SyncMutex 加上 CPU 亲和性设置。嗯,这个坑我踩了整整两天才爬出来。

4.5 实战建议:如何选择调度策略

说了这么多,到底怎么选?我一般遵循这几个原则:

  1. 硬实时任务(比如电机控制、中断响应):用 SCHED_FIFO,优先级设高,确保第一时间响应
  2. 软实时周期性任务(比如传感器采样、数据打包):用 SCHED_RR,时间片设合理,保证公平
  3. 偶发高负载任务(比如日志写入、网络重连):用 SCHED_SPORADIC,平时不抢资源,关键时刻能顶上
  4. 非实时任务(比如配置加载、UI 更新):用默认的 SCHED_OTHER,别跟实时线程抢优先级

最后提醒一句:调试线程调度问题时,pidin 命令是你的好朋友。用 pidin -p <pid> -t 可以看每个线程的优先级、策略和状态。我每次遇到调度问题,第一件事就是跑 pidin,看看线程是不是卡在某个状态上。

好,这一章就到这里。下一章咱们聊聊内存管理,那又是一个容易踩坑的地方。