3. 进程与线程基础:PCB、TCB与生命周期

好,咱们今天聊聊进程和线程最核心的几个数据结构。说实话,很多做嵌入式开发的朋友,写了几年代码,对PCB和TCB的理解还停留在“概念层面”。我个人觉得,不理解这些结构体,你很难真正驾驭QNX这样的硬实时系统。

3.1 进程控制块(PCB)—— 进程的“身份证”

每个进程在QNX内核里都有一个对应的PCB。你可以把它想象成进程的“户口本”。内核靠它来管理所有进程。

PCB里存了什么?我列几个关键字段:

  • 进程ID(PID):唯一标识,就像身份证号。
  • 进程状态:运行、就绪、阻塞、僵死等。
  • 内存映射信息:代码段、数据段、堆栈的地址范围。
  • 文件描述符表:打开了哪些文件、管道、设备。
  • 信号处理表:每个信号对应的处理函数。
  • 调度参数:优先级、调度策略、时间片。
  • 父子进程指针:用于维护进程树。

嗯,这里要注意,QNX的PCB设计得非常紧凑。我当年在做一个车载项目时,系统里同时跑了40多个进程,每个PCB只占不到1KB。你想想看,如果每个PCB都膨胀到几KB,内存压力就大了。

核心要点:PCB是内核视角的进程描述。它不包含线程信息,线程信息在TCB里。

3.2 线程控制块(TCB)—— 真正的执行单元

QNX是纯微内核,线程才是调度单位。每个线程都有一个TCB。我习惯把TCB叫做“执行上下文”。

TCB里最重要的东西:

  • 寄存器上下文:PC、SP、通用寄存器。这是线程切换时保存/恢复的关键。
  • 线程状态:RUNNING、READY、BLOCKED、SUSPENDED。
  • 所属进程指针:指向父PCB。
  • 栈指针:用户栈和内核栈的地址。
  • 调度信息:当前优先级、剩余时间片。
  • 同步对象等待队列:如果线程在等mutex或condvar,这里会记录。

我曾经踩过一个坑:在QNX 6.5上,一个线程因为栈溢出,把TCB里的调度信息给覆盖了。结果系统调度器直接崩溃。从那以后,我每次创建线程都会显式设置栈大小,绝不用默认值。

避坑指南:我曾经见过有人把PCB和TCB混为一谈。记住:PCB是进程的,TCB是线程的。一个PCB可以挂多个TCB。在QNX里,进程是资源容器,线程才是干活的人。

3.3 PCB与TCB的关系

说白了,PCB和TCB是一对多的关系。一个进程至少有一个主线程,也可以有多个子线程。

我画个简单的结构图给你看:

PCB (进程控制块)
├── PID: 1234
├── 内存映射
├── 文件描述符表
└── 线程列表
    ├── TCB 1 (主线程)
    │   ├── 寄存器上下文
    │   ├── 栈指针
    │   └── 调度信息
    ├── TCB 2 (工作线程)
    │   ├── 寄存器上下文
    │   ├── 栈指针
    │   └── 调度信息
    └── TCB 3 (定时器线程)
        ├── 寄存器上下文
        ├── 栈指针
        └── 调度信息

你想想看,当内核做线程切换时,它只操作TCB,根本不管PCB。只有进程创建、销毁、内存管理这些操作,才会动到PCB。

3.4 进程生命周期

进程从出生到死亡,一共经历几个状态。我按QNX的实际流程给你捋一遍:

状态 说明 触发条件
NEW 进程刚被创建,PCB已分配,但还没就绪 调用fork()或spawn()
READY 进程已就绪,等待调度器分配CPU 资源就绪,进入就绪队列
RUNNING 正在CPU上执行 调度器选中该进程
BLOCKED 等待某个事件(I/O、信号、锁) 调用阻塞式API
STOPPED 被信号暂停 收到SIGSTOP等
ZOMBIE 进程已退出,但PCB还没被父进程回收 调用exit(),父进程未wait()

嗯,这里有个细节。QNX里没有真正的“僵死”状态?其实有的。我记得有一次调试一个服务进程,发现它退出后PID一直占着。查了半天,原来是父进程忘了调用waitpid()。这就是典型的ZOMBIE。

个人经验:我建议你在写QNX服务程序时,一定要注册SIGCHLD信号处理函数,或者在主循环里定期调用waitpid()。否则系统里会积累一堆僵尸进程,最终导致无法创建新进程。

3.5 线程生命周期

线程的生命周期和进程类似,但更轻量。线程的状态包括:

  • RUNNING:正在执行
  • READY:就绪等待调度
  • BLOCKED:等待同步对象、消息、信号
  • INACTIVE:线程已创建但还没启动
  • DEAD:线程已退出,资源待回收

线程的创建和销毁比进程快得多。我做过测试,在QNX 7.0上,创建一个线程大约只需要几微秒,而创建一个进程需要几十微秒。所以,如果你需要并发执行多个任务,用线程比用进程划算。

但是,线程也有代价。所有线程共享同一个地址空间,一个线程的野指针可能搞垮整个进程。我曾经在一个多线程数据采集项目里,因为一个线程的缓冲区溢出,导致另一个线程的控制结构被破坏。排查了整整两天才找到根因。

总结一下:

  • PCB是进程的“户口本”,TCB是线程的“工作证”
  • 进程是资源容器,线程是执行单元
  • 进程状态有6种,线程状态有5种
  • 创建线程比创建进程快10倍以上
  • 线程共享地址空间,要小心内存越界

好,这一章就到这里。下一章咱们聊聊QNX的调度策略,看看内核是怎么决定“谁先跑”的。到时候我会分享一些实际项目中的调度调优经验。