🐧 第四章 进程调度器
📄 一句话概括

换人要付两笔钱:看得见的(保存/恢复寄存器、换栈、换页表)与看不见的(缓存和 TLB 全凉)。时间片从 16 缩到 1,切换次数从 9 次涨到 99 次——这笔税交得值不值,取决于你要的响应速度。

📝 本节和前面几节的关系

4.4 说调度在【分配】,这一节说分配本身要花钱:每换一次人,就有一笔固定开销,与换了多少活无关。

一、切换一次,具体发生什么

保存现场:当前进程的寄存器(程序计数器、栈指针、通用寄存器)存进它的进程控制块。 换地址空间:把页表基址寄存器换成新进程的——这一步就换掉了整个【世界】。 换栈:内核栈也要换,因为返回地址在新进程的栈上。 恢复现场:把新进程上次存下的寄存器装回去,跳回它当初停下的地方。 四步都是固定开销,与进程多大、活儿多少无关。

二、看不见的那笔最贵

切完之后,缓存和 TLB 都是凉的。 TLB 记的是【虚拟地址到物理地址】的翻译,换进程就得刷(或用地址空间标识区分)。刷完之后前几百次访存都要重新查页表。 CPU 缓存里装的是上一个进程的数据,新进程一上来基本全是缺。这段时间它跑得比平时慢好几倍。 这笔开销没人给你记账——top 里看不到【缓存冷】这一项,但它真实存在,而且往往比保存寄存器那点开销大一个数量级。

三、时间片越短,税越重

实测:时间片 16 时切换 9 次,时间片 1 时切换 99 次。 每拍切换的比例从 0.09 涨到 0.99——也就是几乎每一拍都在换人。 如果每次切换真机上是几微秒,那么时间片 1 毫秒时,光切换就吃掉可观的 CPU 时间。 这就解释了为什么 Linux 的默认时间片是毫秒级而不是微秒级:在【响应快】与【切换税】之间取一个平衡。

四、三个减少切换的办法

一、别开太多线程。每个线程都是一个可调度实体,线程数超过核数,切换就是常态。 二、减少锁竞争。等锁会让出 CPU,醒来又要抢——一次等锁可能引起两次切换。 三、用【批量】代替【频繁小任务】。同一件事拆成一千个小任务,调度器要切换一千次;合成几个大任务,切换次数直接掉下来。 这三条在工程上非常实用:性能问题的根因,有一大半最终能追到【切换太多】上。

五、动手 Demo:上下文切换——换人不要钱吗

先看【切换一次要做什么】的清单,再量【时间片变小,切换次数怎么涨】。 表里最后一行【每拍切换】是重点:它直接告诉你税有多重。

rows = []
for q in (1, 2, 4, 8, 16):
    ps = [Proc("P%d" % i, 20) for i in range(1, 6)]
    done, sw, _tl, total = run(ps, quantum=q, ncpu=1)
    rows.append((q, sw, total))

for q, sw, total in rows:
    print(q, "切换", sw, "总耗时", total, "每拍切换", sw / total)

实跑输出:

--------------------------------------------------------------------
上下文切换——换人不要钱吗
--------------------------------------------------------------------
① 切换一次要干什么

  要做的事                 属于哪一类     能不能省
  -----------------------------------------------------
  保存当前进程的寄存器     固定开销       不能
  换页表(换地址空间)     可能很贵       同进程内可省
  换栈                     固定开销       不能
  刷新 TLB/缓存变冷       隐性开销       换核更严重

  **看不见的那笔最贵**:换完之后缓存和 TLB 都是凉的,
  新进程前几微秒几乎在等内存。**这笔开销没人给你记账。**

② 时间片越小,切换越多,浪费越大

  时间片     切换次数     总耗时     每拍切换
  --------------------------------------------
  1          99           100        0.99
  2          49           100        0.49
  4          24           100        0.24
  8          14           100        0.14
  16         9            100        0.09

  **时间片从 16 缩到 1,切换次数翻上去一大截。**
  **如果每次切换按几微秒算,时间片 1 毫秒就意味着【每一毫秒都在交税】。**
  **所以时间片不是越小越好**——小到切换成本占掉可观比例,就白折腾了。

时间片 16 → 切换 9 次;时间片 1 → 切换 99 次。总耗时都是 100 拍(五个各 20 拍,这是死数)。 切换次数的增长是线性的:时间片砍一半,切换大致翻一倍。所以【小时间片】的代价是可以算出来的。 注意【每拍切换】这个指标:0.99 意味着几乎每拍都在换人,这时候 CPU 花在【管理调度】上的比例就不可忽略了。

📋 在真 Linux 上量这件事:vmstat 1 的 cs 列就是每秒上下文切换次数;pidstat -w 1 能按进程给;perf stat -e context-switches ./程序 单次运行就给。一个健康的系统,cs 通常在几千到几万;上百万就说明切换太频繁了。

30 秒实验

  1. vmstat 1 5 看 cs 列的波动,再开一个死循环 while true; do :; done 观察它变不变。
  2. pidstat -w 1 3(sysstat 包)看哪个进程在频繁切换。
  3. cat /proc/<pid>/status | grep -i switch——每个进程都记着它切换了多少次。

常见误区

  1. 【上下文切换就是保存寄存器】。那只是小头。大头是缓存和 TLB 变凉,以及换页表。
  2. 【切换次数越少越好】。切换是【分时】的前提。零切换意味着一个进程占着 CPU 不放,交互式系统就废了。
  3. 【线程越多并发越高】。超过核数之后,多出来的线程只会增加切换。并发高不高看的是【同时推进的任务数】,不是线程数。
自测三问(点开对答案) 1. 上下文切换的四步是什么?哪一步最贵? —— 保存现场、换地址空间、换栈、恢复现场。**换页表引起的缓存与 TLB 变凉最贵**,而且没人给你记账。 2. 时间片从 16 缩到 1,切换次数怎么变?为什么? —— 大约从 9 次涨到 99 次,**近似反比**——时间片砍一半,切换翻一倍。 3. 工程上减少切换的三个办法? —— 别开太多线程、减少锁竞争、用批量代替频繁小任务。

相关笔记