Skip to content

进程生命周期与 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。代码里据此分流父子逻辑:
    c
    pid_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,子进程退出后变僵尸。

c
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 收养并立即回收。
  • 孤儿进程(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/pidstatcs 列监控)。
  • 优化:线程池复用线程避免频繁创建销毁;协程(用户态调度)避免陷入内核;CPU 亲和性(绑核)减少 cache 失效。

下一步

掌握了进程状态机与 fork/exec/wait 后,下一步进入线程:轻量级进程与多线程模型——理解为什么有了进程还要有线程,以及用户级/内核级线程与一对一/多对一/多对多模型的取舍。