第 17 讲:进程、线程与状态转换
程序是磁盘上静态的指令和数据,进程是“这份程序正在某份资源和 CPU 现场上执行”。操作系统不能只管代码文件,它要管理每一次动态执行,所以需要进程这个抽象。
课件还区分作业:作业是用户提交给系统的一项完整工作,通常带程序、数据和控制说明;高级调度接纳一个作业后,它可以在执行阶段建立一个或多个进程。于是“程序”是静态内容,“作业”是提交/管理单位,“进程”是正在推进并竞争资源的执行实体。
并发不等于并行
- 顺序:活动 A 结束后 B 才开始;
- 并发:A 和 B 的生命周期在时间上重叠,可能在单核上交替执行;
- 并行:同一物理时刻,A 和 B 真的在不同执行单元上运行。
单核也有并发,多核才提供硬件并行的可能。并发程序具有间断性、共享资源导致的非封闭性,以及时序不同导致的不可再现性。
一个丢失更新的例子
共享变量 x=5,两个执行流都做 x=x+1。这条高级语句至少要读、加、写:
A: read x -> 5
B: read x -> 5
A: write 6
B: write 6
结果是 6,而不是期望的 7。问题不是 CPU “算错了”,而是结果依赖并发执行的相对时序,这就是竞争条件。
Bernstein 条件:两段代码能否独立并发
对语句 ,记读变量集为 ,写变量集为 。 可无冲突并发的必要检查是:
前两个排除“一个读时另一个写”,第三个排除写写冲突。例如:
S1: c = a + b R1={a,b}, W1={c}
S2: d = a - b R2={a,b}, W2={d}
三个交集都为空,两者可并发。如果 S2: a=d-b,则 含 ,与 冲突。
进程不只是代码
一个进程至少包含:
- 代码、全局数据、堆和用户栈;
- 程序计数器、通用寄存器、栈指针等 CPU 现场;
- 独立虚拟地址空间的页表根;
- 打开文件、信号处理、权限、计时和调度信息;
- 内核为它维护的进程控制块 PCB。
进程的动态性表现在从创建到终止有寿命;同一程序可被运行多次成为多个进程;同一进程也可通过 exec 替换正在执行的程序映像。
基本状态机
被调度
就绪 ───────→ 运行
↑ │ │
│ I/O/事件完成 │ └─时间片到/被抢占─→ 就绪
│ │
阻塞 ←─等待 I/O/信号量/事件─┘
- 就绪:除 CPU 外的运行条件已具备;
- 运行:正在某个 CPU 上执行;
- 阻塞/等待:即使现在给 CPU 也无法继续,必须等事件。
最常见的错误是把“等 CPU”称为阻塞。那是就绪;阻塞的进程不应放在就绪队列里空转。
更完整模型还可加新建、终止、就绪挂起、阻塞挂起。“挂起”强调进程已被中级调度移出主存,与是否正在等事件是两个维度。

PCB 是 OS 眼中的进程
PCB 常记录:
| 类别 | 示例 |
|---|---|
| 身份 | PID、父进程、用户/组 |
| CPU 现场 | PC、SP、寄存器、处理器状态 |
| 调度 | 状态、优先级、时间片、队列链接 |
| 内存 | 页表根、地址空间描述 |
| 资源 | 打开文件、I/O 状态、信号 |
| 计费/统计 | CPU 时间、创建时间等 |
内核常按状态将 PCB 组成就绪队列和各事件等待队列。所谓“把进程从阻塞变为就绪”,在实现上就是修改 PCB 状态,把它从一条队列移到另一条。
进程控制原语改变什么
提交批作业、用户启动程序、正在运行的进程创建子进程或系统服务需要工作者,都可能触发创建;正常退出、致命异常、被父进程/管理员终止或系统关闭则会撤销进程。创建/撤销、阻塞/唤醒等控制路径要原子地维护:
- 分配或释放 PID、PCB、地址空间和资源引用;
- 保存/建立 CPU 初始现场;
- 修改父子关系、状态和统计;
- 把 PCB 加入或移出正确队列。
课件所说的“原语”强调这段状态修改不能被观察成半完成。例如唤醒不能只把状态字改成就绪却忘了入队,否则进程逻辑上可运行、调度器却永远找不到它。
fork 与 exec 不是同一件事
fork() 复制当前执行环境,一次调用在父子两个进程中各返回一次:
- 父进程得到子 PID;
- 子进程得到 0;
- 失败时父调用返回负值。
现代实现使用写时复制,不会立即拷贝全部物理内存。exec 则保留进程身份等内核上下文,用新可执行文件替换当前地址空间和用户寄存现场。
常见 Shell 流程是:先 fork 创建子进程,子进程再 exec 目标程序;Shell 自己就不会被覆盖。
模式切换与进程上下文切换
模式切换:同一进程因系统调用/异常从用户态进入内核态,服务完后可直接返回原进程。
进程切换:调度器决定让 B 代替 A 占用 CPU,需要:
- 保存 A 的 PC、SP 和寄存器到 A 的 PCB/内核栈;
- 更新 A 状态并加入就绪或阻塞队列;
- 选出 B,更新 B 状态;
- 切换地址空间、内核栈等与 B 相关的执行环境;
- 恢复 B 现场,从 B 上次停下的位置继续。
切换本身不完成用户工作,还会扰动 Cache/TLB,是并发性的成本。
为什么在进程里再引入线程
进程原本同时承担两种角色:
- 拥有地址空间、文件等资源;
- 作为 CPU 上的执行流。
线程把第二个角色拆出来。同一进程的线程共享代码、全局数据、堆、地址空间和打开文件;每个线程自有 PC、寄存器、调度状态和栈。
因此:
- 创建和切换线程通常比进程轻;
- 共享内存通信很直接;
- 一个线程等 I/O 时,同进程其他线程可继续;
- 但共享也意味着更容易出现数据竞争,一个线程的非法写可破坏整个进程。
线程的三种实现路线
用户级线程
线程库在用户空间管理线程表和切换,内核只看到一个进程。
- 切换无需陷入内核,快且可自定调度;
- 若一个线程执行会阻塞整个进程的系统调用,内核无法改调同进程其他用户线程;
- 若只映射到一个内核执行实体,也不能真正在多核并行。
内核级线程
内核看见并调度每个线程。某线程阻塞时可运行其他线程,也可分布到多核;但创建、管理和切换需内核参与,开销较大。
混合模型
将 个用户线程多路复用到 个内核线程,试图兼顾快速用户级管理与内核并行/阻塞感知,但运行时和内核之间需要更复杂的协调。
同一进程的线程共享全局变量、堆和文件,但每个线程需要自己的栈、寄存器现场、调度状态以及线程局部存储(TLS)。正因为共享多,线程创建/通信较轻,也更容易因竞态破坏彼此。任务彼此不可信、需要强隔离,或共享状态收益很小而同步成本很高时,多进程可能比“全部改多线程”更稳妥。
| 映射 | 含义 | 主要特点 |
|---|---|---|
| 多对一 | 多用户线程 → 1 内核线程 | 快,但阻塞与多核受限 |
| 一对一 | 每用户线程 → 1 内核线程 | 并行好,内核开销大 |
| 多对多 | 用户线程 → 内核线程 | 灵活,实现复杂 |
Pthreads 是 API 标准,不强制它必须由用户级还是内核级方式实现。Linux 也不必在核心调度对象中强行分出“进程类”和“线程类”,clone 可通过共享地址空间、文件等标志组合出不同程度的资源共享。
最后用三个“拥有者”检查
- 程序:拥有静态代码和数据布局;
- 进程:拥有地址空间、文件和保护边界;
- 线程:拥有一条可被调度的 CPU 执行现场与栈。
回答资源是否共享、切换要保存什么、一个阻塞会不会拖住其他执行流时,先找对这三层归属。