进程切换
2.2.2 讲的是决定换谁,这一节讲换的过程本身——保存什么、恢复什么、要花多大代价。
本节的考点高度集中在三组辨析上:调度 vs 切换、进程切换 vs 模式切换、切换开销从哪儿来。
机制
切换要做什么
进程切换的实质是:把当前进程的运行现场原样封存,再把下一个进程封存过的现场原样恢复。
三步:
- 保存原进程的上下文——通用寄存器、PC、PSW、栈指针等现场信息写回该进程的 PCB;同时更新 PCB 中的状态、统计信息。
- 更新 PCB 并转移队列——把原进程的 PCB 移入就绪队列或某个阻塞队列。
- 恢复新进程的上下文——从新进程的 PCB 中取出现场装入各寄存器,并切换地址空间(改页表基址寄存器)。
第 3 步里的”切换地址空间”是开销的大头,见下文。
切换开销从哪儿来
直接开销是保存和恢复那几十个寄存器——这部分其实很小。
真正昂贵的是间接开销:
① 切换页表基址寄存器导致 TLB 全部失效。 新进程的逻辑地址与旧进程毫无关系,TLB里缓存的全部映射瞬间作废。之后新进程每访问一个页都要重新走地址转换、重新填 TLB。
② Cache 命中率骤降。 新进程访问的是完全不同的数据,Cache 里装的还是旧进程的内容,需要一段时间才能”热”起来。
这两条合起来,可能比保存寄存器贵一个数量级。 而这也正好解释了线程为什么”轻”——同进程内的线程切换不换地址空间,TLB 不失效,上述两条间接开销全部避免。
切换只能在内核态完成
进程切换必须在内核态执行,有两层原因:
权限层面:切换要读写 PCB(位于内核空间)、要修改页表基址寄存器、要操作内核栈。这些都是特权操作,用户态执行会直接触发特权指令异常。
逻辑层面:切换的决策依据是就绪队列的状态,而就绪队列是内核数据结构,用户态程序根本看不到它。
由此得到一条常考结论:任何一次进程切换,都必然先经历一次模式切换(从用户态进入内核态)。
边界
调度 ≠ 切换
调度是决策(按算法选出下一个该运行的进程),切换是执行(保存旧现场、恢复新现场)。
先有调度,后有切换。但要注意:调度的结果可能是”还是原来那个进程”——此时不发生切换。
例如时间片用完时重新调度,若就绪队列中没有比它更合适的,它可以继续运行,一次切换都不必做。
所以”发生了调度”不等于”发生了切换”。 这是选择题的常见设问点。
进程切换 vs 模式切换
这是全书最高频的边界之一,与 2.1.4 的结论一致:
| 模式切换 | 进程切换 | |
|---|---|---|
| 换的是什么 | CPU 的运行模式(用户态↔内核态) | 正在运行的进程 |
| 进程变了吗 | 没变,还是同一个 | 变了 |
| 要换地址空间吗 | 不用 | 要(改页表基址寄存器) |
| TLB 失效 | 否 | 是 |
| 开销 | 小 | 大 |
关系是单向的:进程切换一定伴随模式切换,反过来不成立。
一个进程调用 getpid() 这类系统调用,进内核、取个数、回用户态,全程都是它自己,没有发生任何进程切换。
上下文切换 vs 进程切换
两个词常被混用,但严格说粒度不同:
- 上下文切换是那个具体动作——保存一份现场、恢复另一份现场。
- 进程切换是整件事,它包含一次上下文切换,还包含更新 PCB、转移队列、切换地址空间。
调度程序的三部分里那个”上下文切换器”,负责的正是前者。它会执行两次上下文切换:第一次保存原进程现场并装入分派程序的上下文,第二次移出分派程序的上下文并装入新进程的现场。
对照速查
| 切换三步 | 内容 |
|---|---|
| ① 保存原进程上下文 | 现场信息写回 PCB |
| ② 更新 PCB 并转移队列 | 移入就绪队列或阻塞队列 |
| ③ 恢复新进程上下文 | 装入寄存器 + 切换地址空间 |
| 开销 | 来源 | 大小 |
|---|---|---|
| 直接 | 保存/恢复寄存器 | 小 |
| 间接 | TLB 全部失效 | 大 |
| 间接 | Cache 命中率骤降 | 大 |
| 调度 | 切换 | |
|---|---|---|
| 性质 | 决策(选谁) | 执行(怎么换) |
| 一定发生吗 | — | 不一定(可能仍是原进程) |
考点
- 调度 ≠ 切换:调度结果可能仍是原进程,此时不发生切换
- 进程切换一定伴随模式切换,反之不成立
- 切换开销的大头是 TLB 失效与 Cache 失效,不是保存寄存器
- 进程切换只能在内核态完成(权限 + 逻辑两层原因)
- 上下文切换器执行两次上下文切换
链接
- 🏠 返回总览:操作系统第 2 章:进程与线程总览
- ⬅️ 上一节:2.2.3 调度的目标
- ➡️ 下一节:2.2.5 CPU 调度算法
- 🔗 上下文的组成见 2.1.2 进程的组成
- 📖 名词库:第 2 章名词库