核心 02-进程线程与调度 预计 25 分钟 kp-006

上下文切换:机制与成本

上下文切换(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 章。

自测题

  1. 上下文切换的直接成本与间接成本分别是什么?

答:直接成本是保存/恢复寄存器、切换内核栈与页表等内核操作本身;间接成本是切换后 CPU 缓存与 TLB 中的旧数据失效、新进程需要重新预热带来的性能损失。

  1. 为什么同进程的线程切换比进程切换便宜?

答:线程共享地址空间,切换不需要更换页表基址、不会导致 TLB 大范围失效,只需保存恢复寄存器与栈。

  1. 时间片越小调度越 responsive,为什么不能无限小?

答:时间片过小会使切换开销占比失控——大量 CPU 时间花在切换本身而非真正工作上。

标签:#上下文切换 #调度 #性能 #TLB