3、进程与线程基础:进程与线程的概念、QNX进程状态模型、线程调度策略(FIFO、RR、SPORADIC)

3.1 进程与线程:别再傻傻分不清

说实话,我见过不少做嵌入式开发的朋友,干了三五年还搞不清进程和线程的区别。这其实不丢人,因为很多RTOS压根就没有进程的概念,上来就是线程。但到了QNX这里,你必须得搞清楚。

进程,说白了就是一个程序的运行实例。它有自己的地址空间、文件描述符、信号处理器。你想想看,每个进程都活在自己的小世界里,互不干扰。一个进程崩了,不会拖累别的进程。这就是QNX微内核设计的精髓——隔离性

线程呢?它是进程里的执行单元。一个进程可以有一个或多个线程,它们共享进程的地址空间和资源。我习惯把进程比作一栋楼,线程就是楼里的住户。楼塌了(进程崩溃),所有住户都完蛋。但住户之间串门(共享内存)很方便。

核心区别一句话:进程是资源分配的最小单位,线程是CPU调度的最小单位。

在QNX里,进程创建用fork()posix_spawn(),线程创建用pthread_create()。我建议你根据场景选:需要强隔离、高可靠性?用多进程。需要高性能、低延迟通信?用多线程。

3.2 QNX进程状态模型

QNX的进程状态模型,比Linux要精简一些。我个人觉得这样反而更好理解。主要有这么几个状态:

状态 含义 说明
READY 就绪 进程已准备好运行,等待CPU
RUNNING 运行 正在占用CPU执行
WAITING 等待 因某种资源不可用而阻塞
STOPPED 停止 收到SIGSTOP等信号暂停
ZOMBIE 僵尸 进程已退出,但父进程未回收

嗯,这里要注意:QNX里没有Linux那种「不可中断睡眠」状态。为什么?因为QNX是实时系统,不允许进程长时间卡死。如果一个进程在等资源,它要么在READY队列里,要么在WAITING队列里,就这么简单。

我曾经在项目中遇到过一个坑:一个高优先级进程因为等一个低优先级进程释放锁,结果两个都卡住了。这就是经典的优先级反转。QNX提供了优先级继承协议来解决,但前提是你得知道怎么用。

避坑指南:pidin命令可以查看进程状态。如果发现某个进程长期处于WAITING状态,多半是死锁或者资源泄漏了。我曾经排查过一个系统,就是靠pidin -p 12345定位到问题。

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

调度策略,说白了就是「下一个该谁上CPU」。QNX支持三种实时调度策略,加上一个默认的分时策略。我重点讲前三个,因为这才是实时系统的核心。

3.3.1 FIFO调度(先进先出)

FIFO,也叫SCHED_FIFO。它的规则很简单:同优先级线程,谁先就绪谁先跑,跑完才让下一个。除非被更高优先级的线程抢占,或者自己主动让出CPU(比如调用sched_yield()或阻塞)。

我个人的习惯是:对时间要求极严、执行时间短且确定的任务,用FIFO。比如中断处理线程、高频率控制循环。

// 设置FIFO调度策略
struct sched_param param;
param.sched_priority = 30;  // 优先级范围1-63
pthread_setschedparam(thread_id, SCHED_FIFO, ¶m);

注意:FIFO线程如果写了个死循环,同优先级的其他线程永远没机会跑。我见过有人把日志打印线程设成FIFO,结果日志量一大,整个系统都卡住了。嗯,这种场景用RR更合适。

3.3.2 RR调度(时间片轮转)

RR和FIFO很像,但多了一个时间片的概念。每个线程跑完自己的时间片(默认4毫秒),就会被放到同优先级队列的末尾,让下一个线程上。

说白了,RR就是FIFO加了个「限时强制切换」。适合多个同等重要的任务轮流执行,比如多个传感器数据采集线程。

// 设置RR调度策略
struct sched_param param;
param.sched_priority = 20;
pthread_setschedparam(thread_id, SCHED_RR, ¶m);

你想想看,如果三个RR线程优先级都是20,每个跑4ms就切换,大家雨露均沾。但如果有更高优先级的FIFO线程就绪了,RR线程也得靠边站。优先级才是王道。

3.3.3 SPORADIC调度(零星调度)

这个策略比较特殊,QNX对它的实现也很有特色。SPORADIC是专门为偶发性的实时任务设计的。比如一个任务平时不干活,但一旦触发,必须在规定时间内完成。

它的核心参数有四个:

  • 预算(budget):每个周期内允许的执行时间
  • 周期(period):预算的补充周期
  • 高优先级:正常执行时的优先级
  • 低优先级:预算耗尽后的备用优先级

我举个例子你就明白了。假设一个SPORADIC线程,预算10ms,周期100ms,高优先级40,低优先级10。它跑完10ms后,优先级降到10,直到100ms周期结束才恢复。这样既保证了实时性,又不会饿死其他低优先级任务。

// 设置SPORADIC调度策略
struct sched_param param;
param.sched_priority = 40;           // 高优先级
param.sched_ss_low_priority = 10;    // 低优先级
param.sched_ss_repl_period.tv_sec = 0;
param.sched_ss_repl_period.tv_nsec = 100000000; // 100ms周期
param.sched_ss_init_budget.tv_sec = 0;
param.sched_ss_init_budget.tv_nsec = 10000000;  // 10ms预算
pthread_setschedparam(thread_id, SCHED_SPORADIC, ¶m);

我的经验:SPORADIC最适合「平时闲着,一忙就猛干」的任务。比如网络协议栈的定时器处理、看门狗刷新。我曾经在一个车载项目中用SPORADIC调度CAN报文处理线程,效果比FIFO好得多——既保证了紧急报文的及时响应,又不会把CPU占死。

3.4 调度策略怎么选?

我整理了一个简单的选择指南,你直接照着用就行:

场景 推荐策略 理由
高频率、短执行、确定性强 FIFO 减少切换开销,保证最低延迟
多个同等重要的周期性任务 RR 公平分配CPU时间
偶发实时任务,怕饿死别人 SPORADIC 预算机制保护低优先级任务
普通非实时任务 分时(默认) 系统自动调整优先级

最后说一句:调度策略不是设完就完事了。你得用pthread_getschedparam()确认设置是否生效。我曾经在某个BSP上发现,内核配置把SPORADIC给关了,结果设置返回成功但实际跑的还是分时调度。嗯,排查了一整天才发现。

进程和线程是QNX的基石,调度策略是实时系统的灵魂。把这些搞透了,你写出来的代码才敢说「可靠」二字。