想象你走进一家热闹的餐厅,厨师面前贴着一排小票:有的菜刚下单(原材料还没准备),有的菜已备好料正等着下锅,锅里正炒着的那道菜占用了厨师的全部注意力,还有一道菜在等蒸锅空出来。每一张小票都记录着“哪道菜、料备到哪一步、火候到几成”。这就是进程世界的缩影——操作系统就是这家餐厅的总调度,它管理的不是菜谱(程序),而是一张张“正在做菜的进度单”(进程)。 CPU 只有一个(或者少数几个),但餐厅里同时有几十桌客人在点菜。厨师不能真的同时炒十道菜,只能炒一下这道、再炒一下那道,靠“记住每道菜炒到哪一步”来来回回切换。操作系统也是如此:它把每个正在运行的程序包装成“进程”,用一块叫做 PCB 的“小票”记录进程的进度,并让进程在新建、就绪、运行、阻塞、终止五个状态之间流转。这篇文章,我们就用“餐厅后厨”这一幅画面,把进程是什么、它有哪些状态、状态之间怎么转换,一次讲透。
理解进程与状态转换,是理解操作系统如何“同时”运行无数程序的基石。对 AI 从业者而言,无论是分析多进程推理服务、排查 GPU 任务排队等待,还是读懂训练框架的分布式调度日志,你看到的每一个"running / waiting / sleep"背后,都是这套状态机在运转。
可视化演示
展示操作系统进程的核心概念:五状态转换图(就绪/运行/等待/新建/终止)、PCB进程控制块结构、以及fork()进程创建与写时复制(CoW)机制。
本节配有交互动效(进入视口后自动加载,约 3KB)。
'+captions.state+'
进程基本概念
客人点了一份宫保鸡丁,后厨的菜谱架上放着《宫保鸡丁做法》。菜谱本身不会动、不会饿、不会被烫到;真正在灶台上切鸡丁、调芡汁、翻锅的那套“动作”才是活的东西。操作系统把“正在发生的一整套做菜动作”取了个名字——进程(Process);而那份永远不会变的菜谱,就是程序(Program)。
用一句话定义:进程是程序在一个数据集合上的一次执行过程。拆开看有三个要素:一是“程序”,即那本菜谱;二是“数据集合”,即此刻备好的鸡丁、花生米、调料;三是“执行”,即 CPU 正在推进的那一行代码。你可以把它想象成“程序 + 数据 + CPU 现场”三样东西的合体。
进程与程序的区别可以打一张对比表:程序是静态的,存在磁盘里,像一本合上的菜谱;进程是动态的,有出生、有运行、有死亡,像灶台上正被翻炒的一道菜。同一个程序可以同时对应多个进程——十桌客人都点宫保鸡丁,就同时有十份“做宫保鸡丁”的进程,每份的进度各不相同。
回到厨房:菜谱放在架子上十年也不会自己变成菜,只有厨师拿起它、备好料、开火翻炒,才构成一个“进程”。所以操作系统的任务从来不是管理菜谱,而是管理每一个正在灶台上的做菜流程——为它们登记、排队、换锅、熄火。理解了这一点,后面的状态转换就顺理成章了。
- 程序项进程必须依托一段可执行代码而存在,没有程序就没有进程。
- 数据集合项进程处理的数据是独立的,不同进程有各自的输入、输出与中间变量。
- 执行现场项进程拥有自己的程序计数器与寄存器状态,记录“菜做到哪一步”。
进程控制块
厨师同时炒三口锅时,为什么不会搞混?因为他手边贴着三张小票,上面写着“宫保鸡丁:鸡丁已切、花生已炸、还剩最后收汁”。这张小票在后厨里至关重要——一旦厨师被叫去处理另一道菜,回来扫一眼小票就能无缝续炒。操作系统里,这张小票叫进程控制块(Process Control Block,PCB)。
PCB 是操作系统为每个进程建立的一张“订单卡片”,它是进程存在的唯一标志。一个进程从被创建到终止,PCB 始终跟随;进程被换下 CPU 时,它的进度就“存”在 PCB 里;下次被调度时,操作系统把 PCB 里的进度“恢复”出来。所以说,PCB 是进程的身份证,也是进程的存档点。
PCB 里记了什么?首先是进程标识符 PID,相当于小票上的菜名和桌号,系统靠它区分每一个进程;其次是进程状态,标明它此刻是就绪、运行还是阻塞;再是程序计数器,记录下一条要执行的指令地址,也就是“菜做到哪一步”;还有通用寄存器、内存分配信息、打开的文件列表等资源清单,相当于“这道菜占用了哪口锅、哪把勺”。
一张张 PCB 汇总起来,就形成了操作系统的“订单墙”。调度器不看菜谱、不闻菜香,只看小票上的状态和进度,就能决定谁上灶、谁回队。这也是为什么 PCB 被称为“进程实体的一部分”——没有 PCB 的进程,操作系统根本无从管理。
- PID 项进程的唯一编号,类似订单号,创建时分配、终止时回收。
- 状态项记录进程当前处于哪个生命周期阶段,是调度器决策的依据。
- PC 项程序计数器保存下一条指令的地址,是“进度条”的核心。
- 资源项登记进程占用的内存、文件、外设,便于回收与隔离。
进程生命周期
一道菜从客人点单到上桌,经历:下单(创建)→ 备好料等灶口(就绪)→ 上灶翻炒(运行)→ 等蒸锅/等调料(阻塞)→ 出菜(终止)。操作系统的进程也遵循同样的“五态一生”:新建态、就绪态、运行态、阻塞态、终止态。
- 新建态(New):小票刚打出来,进程正在被创建,还没进厨房。
- 就绪态(Ready):所有材料备齐,只等一口空灶;进程已具备运行条件,只差 CPU。
- 运行态(Running):正占着 CPU 执行指令,是全厨房唯一真正“动手”的状态。
- 阻塞态(Blocked):运行中需要等待某事件(如 I/O 完成),于是暂时让出 CPU 去等待。
- 终止态(Terminated):菜已出,小票作废,进程结束并等待系统回收资源。
关键认知:只有一个 CPU 时,同一时刻最多只有一个进程处于运行态;但可以有很多进程同时处于就绪态和阻塞态。就绪是“万事俱备,只欠 CPU”,阻塞是“CPU 给了我我也干不了,因为我在等别的东西”。
后厨的节奏由此变得井然有序:点单的小票先“新建”,备好料进“就绪队列”,轮到灶口才“运行”,等蒸锅时“阻塞”,出菜后“终止”。每一张小票的一生,都是这五个状态的一次完整旅行。理解五态,等于拿到了阅读一切操作系统状态监控数据的钥匙。
- 创建分配 PID、建立 PCB、装入程序代码,进程从无到有。
- 就绪创建完成后进入就绪队列,等待调度器分配 CPU。
- 运行调度器从就绪队列选中一个进程,赋予它 CPU 的使用权。
- 阻塞运行中遇到 I/O 请求或等待事件,进程主动让出 CPU。
- 终止进程执行完毕或出现致命错误,系统回收资源并删除 PCB。
状态转换机制
厨房里最忙的人不是炒菜的厨师,而是排菜的“调度员”:他面前有两条队——“已备好料的就绪队”和“在等食材/等锅的阻塞队”。一个进程为什么从运行变就绪?因为时间片用完被换下;为什么从运行变阻塞?因为它在等 I/O;为什么从阻塞变就绪?因为它等的事件完成了。这些“为什么”,就是状态转换的驱动事件。
核心转换关系可以总结为四条弧线:就绪→运行是调度器分配 CPU;运行→就绪是时间片到期被抢占;运行→阻塞是进程主动请求 I/O;阻塞→就绪是 I/O 完成被唤醒。特别提醒:阻塞态不能直接进运行态,必须先回就绪队重新排队——就像等蒸锅的菜,蒸锅空出来只是回到“可上灶”的队列,不能直接抢正在炒菜的灶口。
进程中“换人”的这个动作,专业术语叫上下文切换(Context Switch):把当前进程的 PC、寄存器等现场保存进它的 PCB,再把下一个进程 PCB 里的现场恢复到寄存器。这正是“厨师放下 A 菜、抄起 B 菜的小票、立刻续炒”的底层实现。
理解状态转换的关键,是把每条弧线都和一个“事件”绑定:调度的钟声、时间片的哨声、I/O 完成的铃响。下次你在 top 命令里看到进程处于 S(sleeping)状态,不妨想想它正在哪条弧线上——是刚发出 I/O 请求,还是在等待某个事件完成。
- 调度事件就绪队列队首的进程被调度器选中,从就绪变为运行。
- 时间片到进程用完了分配给它的 CPU 时间,被强制换下。
- 等待事件进程请求数据或等待外设,进入阻塞队列。
- 唤醒事件等待的 I/O 完成,进程状态变为就绪。
- 切换执行上下文切换完成两个进程现场的保存与恢复。
进程主要特征
进程有四个招牌特征,可以借后厨一一对应:动态性——进程是“正在做菜”,有创建有消亡,而菜谱永远静止;并发性——多个进程在一段时间内交替推进,像厨师在三口锅之间来回翻炒;独立性——每个进程有自己的地址空间和资源,A 菜炒糊了不影响 B 菜;异步性——各进程以不可预知的速度推进,谁先谁后全看调度,像客人无法預知自己这桌上菜的确切时刻。
这四个特征不是理论空谈,它们直接决定了操作系统怎么设计。因为并发,所以需要 PCB 保存进度;因为独立,所以进程间不能随便访问对方内存,需要隔离机制;因为异步,所以需要队列和状态机来管理不确定性。
在资源管理上,进程还有一个重要身份:进程是系统进行资源分配和调度的基本单位。内存、文件、设备,都以进程为单位来分配和回收。就像餐厅里,灶台、蒸锅、调料架的配额是下发给“每一张小票”的。
记住这四个特征,就能回答面试官最爱问的一道题:“程序和进程有什么区别?”——程序是静态的、永久的,进程是动态的、暂时的;程序可被多个进程共享,进程不能脱离程序而存在。后厨里的菜谱可以传三代,但任何一次“做菜”都只是一顿饭的时间。
- 动态性进程有完整的生命周期,由创建而产生、由调度而运行、由终止而消亡。
- 并发性多个进程轮流使用 CPU,宏观上同时推进,微观上交替执行。
- 独立性各进程拥有独立的内存空间与资源,互不干扰。
- 异步性进程推进速度不可预知,但结果应当确定。
进程与线程
一道复杂的菜(比如佛跳墙)往往由一个厨师和两个帮厨共同完成:厨师掌勺,帮厨切菜、看火。他们共享同一本菜谱、同一个操作台,但各干各的活。操作系统把这个思路实体化了:线程(Thread)就是进程内的一条执行流,一个进程可以包含多个线程,它们共享进程的代码、数据和资源,却拥有各自独立的程序计数器和栈。
为什么需要线程?因为进程太重了——创建进程要分配独立的资源、切换时要保存整个现场,就像为一道菜单独开一个厨房。而线程仿佛“同一厨房里的多个厨师”,创建快、切换轻,还能天然共享资源(同一份食材,不需要复制三份)。因此现代操作系统把进程定位为“资源分配的基本单位”,把线程定位为“CPU 调度的基本单位”。
进程与线程的对比:进程之间相互隔离,一个进程崩溃一般不影响其他进程;而同一进程内的线程共享地址空间,一个线程越界可能拖垮整个进程。资源方面,进程拥有独立的内存、文件、设备;线程只拥有自己的栈和寄存器,其余全部共享。
后厨的比喻在这里达到圆满:进程是“一家餐厅”的完整配置(店面、食材、执照),线程是店里忙碌的厨师。开一家新餐厅代价高昂,但在同一家店里多雇一个帮厨却便宜得多。这也解释了为什么如今高性能服务器动辄“多线程”——在高并发的 AI 推理服务里,一个进程内的多个线程共享模型权重,比开多个进程划算得多。
- 线程定义线程是进程内的一个执行单元,是系统独立调度和分派 CPU 的基本单位。
- 共享资源同一进程的线程共享代码段、数据段、堆和打开的文件。
- 独立现场线程拥有自己的 PC、寄存器和栈,保证各自执行路径独立。
- 关系总结进程是资源分配单位,线程是 CPU 调度单位。
AI 应用场景
场景一:大模型推理服务的并发调度
阶段:模型部署与推理服务
解决的问题:当多个用户同时请求大模型推理时,如果按传统方式每个请求开一个进程,内存开销巨大(每个进程都要加载一份模型权重);用进程+线程的思维,一个进程内创建多个线程,共享常驻内存中的模型参数,让 GPU 排队调度更高效。
具体案例:某 AI 公司用 vLLM 部署 7B 模型,推理进程常驻显存,每个请求由一个工作线程处理。观察 nvidia-smi 时你会看到进程状态在 R(运行)与 S(休眠等待 I/O)之间跳动——这正是“运行态→阻塞态→就绪态”的实时写照。当一个请求等待 GPU 计算结果返回时,该线程阻塞,CPU 立即调度其他线程处理新请求。
场景二:训练任务的状态监控与故障诊断
阶段:模型训练与资源管理
解决的问题:大模型训练时,数据加载(I/O)常常成为瓶颈,导致 GPU 空转。用进程状态模型来诊断:如果训练进程长期处于阻塞态,说明它在等数据;如果频繁在就绪和运行间切换,说明 CPU 在抢时间片。理解了状态转换,就能精准定位是 I/O 慢还是 CPU 不足。
具体案例:训练工程师用 top 查看进程状态列,发现 D(不可中断睡眠,即阻塞在磁盘 I/O)占比超高,于是改用 DataLoader 多进程预取数据,把“等待数据”的阻塞态时间转化为“计算+加载并行”,训练吞吐提升 38%。
场景三:微服务架构中的进程隔离设计
阶段:AI 应用架构设计
解决的问题:AI 平台常把不同模型服务拆成独立进程部署,利用“进程独立性强”的特性做故障隔离——一个模型服务崩溃不会拖垮整个平台;同时利用“线程共享资源”的特性,在单进程内处理海量并发推理请求。
具体案例:某推荐系统平台把 CTR 模型、向量检索、重排模型分别部署为独立进程,进程间通过消息队列通信;每个进程内部用线程池处理请求。一次线上事故中,重排进程因内存泄漏被杀掉重启,其余进程毫发无损——进程的独立性成了系统稳定的最后一道防线。
Python 演示
"""进程五态转换模拟器:演示就绪、运行、阻塞、终止之间的转换。"""import timefrom collections import deque class PCB: """进程控制块:相当于后厨的点单小票。""" def __init__(self, pid, name): self.pid = pid self.name = name self.state = "NEW" self.pc = 0 # 程序计数器:菜做到哪一步 self.total_ticks = 4 # 总共需要 4 个时间片 class Scheduler: """简化调度器:管理就绪队列与阻塞队列。""" def __init__(self): self.ready_queue = deque() self.blocked_queue = deque() self.running = None def create(self, pid, name): p = PCB(pid, name) p.state = "READY" self.ready_queue.append(p) print(f"[创建] {name}(pid={pid}) 进入就绪队列") return p def tick(self): if self.running is None and self.ready_queue: self.running = self.ready_queue.popleft() self.running.state = "RUNNING" print(f"[调度] {self.running.name} 上 CPU 运行") if self.running is None: print("[空闲] 没有可运行的进程") return self.running.pc += 1 if self.running.pc >= self.running.total_ticks: print(f"[完成] {self.running.name} 执行完毕 (终止)") self.running.state = "TERMINATED" self.running = None return if self.running.pc == 2: print(f"[阻塞] {self.running.name} 请求 I/O, 进入阻塞队列") self.running.state = "BLOCKED" self.blocked_queue.append(self.running) self.running = None return print(f"[时间片] {self.running.name} 用完时间片, 回到就绪队列") self.running.state = "READY" self.ready_queue.append(self.running) self.running = None def wakeup(self): if self.blocked_queue: p = self.blocked_queue.popleft() p.state = "READY" self.ready_queue.append(p) print(f"[唤醒] {p.name} I/O 完成, 回到就绪队列") def main(): sched = Scheduler() sched.create(1, "train_worker") sched.create(2, "data_loader") for i in range(12): print(f"\n--- 第 {i + 1} 个时间片 ---") sched.tick() if i == 3: sched.wakeup() time.sleep(0.3) if __name__ == "__main__": main()
class PCB:用一张小票抽象进程控制块,记录 pid、状态和程序计数器。self.ready_queue = deque()就绪队列用双端队列实现,先进先出调度。def create(self, pid, name):创建进程并放入就绪队列,对应“新建态→就绪态”。self.running = self.ready_queue.popleft()从就绪队列取一个进程上 CPU,对应“调度”。if self.running.pc == 2:模拟进程运行一段时间后请求 I/O,进入阻塞态。sched.wakeup()模拟 I/O 完成,把阻塞进程唤醒回就绪队列。
常见误区
误区认为进程和程序是同一个东西,“我写了一个 Python 脚本,那它就是一个进程”。
正解程序是静态的指令集合,像一本菜谱;进程是程序被加载到内存、获得资源并开始执行后的动态实体,像“正在灶台上做这道菜”这件事。一个程序可以对应多个进程(同一脚本被启动三次就是三个进程),同一进程的代码段也可能被多个进程共享。判断标准很简单:有 PID、有状态、有 PCB 的才是进程。
为何常见:日常开发中我们常说“跑一个程序”,给人的错觉是程序自己会跑;其实真正跑起来的是操作系统为它创建的进程。
误区看到进程处于阻塞态,就以为它“死机”了或者不再往前推进。
正解阻塞态是进程主动或因 I/O 被迫让出 CPU,去等待某个事件(键盘输入、磁盘读取、网络包到达)。事件完成后进程会回到就绪态,等待再次被调度。它更像“菜在等蒸锅”,而不是“厨房关门”。区分阻塞与终止的关键:阻塞的进程还有未来,终止的进程已经销毁。
为何常见:top 命令里大量进程显示 S 状态,新手容易误读为系统卡死;其实这正是操作系统高效利用 CPU 的表现——等待时不占 CPU。
误区认为“运行变就绪”和“运行变阻塞”都是被动的、被系统打断的,因此没必要区分。
正解两者本质不同。时间片到期是被抢占(preempt),进程本身没做错什么,只是配额用完,回就绪队重新排队;而阻塞是进程主动要求等待 I/O 或某个事件,原因在进程自身。可以这样记:时间片到期是“到点下班”,阻塞是“我得等个快递”。状态机里这两条弧线方向完全不同,调度器对它们的处理方式也截然不同。
为何常见:两者都导致进程离开 CPU,表象相似,初学者容易混淆“被动让出”与“主动等待”。
误区以为“并发”就是“并行”,觉得只要进程多,就能同时跑在多个核上。
正解并行(parallel)确实要求多核同时执行;而并发(concurrent)是逻辑上的同时,单个核上多个进程靠时间片轮流执行。经典的五态模型讨论的正是单核视角下进程的宏观并发。多核只是多了几个“运行态”名额,状态机和转换规则完全不变。就像餐厅里有两位厨师,也只是两口锅同时在炒,每口锅依然要遵守“一锅一菜”的铁律。
为何常见:日常用语里“并发”和“并行”经常混用,加上多核宣传深入人心,容易忽略单核轮流执行这个基础事实。
面试题
Q1. 进程与程序的本质区别是什么?
- A.进程是静态的,程序是动态的
- B.程序是静态的,进程是动态的← 正确答案
- C.两者都是静态的
- D.两者都是动态的
答案:B。程序是存放在存储介质上的指令集合,像一本不会自己变动的菜谱;进程是程序在数据集合上的一次执行过程,有创建、运行、终止,像灶台上正在被翻炒的菜。因此“静态对程序、动态对进程”才是正确的对应关系。
Q2. 一个进程被创建后,首先进入哪个状态?
- A.运行态
- B.阻塞态
- C.就绪态← 正确答案
- D.终止态
答案:C。进程创建完成后会先进入就绪态,等待调度器分配 CPU。它不能直接从新建跳到运行,就像备好料的菜必须先进“等灶口”的队列,而不能直接抢正在炒菜的锅。只有被调度器选中后才会从就绪态转为运行态。
Q3. 下列哪个状态转换是“时间片用完”触发的?
- A.运行态 → 就绪态← 正确答案
- B.就绪态 → 运行态
- C.运行态 → 阻塞态
- D.阻塞态 → 就绪态
答案:A。时间片用完时进程被剥夺 CPU,从运行态回到就绪态排队,相当于“到点下班回队重排”。B 是调度器主动分配 CPU,C 是进程主动请求 I/O,D 是等待的事件完成。只有 A 对应被动的时间片到期抢占。
Q4. 一个运行中的进程发出 I/O 请求,它的状态变化是?
- A.运行态 → 就绪态
- B.运行态 → 阻塞态← 正确答案
- C.阻塞态 → 就绪态
- D.就绪态 → 运行态
答案:B。进程运行中请求 I/O 时,它无法继续执行,必须等待 I/O 完成,因此进入阻塞态。它不会先回就绪态,因为就绪态意味着“万事俱备只欠 CPU”,而此刻它缺的是数据而非 CPU。I/O 完成后它才从阻塞态回到就绪态。
Q5. PCB(进程控制块)中不需要记录下列哪项信息?
- A.进程标识符 PID
- B.程序计数器 PC
- C.进程当前状态
- D.另一份菜谱的完整内容← 正确答案
答案:D。PCB 相当于后厨的点单小票,记录 PID(哪道菜)、PC(做到哪一步)、状态(在等什么)、资源占用(占着哪口锅)。它不需要保存其他程序的代码——那是被共享的程序本体,不归单一进程独有,类似小票上不需要抄写完整菜谱。
Q6. 下列关于就绪态与阻塞态的对比,正确的有?
- A.就绪态进程只缺 CPU← 正确答案
- B.阻塞态进程可能缺 CPU 也可能缺数据
- C.就绪态进程可以直接运行
- D.阻塞态进程必须回到就绪态才能运行← 正确答案
答案:AD。就绪态的一切资源都已齐备,只差被调度器选中获得 CPU,所以 A 正确。阻塞态进程是在等某个事件(数据、I/O),它即使有 CPU 也无法推进,所以阻塞态必须等事件完成后回到就绪态、再经调度才能运行,D 正确。阻塞态并不“缺 CPU”,它缺的是等待的事件,B 错误;就绪态也不能“直接运行”,必须经过调度器分配 CPU,C 错误。
Q7. 上下文切换主要完成的工作是?
- A.把当前进程的小票撕掉
- B.保存当前进程现场到 PCB,并恢复下一个进程的现场← 正确答案
- C.为新进程重新编译代码
- D.清空内存中的所有数据
答案:B。上下文切换是“厨师换菜”的底层实现:先把当前进程的 PC、寄存器等现场保存进它的 PCB,再把下一个进程 PCB 里的现场恢复到寄存器,这样新进程能无缝续跑。它不销毁 PCB、不编译代码、不清空内存,只是交换“存档”。
Q8. 关于进程的“并发性”,下列说法正确的是?
- A.多个进程必须同时在不同 CPU 上运行
- B.多个进程在一段时间内交替执行,宏观上同时推进← 正确答案
- C.多个进程互相等待,谁也不运行
- D.只有一个进程能存在于系统中
答案:B。并发是逻辑上的同时:在单核 CPU 上,多个进程靠时间片快速交替,宏观上好像同时运行,微观上任意时刻只有一个进程在运行。并行(A)才要求多核同时执行。C 是死锁场景,D 与多道程序设计矛盾。
Q9. 下列哪些转换在经典五态模型中是合法的?
- A.就绪态 → 运行态← 正确答案
- B.阻塞态 → 运行态
- C.运行态 → 阻塞态← 正确答案
- D.阻塞态 → 就绪态← 正确答案
答案:ACD。经典五态模型中合法的转换包括:就绪→运行(调度)、运行→就绪(时间片到)、运行→阻塞(等待 I/O)、阻塞→就绪(I/O 完成)、运行→终止。阻塞态不能直接进运行态,因为等待事件完成后必须先回到就绪队列排队,就像等蒸锅的菜蒸好后要重新排“上灶队”,不能直接抢正在炒菜的锅。
Q10. 下面关于多线程与进程关系的描述,正确的有?
- A.进程是资源分配的基本单位← 正确答案
- B.线程是 CPU 调度的基本单位← 正确答案
- C.同一进程内的线程共享进程的代码和数据← 正确答案
- D.不同进程内的线程可以共享同一段栈空间
答案:ABC。经典结论:进程是资源分配的基本单位(A 对),线程是系统独立调度和分派 CPU 的基本单位(B 对)。同一进程的多个线程共享代码段、数据段和堆,但各自拥有独立的栈和寄存器(C 对)。不同进程的地址空间相互隔离,它们的线程栈也彼此独立,不能共享同一段栈空间(D 错)。