上下文切换:机制与成本
上下文切换(context switch)是内核暂停当前执行流、保存其完整状态、再恢复另一执行流状态使其继续运行的操作。
一句话定义
上下文切换(context switch)是内核暂停当前执行流、保存其完整状态、再恢复另一执行流状态使其继续运行的操作。
为什么重要
虚拟化 CPU 的本质就是"不停地快速切换"。切换成本直接决定了调度器的设计(时间片不能太小)、线程数量的合理上限、以及很多延迟毛刺的来源。凡是"系统空闲但程序变慢"的疑案,切换开销都是头号嫌疑人。
前置知识
kp-002(寄存器与 TLB 是什么);kp-005(状态机中运行/就绪的转换)。
核心概念
- 直接成本:保存/恢复寄存器组、切换内核栈、更新 PCB、执行调度器代码本身。
- 间接成本:切换后 CPU 缓存与 TLB 里装的是"前任"的数据,新进程要重新预热——这部分常大于直接成本。
- 自愿 vs 非自愿切换:自愿指进程因 I/O、锁等主动让出;非自愿指时间片用尽被抢占。
- 线程切换 vs 进程切换:同进程内线程切换不换页表,省掉地址空间切换与 TLB 刷新的大头。
原理与机制
以两个进程 A、B 之间的切换为例(伪代码):
时钟中断发生(A 正在运行,用户态)
-> 硬件保存 A 的用户寄存器到 A 的内核栈,陷入内核
-> 内核把 A 标记为就绪,PCB 记录内核栈指针
-> 调度器选中 B
-> 切换内核栈指针到 B 的内核栈
-> 若 B 属于不同进程:切换页表基址寄存器(CR3),TLB 部分失效
-> 从 B 的 PCB 恢复其寄存器组
-> 返回用户态,B 从上次被打断的位置继续执行
成本数量级:现代 Linux 上一次进程切换的直接开销约 1~10 微秒;叠加缓存/TLB 预热后,高频切换场景(每秒数万次)可吃掉可观 CPU 时间。这就是为什么 kp-010 的调度器绝不允许时间片无限小,也是 kp-015 无锁编程追求"减少切换"的原因之一。
图示
时间轴: A运行 |A上下文保存+B恢复| B运行 |...|
^-- 直接成本(微秒级)
缓存/TLB: [A的热数据] [B的热数据] <-- 间接成本:B 重新预热
直观类比
上下文切换像换班:直接成本是交接班时填表(保存/恢复寄存器),间接成本是新班次对现场完全不熟、要花时间重新摸清情况(缓存与 TLB 预热)——后者往往更耗时。
实例或案例
vmstat 1 # cs 列:每秒上下文切换次数
pidstat -w -p <pid> 1 # 该进程的自愿/非自愿切换分项(sysstat 工具)
perf stat -e context-switches,cpu-migrations ls # 一次命令的切换计数
实验:跑一个纯计算死循环,观察 vmstat 的 cs 很低;再跑大量短命小进程(如 while true; do true; done),cs 飙升且用户态 CPU 占比下降——切换成本的可视化。
常见误区
- 认为切换是免费的、开几千个线程没关系:内存可以撑,但切换与调度开销撑不住。
- 混淆"阻塞"与"切换成本":阻塞本身把 CPU 让给别的进程是好事;真正的代价是切换动作与其后的缓存失效。
- 认为线程切换和进程切换一样贵:同进程线程切换不换页表,明显更便宜,这是线程存在的核心理由之一。
与其他知识点的关系
kp-010 的调度算法在"时间片大小"上与切换成本直接博弈;kp-015 与 kp-028 的设计都包含减少陷入与切换的动机。
延伸阅读
《操作系统导论》进程 API 与调度机制章节;《Linux 内核设计与实现》第 4 章。
自测题
- 上下文切换的直接成本与间接成本分别是什么?
答:直接成本是保存/恢复寄存器、切换内核栈与页表等内核操作本身;间接成本是切换后 CPU 缓存与 TLB 中的旧数据失效、新进程需要重新预热带来的性能损失。
- 为什么同进程的线程切换比进程切换便宜?
答:线程共享地址空间,切换不需要更换页表基址、不会导致 TLB 大范围失效,只需保存恢复寄存器与栈。
- 时间片越小调度越 responsive,为什么不能无限小?
答:时间片过小会使切换开销占比失控——大量 CPU 时间花在切换本身而非真正工作上。