4. QNX进程与线程模型

进程和线程,是实时系统的骨架。在 QNX 里,这两块玩不转,后面 CAN 总线仿真跑起来肯定要出幺蛾子。今天咱们就把进程创建、销毁,还有那三种调度策略——FIFO、RR、SPORADIC,以及优先级管理,彻底捋清楚。

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

QNX 是 POSIX 兼容的,所以传统的 fork() 能用。但我个人习惯,在嵌入式场景下,更推荐用 spawn() 系列函数。为什么?因为 fork() 会复制整个进程地址空间,在资源受限的板子上,这开销可不小。

核心区别:

  • fork():创建子进程,复制父进程所有内存。简单,但慢,且吃内存。
  • spawn():直接加载一个新程序,不复制父进程空间。快,省资源,适合嵌入式。

我曾经在一个项目中,用 fork() 创建几十个监控进程,结果内存直接爆了。后来换成 posix_spawn(),问题迎刃而解。你想想看,一个 CAN 总线节点上可能挂几十个传感器,每个传感器一个进程,用 fork() 复制一遍,那还得了?

看个例子,用 posix_spawn() 启动一个 CAN 数据采集进程:

#include <spawn.h>
#include <sys/wait.h>

pid_t pid;
posix_spawnattr_t attr;
posix_spawnattr_init(&attr);

// 设置调度策略和优先级,后面会细讲
struct sched_param param;
param.sched_priority = 30;
posix_spawnattr_setschedpolicy(&attr, SCHED_FIFO);
posix_spawnattr_setschedparam(&attr, &param);
posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETSCHEDULER);

char *argv[] = {"/usr/bin/can_read", "can0", NULL};
char *envp[] = {NULL};

int ret = posix_spawn(&pid, "/usr/bin/can_read", NULL, &attr, argv, envp);
if (ret == 0) {
    printf("子进程 PID: %d 已创建\n", pid);
} else {
    printf("创建失败,错误码: %d\n", ret);
}

posix_spawnattr_destroy(&attr);

销毁进程呢?用 kill() 发个 SIGTERM 信号,或者 SIGKILL 强制干掉。但要注意,强制杀进程前,最好先让它自己清理资源,比如关闭 CAN 套接字。

避坑指南: 我曾经在调试时,直接用 SIGKILL 杀掉一个正在写 CAN 帧的进程,结果导致 CAN 控制器状态没复位,下次启动时总线一直报错。后来我改成先发 SIGTERM,在信号处理函数里做清理,再 exit(0),就稳了。

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

QNX 是个硬实时系统,调度策略是它的灵魂。三种策略,各有各的脾气。

策略 调度方式 适用场景 我踩过的坑
SCHED_FIFO 先入先出,不抢占直到主动让出或阻塞 高优先级、短任务、不可打断的关键操作 一个线程死循环,整个系统卡死
SCHED_RR 时间片轮转,同优先级轮流执行 多个同优先级任务,需要公平分配 CPU 时间片设太短,上下文切换开销大
SCHED_SPORADIC 偶发调度,限制高优先级任务的执行时间 偶发紧急任务,防止饿死低优先级任务 参数没算好,紧急任务被限流了

说白了,FIFO 就是「谁先来谁先跑,跑完才让位」。RR 是「大家轮流跑,每人一小段」。SPORADIC 是「你优先级高,但别太贪,跑一会儿就得歇歇」。

4.2.1 SCHED_FIFO:霸道总裁模式

FIFO 策略下,一个线程只要不主动阻塞(比如等消息、等 I/O),或者不被更高优先级的线程抢占,它就会一直运行。这在处理 CAN 总线的高优先级错误帧时非常有用。我记得有一次,CAN 总线出现大量错误帧,我专门开了一个 FIFO 线程,优先级设到最高,专门处理总线错误恢复,效果立竿见影。

小技巧: 用 FIFO 时,一定要在关键路径上加 sched_yield() 或者阻塞调用,否则同优先级的其他线程永远没机会跑。我曾经见过一个同事,FIFO 线程里写了个 while(1) 空转,结果整个系统其他任务全部饿死。

4.2.2 SCHED_RR:公平轮转

RR 就是在 FIFO 基础上加了时间片限制。每个线程跑完自己的时间片,如果没阻塞,就挂到同优先级队列末尾,换下一个跑。时间片默认是 4 毫秒,但可以调。

我建议,在 CAN 总线仿真中,多个数据发送线程可以用 RR。比如三个线程分别发 ID 0x100、0x200、0x300 的报文,用 RR 策略,每个线程轮流发送,总线负载更均衡。

#include <pthread.h>

pthread_attr_t attr;
struct sched_param param;

pthread_attr_init(&attr);
pthread_attr_setschedpolicy(&attr, SCHED_RR);
param.sched_priority = 20;
pthread_attr_setschedparam(&attr, &param);

// 创建线程
pthread_t thread;
pthread_create(&thread, &attr, can_send_task, NULL);

4.2.3 SCHED_SPORADIC:偶发救火队

这个策略有点意思。它允许一个高优先级线程运行一段时间(称为「爆发时间」),然后强制降到低优先级,让其他线程有机会运行。等冷却期过了,再恢复高优先级。

为什么会这样?你想想看,如果一个高优先级线程处理突发数据,比如 CAN 总线上的诊断请求,它可能平时没事,但一来就是一大波数据。如果用 FIFO,它可能长时间霸占 CPU,导致其他周期性任务(比如发送心跳报文)被饿死。SPORADIC 就是解决这个问题的。

参数配置要点:

  • sched_ss_low_priority:低优先级值,爆发期结束后降到的优先级。
  • sched_ss_max_repl:最大补充次数,防止无限爆发。
  • sched_ss_repl_period:补充周期,多久能恢复一次高优先级。
  • sched_ss_init_budget:初始爆发预算,每次能跑多久。

我曾经在一个项目中,用 SPORADIC 处理 CAN 总线上的固件升级数据流。升级时数据量巨大,但平时几乎没流量。用 SPORADIC 后,升级线程爆发时能快速处理数据,爆发结束后自动让出 CPU,不影响其他控制任务。嗯,这里要注意,预算和周期一定要算准,否则要么升级太慢,要么影响其他任务。

4.3 优先级管理:数字越小越重要?错!

QNX 里,优先级范围是 0 到 255。但注意,数字越大,优先级越高。255 是最高,0 是最低。这和很多 RTOS 相反,刚接触时容易搞混。

我刚开始做 QNX 时,习惯性地把最高优先级设成 1,结果发现低优先级的线程反而先跑。查了半天文档,才反应过来——QNX 是反的!

优先级范围 典型用途 说明
255 (最高) 中断处理、时钟同步 慎用,会饿死所有其他任务
200 - 254 高实时任务,如 CAN 错误恢复 我一般把 CAN 关键帧处理放这里
100 - 199 普通实时任务,如周期性数据发送 RR 策略常用这个区间
1 - 99 非实时任务,如日志记录、界面刷新 优先级低,但不会被饿死
0 (最低) 空闲任务 系统空闲时运行

重要提醒: 不要把多个线程设成同一个高优先级,除非你用 RR 策略。否则 FIFO 策略下,第一个线程会一直跑,后面的永远没机会。我曾经在 CAN 总线仿真中,把三个发送线程都设成优先级 200,用的 FIFO,结果只有第一个线程在发数据,其他两个干瞪眼。后来改成 RR,或者把优先级错开,才正常。

4.4 知识体系总览:一张图看懂

下面这张图,把进程线程管理、调度策略、优先级的关系串起来了。你可以把它当作本章的思维导图。

QNX 进程与线程管理知识体系 QNX 进程与线程管理核心 进程与线程管理 进程创建与销毁 线程调度策略 优先级管理 fork() 复制 spawn() 加载 SCHED_FIFO SCHED_RR SCHED_SPORADIC 0(低) → 255(高) 进程 PID 管理 时间片/爆发预算 优先级继承/置顶 核心原则:高优先级线程不应长时间霸占 CPU SPORADIC 策略是解决优先级反转的利器

这张图把本章的核心知识点都串起来了。从进程创建的两个路径,到三种调度策略,再到优先级范围,最后落到几个关键属性上。你可以把它当作一个快速参考。

实战建议: 在 CAN 总线仿真项目中,我通常这样分配:

  • 总线错误恢复线程:SCHED_FIFO,优先级 250
  • 关键报文发送线程:SCHED_RR,优先级 200,时间片 2ms
  • 非关键数据记录线程:SCHED_OTHER(普通分时),优先级 50

这样既保证了实时性,又不会让高优先级线程饿死其他任务。