进程生命周期与 PCB
基于通用操作系统概念 · 核于 2026-08
速查
- 进程生命周期:新建(New,分配 PCB 与资源)→ 就绪(Ready,等 CPU)→ 运行(Running,占 CPU)↔ 阻塞(Blocked,等事件)→ 终止(Terminated,释放资源,PCB 待回收)。五态在三态基础上加新建与终止。
- 三态核心转换:就绪→运行(调度器选中)、运行→就绪(时间片用完/抢占)、运行→阻塞(主动等待 IO/锁)、阻塞→就绪(事件发生被唤醒)。阻塞不能直接回运行,就绪不会直接变阻塞。
- PCB 内容四类:①进程标识(PID/PPID/UID);②处理机现场(PC/SP/PSW/通用寄存器);③调度信息(状态/优先级/时间片/等待事件);④控制信息(页表基址/打开文件表/信号掩码/父子链接/记账)。PCB 是进程存在的唯一标志。
- PCB 组织方式:链接方式(就绪队列/各种阻塞队列,节点是 PCB 指针)或索引方式(PCB 表+状态索引)。调度器就是从就绪队列挑 PCB。
- fork():创建子进程,复制父进程的地址空间(现代用 COW 写时复制,只复制页表不复制物理页)、打开文件表、信号设置;子进程 PID/PPID 不同,fork 对父进程返回子进程 PID、对子进程返回 0——据此区分父子执行路径。
- exec():用新程序替换当前进程的代码段/数据段/堆栈,PID 不变,地址空间被新程序覆盖。常与 fork 配合:
fork出子进程后立即exec装载新程序(如 shell 启动命令)。 - wait()/waitpid():父进程阻塞等待子进程结束,回收子进程的 PCB(避免僵尸),获取子进程退出状态。父进程不 wait 会导致子进程变僵尸。
- exit():进程主动结束,释放大部分资源(内存/文件),但保留 PCB(含退出状态)等父进程 wait 回收——这就是僵尸态。
- 僵尸进程(Zombie,状态 Z):子进程已 exit 但父进程未 wait,PCB 残留占用 PID。危害:PID 表有限(默认约 32768),大量僵尸会耗尽 PID,无法创建新进程。解决:父进程及时 wait,或 kill 父进程让 init 收养回收。
- 孤儿进程(Orphan):父进程先于子进程结束,子进程被 init(PID 1)收养,由 init 在子进程退出时 wait 回收。孤儿无害(init 负责善后),与僵尸不同。
- 上下文切换全过程:①保存当前进程寄存器到 PCB;②更新 PCB 状态、移入相应队列;③从就绪队列选下一进程 PCB;④恢复其寄存器;⑤切换页表(更新 CR3,进程切换才有);⑥TLB 可能 flush;⑦CPU 流水线冲刷,跳到新 PC。
- 进程切换代价量级:约 数微秒(TLB flush 是大头),比线程切换(不换页表)贵。频繁切换是高并发性能杀手——线程池、协程都为减少切换。
- 进阶顺序:本叶 → 线程与多线程模型 → 参考。
一、进程状态机:三态与五态
进程从创建到销毁,在几个状态间转换。三态模型是核心(就绪/运行/阻塞),五态加上新建与终止:
新建 New ──分配PCB/内存──▶ 就绪 Ready
│ ▲
调度器选中 ───┘ │ 时间片用完/抢占
▼ │
运行 Running ──执行完──▶ 终止 Terminated
│ │ (PCB待回收)
主动等待(read/锁) │ │
▼ │
阻塞 Blocked
(等IO/事件)
│
事件发生(IO完成)│
▼
就绪 Ready- 新建(New):进程刚被创建,OS 正在为其分配 PCB、内存、初始化资源,尚未就绪。
- 就绪(Ready):万事俱备只欠 CPU,在就绪队列里排队等调度器挑选。可能有多个就绪进程(多核可同时运行多个)。
- 运行(Running):占用 CPU 执行指令。单核上同时只有一个进程处于运行态;多核上可有多个。
- 阻塞(Blocked/Waiting/Sleep):进程在等待某事件(IO 完成、信号、锁释放、
sleep到期),即使给它 CPU 也无法执行。被组织在按等待事件分类的阻塞队列里。 - 终止(Terminated/Zombie):进程执行完毕或异常退出,已释放大部分资源,但 PCB 仍保留(含退出码)等父进程 wait 回收。
关键转换方向(面试高频易错点):
- 就绪 → 运行:调度器选中(dispatch)。
- 运行 → 就绪:时间片用完、或被更高优先级进程抢占。
- 运行 → 阻塞:进程主动请求等待(调用
read/wait/申请锁)——是进程自己"想去阻塞",不是被调度器塞进去。 - 阻塞 → 就绪:等待的事件发生(IO 完成、锁释放),内核把进程唤醒到就绪队列。
- 不存在的转换:阻塞 → 运行(必须先经就绪)、就绪 → 阻塞(就绪态没在跑,不可能突然去等)。
二、PCB:进程控制块详解
**PCB(Process Control Block)**是 OS 描述与控制进程的核心数据结构,是进程存在的唯一标志。Linux 内核中叫 task_struct。PCB 内容分四类:
| 类别 | 字段 | 作用 |
|---|---|---|
| 进程标识 | PID、PPID、UID、GID、进程组 | 唯一标识进程、归属关系(用于权限/信号) |
| 处理机现场 | PC、SP、PSW/EFLAGS、通用寄存器 | 上下文切换时保存/恢复——进程的"断点" |
| 调度信息 | 状态、优先级、调度策略、时间片剩余、等待事件 | 调度器据此挑选、挂入对应队列 |
| 控制信息 | 页表基址(CR3)、打开文件表、信号掩码、信号量、消息队列、父子兄弟链接 | 资源清单与进程树组织 |
| 记账信息 | CPU 时间、内存占用、IO 统计 | 监控、计费、性能分析 |
- PCB 的组织:OS 用链接方式(就绪队列、各类阻塞队列,节点间用指针串)或索引方式(一张 PCB 表 + 状态索引)组织 PCB。调度器就是"从就绪队列头摘一个 PCB"。
- 上下文切换 = 读写 PCB:保存现场就是"把寄存器写进当前 PCB",恢复现场就是"把下一进程 PCB 的寄存器装回 CPU"。
三、fork / exec / wait:进程创建与程序加载
Unix/Linux 用 fork+exec+wait 三件套管理进程创建与程序运行:
fork()——复制进程:创建一个当前进程的副本(子进程)。子进程获得父进程地址空间的副本(现代用写时复制 COW:fork 时只复制页表,物理页父子共享,谁写谁才真正复制)、打开文件表副本、信号设置副本。关键差异:
- 子进程有独立的 PID、PPID(=父进程 PID)。
- fork 返回值:对父进程返回子进程 PID(>0),对子进程返回 0,失败返回 -1。代码里据此分流父子逻辑:
cpid_t pid = fork(); if (pid == 0) { // 子进程执行路径 } else if (pid > 0) { // 父进程执行路径 }exec()——替换程序:用磁盘上的新程序覆盖当前进程的代码段、数据段、堆栈,PID 不变,地址空间内容全换。执行 exec 后,原进程的代码"消失",从新程序的 main 开始跑。家族:
execl/execv/execve/execvp等,差别在参数传递方式与是否搜索 PATH。fork + exec 经典模式:shell 执行命令
ls——shell 先fork出子进程,子进程立刻execve("/bin/ls", ...)加载 ls 程序;这样 shell 自身不受 ls 影响,且能继续接受下一条命令。wait()/waitpid()——回收子进程:父进程阻塞(或非阻塞轮询)等待子进程结束,回收子进程 PCB,获取其退出码。若父进程不 wait,子进程退出后变僵尸。
pid_t pid = fork();
if (pid == 0) {
execve("/bin/ls", argv, envp); // 子进程:加载 ls,不再返回
perror("exec"); exit(1);
}
// 父进程:等待子进程结束并回收
int status;
waitpid(pid, &status, 0);四、僵尸进程与孤儿进程
子进程退出后的善后,是进程管理的高频考点:
- 僵尸进程(Zombie,状态 Z):子进程已
exit()退出,但父进程尚未wait()回收其 PCB。此时子进程的内存/文件已释放,只剩 PCB(含退出码、资源使用统计)留在内核,状态标为 Z。- 危害:PID 数量有限(默认约 32768,可调到百万),大量僵尸会耗尽 PID,导致无法
fork新进程。每个僵尸占一个 PID 与少量内核内存。 - 解决:①父进程及时
wait/waitpid,或用SIGCHLD信号处理自动回收;②若父进程失职,kill父进程,僵尸被 init 收养并立即回收。
- 危害:PID 数量有限(默认约 32768,可调到百万),大量僵尸会耗尽 PID,导致无法
- 孤儿进程(Orphan):父进程先于子进程退出,子进程失去父亲。内核把孤儿重新挂到 init(PID 1) 名下(收养)。孤儿继续运行,退出时由 init wait 回收——孤儿无害。
- 区分:僵尸是"子死父不收"(有害,占资源);孤儿是"父死子还在"(无害,init 兜底)。两者都因父子不同步,但后果相反。
五、上下文切换全过程与代价
上下文切换是 CPU 从执行一个进程切到另一个的完整过程,理解它才能理解并发性能:
进程 A 正在运行(CPU 寄存器= A 的现场)
│ 时钟中断/阻塞/抢占
▼
① 保存 A 的寄存器(PC/SP/PSW/通用)到 A 的 PCB
② 更新 A 的 PCB 状态(就绪/阻塞),移入对应队列
③ 调度器从就绪队列选下一个进程 B
④ 从 B 的 PCB 恢复寄存器到 CPU
⑤ 若 A、B 不同进程:更新 CR3(页表基址)→ flush TLB(除非 PCID)
⑥ CPU 流水线冲刷,跳到 B 的 PC 继续执行- 代价明细:寄存器保存恢复(数十 ns)、特权级切换与流水线冲刷(数十 ns)、TLB flush(最大头,几百 ns~μs,进程切换才有)、CPU cache 冷启动(新进程的数据不在 cache,要重新加载)。
- 进程切换 vs 线程切换:同进程的线程共享地址空间与页表,切换时不换 CR3、不 flush TLB,比进程切换便宜得多。这是高并发服务用线程/协程而非多进程的关键原因。
- 量级:进程切换约 数微秒;线程切换约 百纳秒~微秒。看似小,但每秒万次切换会吃掉显著 CPU(用
vmstat/pidstat看cs列监控)。 - 优化:线程池复用线程避免频繁创建销毁;协程(用户态调度)避免陷入内核;CPU 亲和性(绑核)减少 cache 失效。
下一步
掌握了进程状态机与 fork/exec/wait 后,下一步进入线程:轻量级进程与多线程模型——理解为什么有了进程还要有线程,以及用户级/内核级线程与一对一/多对一/多对多模型的取舍。