换人要付两笔钱:看得见的(保存/恢复寄存器、换栈、换页表)与看不见的(缓存和 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 秒实验
vmstat 1 5看cs列的波动,再开一个死循环while true; do :; done观察它变不变。pidstat -w 1 3(sysstat 包)看哪个进程在频繁切换。cat /proc/<pid>/status | grep -i switch——每个进程都记着它切换了多少次。
常见误区
- 【上下文切换就是保存寄存器】。那只是小头。大头是缓存和 TLB 变凉,以及换页表。
- 【切换次数越少越好】。切换是【分时】的前提。零切换意味着一个进程占着 CPU 不放,交互式系统就废了。
- 【线程越多并发越高】。超过核数之后,多出来的线程只会增加切换。并发高不高看的是【同时推进的任务数】,不是线程数。
自测三问(点开对答案)
1. 上下文切换的四步是什么?哪一步最贵? —— 保存现场、换地址空间、换栈、恢复现场。**换页表引起的缓存与 TLB 变凉最贵**,而且没人给你记账。 2. 时间片从 16 缩到 1,切换次数怎么变?为什么? —— 大约从 9 次涨到 99 次,**近似反比**——时间片砍一半,切换翻一倍。 3. 工程上减少切换的三个办法? —— 别开太多线程、减少锁竞争、用批量代替频繁小任务。- ☐ 4.6 进程的状态
相关笔记
- 结束进程 — 僵尸也是一种状态
- 进程的状态 — 状态就是队列
- 多级页表 — TLB 的前置知识
- 🐧 Linux是怎样工作的 · 索引