主题
第二章 进程与线程:CPU 的虚拟化起点
1. 从一个问题出发:一个 CPU 怎么"同时"跑一百个程序
上一章说过,操作系统最重要的魔术是虚拟化。这一章讲第一个魔术:CPU 虚拟化。
机器上可能只有 4 个 CPU 核心,但你同时开着浏览器、音乐、聊天软件和几十个后台服务。操作系统的做法不是变出更多 CPU,而是让 CPU 快速轮流:每个程序跑几毫秒就被暂停,换下一个上场。切换足够快,人的感官就以为大家在同时运行。
要实现"暂停一个程序、以后还能原样恢复",操作系统必须回答一个问题:一个正在运行的程序,到底由哪些状态组成?暂停时要把什么存起来? 对这个问题的完整回答,就是"进程"这个概念。
2. 程序 vs 进程:静态与动态的区别
程序是躺在磁盘上的一个可执行文件,是一段静态的指令和数据,像一份菜谱。
进程(process)是程序被加载进内存、跑起来之后的那个"活的执行过程",像照着菜谱做菜的整个动态过程——有进度、有中间状态、会占用炉灶和食材。
| 对比项 | 程序 | 进程 |
|---|---|---|
| 性质 | 静态的文件 | 动态的执行过程 |
| 生命周期 | 长期存在于磁盘 | 有创建、运行、消亡的过程 |
| 与资源的关系 | 不占用 CPU、内存 | 占用内存、CPU 时间、打开的文件等 |
| 数量对应 | 一个程序 | 可对应多个进程,同一程序可同时运行多份 |
考试高频表述:"进程是程序的一次执行","进程 = 程序 + 数据 + PCB"。要点是动态性:进程有生命周期,程序没有。同一个浏览器程序开三个窗口,可能就是三个进程。
进程的几个教材级特征:动态性(最基本特征)、并发性、独立性(资源分配和调度的基本单位)、异步性(走走停停、推进速度不可预知)。
3. 进程由什么组成:PCB 与地址空间
3.1 PCB:进程的"身份证 + 病历本"
操作系统要管理成百上千个进程,必须为每个进程建立一份档案,这份档案叫进程控制块(PCB, Process Control Block)。PCB 是进程存在的唯一标志——创建进程本质上就是创建 PCB,撤销进程就是回收 PCB。
PCB 里主要记四类信息:
| 类别 | 内容举例 | 用途 |
|---|---|---|
| 标识信息 | 进程号 PID、父进程号、用户 | 认出这是谁 |
| 现场信息 | 程序计数器、通用寄存器、状态字 | 暂停后能原样恢复 |
| 控制信息 | 当前状态、优先级、队列指针 | 调度时决定谁上场 |
| 资源清单 | 内存指针(页表位置)、打开的文件、占用的设备 | 记账与回收 |
一句话点透:CPU 虚拟化的秘密全在 PCB 里——把寄存器现场存进 PCB,进程就"冻结"了;把 PCB 里的现场装回寄存器,进程就"解冻"继续跑,它自己毫无察觉。
3.2 地址空间布局:进程眼中的内存
每个进程都以为自己独占一条从低地址到高地址的完整内存,典型布局如下:
记两个方向:栈向低地址方向生长,堆向高地址方向生长,两者相向而行。代码区只读且可被同一程序的多个进程共享。这张图第五章讲内存管理时还会用到。
4. 进程的状态:三态与五态模型
4.1 为什么至少需要三个状态
进程不是"要么在跑、要么没跑"这么简单。关键洞察是:没在跑的进程分两种情况——一种是"万事俱备只差 CPU",另一种是"就算给它 CPU 也没法跑,因为它在等磁盘、等输入"。这两种必须分开,否则调度器会把 CPU 白白分给一个还在等数据的进程。
于是有了三个基本状态:
- 运行态:正占着 CPU 执行。单 CPU 下同一时刻最多一个进程处于运行态。
- 就绪态:只差 CPU,其他条件全齐,排队等调度。
- 阻塞态(等待态):在等某个事件(如 I/O 完成),此时给它 CPU 也没用。
4.2 状态迁移图:每条边为什么存在
逐条边解释"为什么":
| 迁移 | 触发原因 | 主动还是被动 |
|---|---|---|
| 就绪 → 运行 | 调度程序选中它 | 被动,由操作系统决定 |
| 运行 → 就绪 | 时间片到、被抢占 | 被动,进程自己并不想让 |
| 运行 → 阻塞 | 进程自己请求等待某事件 | 主动,只能由运行中的进程自己发起 |
| 阻塞 → 就绪 | 所等事件发生 | 被动,由中断处理程序或其他进程唤醒 |
哪些迁移不可能发生,这是选择题最爱:
- 阻塞 → 运行:不可能直接发生。事件完成后必须先回就绪队列排队,能不能马上上 CPU 要看调度。
- 就绪 → 阻塞:不可能。阻塞是"运行中发现要等东西"才发生的,一个没在运行的进程无从提出等待请求。
一句话点透:变阻塞必是自己的选择,脱离阻塞必靠别人唤醒。
4.3 五态模型:加上创建与终止
三态之外,再加"创建态"(正在分配 PCB 和资源,尚未准备好)和"终止态"(已结束但 PCB 尚未回收,留着给父进程查退出状态),就是五态模型。有的教材还引入挂起(进程被换出到磁盘),形成七态,考试一般点到为止:挂起是"进程整体暂时搬出内存",与阻塞(在内存中等事件)是两个维度。
5. 进程的创建与终止
创建一个进程,操作系统要做四件事:分配唯一 PID 并建立 PCB → 分配内存等资源 → 加载程序代码和数据 → 把 PCB 挂入就绪队列。典型创建时机:系统初始化、用户命令启动、正在运行的进程调用"创建子进程"的系统调用(如 UNIX 的 fork)、批处理作业被调度装入。
终止分三类:正常结束(程序跑完主动退出)、异常结束(除零、非法内存访问等错误被内核强制终止)、外界干预(用户或父进程发出终止命令)。终止时内核回收其内存、关闭其文件,最后撤销 PCB。
父子关系值得记一句:子进程可以继承父进程的资源;父进程被撤销时,有的系统会级联撤销全部子孙进程。
6. 线程为什么被发明
进程用了很多年之后,人们发现两个痛点:
痛点一:进程切换太贵。 切换进程要保存和恢复全套现场,还要切换地址空间(换页表、冲刷 TLB 缓存),开销可观。如果一个网页浏览器想同时"下载图片 + 渲染页面 + 响应点击",为每件事开一个进程,切换成本高得离谱。
痛点二:进程之间共享数据太难。 进程的设计初衷就是互相隔离,各有各的地址空间。可上面三件事偏偏要频繁共享同一份网页数据,跨进程传数据既慢又麻烦。
反思一下会发现,进程这个概念其实捆绑了两种职责:
- 资源的容器:地址空间、打开的文件、设备——这些是"财产"。
- 执行的流:程序计数器、寄存器、栈——这些是"干活的人"。
线程(thread)就是把这两者拆开:让一个资源容器(进程)里可以住多个执行流(线程)。同一进程的多个线程共享地址空间和打开的文件,但各自有自己的程序计数器、寄存器和栈。
一句话点透:进程是资源分配的基本单位,线程是 CPU 调度的基本单位——这句话是全章最高频考点,务必一字不差记住。
7. 进程 vs 线程:全维度对照表
| 对比维度 | 进程 | 线程 |
|---|---|---|
| 定义角色 | 资源分配和保护的基本单位 | 处理机调度的基本单位 |
| 地址空间 | 各自独立 | 同进程内所有线程共享 |
| 拥有资源 | 拥有内存、文件、设备 | 基本不拥有资源,只有栈、寄存器等运行必需品 |
| 切换开销 | 大:换地址空间、刷缓存 | 小:不换地址空间,只换现场和栈 |
| 通信方式 | 需要专门的进程间通信机制,如管道、消息、共享内存 | 直接读写共享变量即可,但需要同步 |
| 隔离与健壮性 | 一个进程崩溃通常不影响他人 | 一个线程崩溃可能拖垮整个进程 |
| 创建销毁开销 | 大 | 小 |
注意最后一行的反面含义:线程共享带来了效率,也带来了脆弱——隔离性是进程的优势,共享性是线程的优势,两者是同一枚硬币的两面。第四章讲并发问题时,祸根正是线程间的共享。
8. 用户级线程 vs 内核级线程
线程可以由谁来管理?两种方案:
用户级线程:线程的创建、切换、调度全部由用户空间的线程库完成,内核根本不知道线程的存在,只看得见进程。
内核级线程:线程由内核直接管理,内核为每个线程建立控制块,调度以线程为单位。
| 对比项 | 用户级线程 | 内核级线程 |
|---|---|---|
| 内核是否感知 | 不感知 | 感知 |
| 切换开销 | 极小,不用进内核态 | 较大,需要陷入内核 |
| 一个线程阻塞时 | 整个进程全部线程被拖住 | 只阻塞该线程,其他线程照常调度 |
| 能否利用多核 | 不能,内核只给该进程一个 CPU | 能,多个线程可同时在多核上跑 |
| 调度自主权 | 应用可自定义调度策略 | 由内核统一调度 |
记忆抓手:用户级线程"便宜但瞎"——切换便宜,但内核瞎、看不见线程,一人阻塞全家等待;内核级线程"贵但明"——切换贵,但内核明察秋毫,能并行、能单独阻塞。现代主流系统(Linux、Windows)以内核级线程为主,也存在两者组合的多对多模型。
9. 上下文切换到底切换了什么
上下文(context)就是"恢复执行所需的全部现场"。上下文切换的完整流程:
切换内容按"进程切换"与"线程切换"区分:
| 切换内容 | 同进程内线程切换 | 进程切换 |
|---|---|---|
| 寄存器、程序计数器、栈指针 | 要切 | 要切 |
| 内核中的调度信息 | 要切 | 要切 |
| 地址空间(页表) | 不切,这正是线程切换便宜的原因 | 要切 |
| TLB、缓存的有效性 | 基本不受影响 | 可能大量失效,隐性成本高 |
另外注意:上下文切换本身是纯开销——切换期间 CPU 没有为任何用户程序干活。切换太频繁,系统就会把大量时间浪费在"换人"而不是"干活"上,这为第三章讨论时间片大小埋下伏笔。
10. 本章要点回顾
| 主题 | 必须记住的结论 |
|---|---|
| 程序与进程 | 程序静态、进程动态;进程 = 程序的一次执行 |
| PCB | 进程存在的唯一标志,存放标识、现场、控制、资源四类信息 |
| 三态模型 | 就绪差 CPU,阻塞差事件;阻塞不能直达运行,就绪不能直达阻塞 |
| 主动与被动 | 运行→阻塞是进程主动的,其余迁移都是被动的 |
| 线程的动机 | 进程切换贵 + 共享数据难,拆出"执行流"单独成为调度单位 |
| 核心口诀 | 进程是资源分配的基本单位,线程是 CPU 调度的基本单位 |
| 用户级 vs 内核级线程 | 用户级便宜但一人阻塞全家等、不能用多核;内核级相反 |
| 上下文切换 | 线程切换不换地址空间,所以便宜;切换本身是纯开销 |
11. 做题提醒
- "阻塞态的进程等到事件完成后直接投入运行"——错,必须先回就绪态排队。
- "就绪态的进程可以转为阻塞态"——错,只有运行中的进程才能提出等待请求。
- 单 CPU 系统中,处于运行态的进程最多 1 个,但就绪、阻塞态的进程可以有多个。
- "同一进程的各线程共享栈空间"——错,栈是线程私有的;共享的是代码、全局数据、堆和打开的文件。
- 时间片用完后进程进入就绪态而不是阻塞态——它没有在等任何事件,只是暂时没了 CPU。
- 判断切换开销的题目:同进程内线程切换 < 跨进程切换,根本原因是是否需要切换地址空间。
