Skip to content

入门:程序与进程、PCB、线程与并发抽象

基于通用操作系统概念 · 核于 2026-08

速查

  • 程序 vs 进程:程序是磁盘上的静态可执行文件(代码+数据);进程是程序装入内存后的动态运行实例——多了 PCB、地址空间、堆栈、打开的文件表等运行时结构。一个程序可对应多个进程(同时开多个浏览器窗口)。
  • 进程(Process):OS 资源分配与保护的基本单位。每个进程有独立虚拟地址空间(代码段/数据段/堆/栈)、独立的打开文件表、独立 PID。进程间内存隔离,崩溃互不影响。
  • PCB(进程控制块):OS 为每个进程维护的描述与控制结构(Linux 叫 task_struct)。包含:PID/PPID、进程状态、寄存器现场(PC/SP/通用寄存器)、CPU 调度信息(优先级/时间片)、内存指针(页表基址)、打开文件表、记账信息。PCB 是进程存在的唯一标志——创建进程=创建 PCB,销毁进程=回收 PCB。
  • 进程三态机就绪(Ready)(万事俱备只欠 CPU)↔ 运行(Running)(占用 CPU 执行指令)→ 阻塞(Blocked/Waiting)(等 IO/信号/锁,暂时不能跑)。运行→就绪=时间片用完被抢占;运行→阻塞=主动等待(如 read);阻塞→就绪=等待的事件发生(IO 完成)。
  • 五态扩展:三态 + 新建(New)(正在分配 PCB/内存)+ 终止(Terminated)(执行完毕,PCB 待回收)。加上**挂起(Suspend)**到磁盘交换区即为七态。
  • 上下文切换(Context Switch):CPU 从执行 A 切到执行 B 的过程——保存 A 的现场(PCB 里)、恢复 B 的现场(从 B 的 PCB)、切换栈与页表。代价:寄存器/SP 保存、TLB flush、CPU 流水线冲刷,约数微秒。进程切换比线程切换贵(要换页表)。
  • 线程(Thread):进程内的执行单元,是 CPU 调度的基本单位。同进程的多个线程共享地址空间(代码/堆/全局数据/打开文件表),但各有独立的栈、PC、寄存器现场。线程切换不换页表,比进程切换便宜。
  • TCB(线程控制块):描述线程的结构(线程 ID、寄存器现场、栈指针、状态),同进程的 TCB 链在同一 PCB 下。
  • 用户级 vs 内核级线程用户级线程(用户态库管理,内核看不到,如早期 Java 绿色线程)切换快但不能多核并行(一个线程阻塞会阻塞整个进程);内核级线程(内核管理,一对一映射到调度实体)能多核并行但切换要陷入内核。
  • 多线程模型多对一(多用户线程映射一个内核线程,如早期 Solaris)切换快但不能并行且一个阻塞全阻塞;一对一(Linux pthread、Windows)每个用户线程对应一个内核线程,能并行;多对多(Go 的 goroutine 调度、Solaris M:N)灵活结合两者优点。
  • fork/execfork() 复制当前进程创建子进程(COW 复制地址空间,子进程 PID 不同、返回值 0);exec() 用新程序替换当前进程的代码段和数据段(PID 不变)。经典 fork+exec 模式启动新程序。
  • 僵尸进程(Zombie):子进程已退出但父进程尚未 wait() 回收,PCB 残留占用 PID;过多会耗尽 PID 表。孤儿进程(Orphan):父进程先死,子进程被 init(PID 1)收养继续运行,孤儿无害。
  • 进阶顺序进程生命周期与 PCB线程与多线程模型参考

一、程序 vs 进程:从静态文件到动态实例

程序与进程是初学者最容易混淆的概念,区别在于"静"与"动":

维度程序(Program)进程(Process)
本质磁盘上的静态可执行文件程序装入内存后的动态运行实例
组成代码 + 数据(静态)代码+数据 + PCB + 堆栈 + 地址空间 + 打开文件表
生命周期永久存在(直到删除)有创建、运行、终止的生命周期
资源不占 CPU/内存(只占磁盘)占 CPU 时间、内存、文件句柄
数量一个程序一个程序可对应多个进程实例(多开)
  • 多开:双击浏览器图标多次,磁盘上是同一个程序(chrome.exe),但内存里有多个进程,各自独立。
  • 进程 = 程序 + 执行上下文:执行上下文就是 PCB 里保存的寄存器现场、状态、资源清单。所以"进程是程序的一次执行"。

二、PCB:进程的"身份证"

进程控制块(PCB,Process Control Block)是 OS 管理、调度、控制进程的核心数据结构。Linux 中叫 task_struct(约 1000 多个字段)。PCB 是进程存在的唯一标志——创建进程就是创建 PCB,销毁进程就是回收 PCB。

  • 进程标识:PID(进程号)、PPID(父进程号)、UID(用户号)、GID(组号)。
  • 处理机状态(现场):CPU 寄存器现场(PC 程序计数器、SP 栈指针、PSW/EFLAGS 状态字、通用寄存器)——上下文切换时保存/恢复的就是这些。
  • 进程调度信息:状态(就绪/运行/阻塞)、优先级、调度策略、时间片剩余、等待的事件。
  • 进程控制信息:内存指针(页表基址 CR3、段表)、打开文件表、信号掩码、信号量、消息队列指针、与其它进程的链接(父子兄弟队列)。
  • 记账信息:CPU 时间、内存占用、IO 统计(用于计费与监控)。

PCB 集中存放:OS 维护若干队列(就绪队列、阻塞队列、不同优先级的运行队列),队列节点就是 PCB。切换进程 = 把当前 PCB 写回队列、从队列取出下一个 PCB 恢复现场。

三、进程状态机:就绪、运行、阻塞

进程在生命周期中于几个状态间转换,三态模型是核心:

        ┌──────── 时间片用完 / 被抢占 ────────┐
        ▼                                     │
     ┌──────┐   调度器选中    ┌──────┐
     │ 就绪 │ ─────────────▶ │ 运行 │
     │Ready │ ◀───────────── │Running│
     └──────┘    运行完      └──────┘
        ▲                  │  │
        │                  │  │ 主动等待(read/wait/锁)
        │ IO完成/事件发生   │  ▼
        └────────────── ┌──────┐
                       │ 阻塞 │
                       │Blocked│
                       └──────┘
  • 就绪(Ready):进程已具备运行条件(内存/资源齐备),只等 CPU。被组织在就绪队列里,等调度器挑选。
  • 运行(Running):进程占用 CPU 执行指令。单核上同时只有一个进程运行;多核上可有多个。
  • 阻塞(Blocked/Waiting/Sleep):进程在等待某事件(IO 完成、信号、锁释放、sleep),即使给它 CPU 也没法执行。
  • 关键转换
    • 就绪→运行:调度器选中(dispatch)。
    • 运行→就绪:时间片用完或被更高优先级抢占。
    • 运行→阻塞:进程主动请求等待(调用 read/wait/lock)——注意不会从就绪直接到阻塞。
    • 阻塞→就绪:等待的事件发生(IO 完成、锁释放),由内核唤醒——不会直接回运行。

四、上下文切换:进程切换的代价

**上下文切换(Context Switch)**是 CPU 从执行一个进程/线程切换到另一个的过程,是理解并发性能的关键:

  1. 保存现场:把当前进程的 CPU 寄存器(PC、SP、PSW、通用寄存器)保存到它的 PCB。
  2. 更新队列:把当前 PCB 移到就绪/阻塞队列,从就绪队列取出下一个 PCB。
  3. 恢复现场:把下一个进程的寄存器现场从它的 PCB 装回 CPU。
  4. 切换地址空间(进程切换才有):更新 CR3(页表基址寄存器)→ flush TLB(除非用 PCID)。
  5. CPU 流水线冲刷:跳到新 PC 后流水线要重新填充。
  • 进程切换 vs 线程切换:进程切换要换页表(TLB flush,最贵);同进程的线程切换不换页表(共享地址空间),只换栈和寄存器,便宜得多
  • 代价量级:进程切换约 数微秒(含 TLB 重建);线程切换约 百纳秒~微秒。看似小,但高并发下每秒万次切换会吃掉可观 CPU——所以别开太多线程。
  • 何时触发:时间片用完(时钟中断)、进程阻塞(IO/锁)、高优先级抢占、显式 yield

五、线程:进程内的执行单元

**线程(Thread)**是进程内部更细的执行单位。引入线程后,进程是资源单位,线程是调度单位

  • 共享部分:同进程的线程共享代码段、数据段、堆、打开文件表、信号处理函数——这正是线程间通信方便(直接读写共享变量)但需要同步(竞态)的根源。
  • 私有部分:每个线程有独立的(函数调用栈)、PC(执行到哪)、寄存器现场线程局部存储(TLS)errno
  • 为什么用线程:相比多进程,多线程①创建快(不复制地址空间);②切换便宜(不换页表);③通信方便(共享内存)。代价是共享带来竞态与崩溃扩散(一个线程写崩地址空间,全进程线程完蛋)。

六、用户级 vs 内核级线程

线程是否对内核可见,决定了它能否真正并行:

  • 用户级线程(ULT):由用户态线程库(如早期 Java 绿色线程、协程)管理,内核完全感知不到,整个进程被当作一个调度实体。优点:切换极快(不陷内核)、可定制调度策略;缺点:①一个线程阻塞(如 read)会阻塞整个进程;②不能利用多核(内核只分一个核给进程)。
  • 内核级线程(KLT):内核直接管理,每个线程是独立调度实体。优点:①能多核并行;②一个线程阻塞不影响其它。缺点:创建/切换要陷内核,开销大。Linux pthread、Windows 线程都是内核级。
  • 多线程模型:把用户线程映射到内核线程的方式——多对一(多用户线程对应一个内核线程,ULT 特性)、一对一(每个用户线程一个内核线程,KLT 特性)、多对多(M 个用户线程映射 N 个内核线程,兼顾两者,如 Go goroutine + GMP 调度)。

下一步

理解了进程/线程的基本概念后,下一步深入两条线——进程生命周期与 PCB(状态机详解、fork/exec/wait、僵尸与孤儿、上下文切换全过程与代价)与线程:轻量级进程与多线程模型(线程定义、TCB、用户级 vs 内核级、多线程模型、进程 vs 线程对比)。