休眠与性能 — 没活干的任务靠边站,CPU 时间还给大家
休眠与性能 — 没活干的任务靠边站,CPU 时间还给大家
这一章讲什么: 抢占式多任务的心脏跳起来了,但调度还蠢—— 没事干的主任务照样每 20ms 分走一份 CPU,任务多了鼠标就卡。 本章给调度装上常识:没活 = 休眠(退出运行队列),来活 = 唤醒(回到队尾); 消息队列从「全局一个」变成「每任务一个」,来消息自动唤醒收件人; 再按优先级分队列,高级别有活时低级别全部让路;最后放一个永不休眠的空闲任务兜底。 顺带给第 9 章的「先测量」纲领补一次诚实的实测。
1. 先看现象:S 键让任务睡着,W 键叫醒
主线例子:
按 S → TaskB::Sleep() → 从运行队列移除 → 它的计数窗口停住
按 W → TaskB::Wakeup() → 加回队列末尾 → 接着上次的数字继续数
主任务:ReceiveMessage() 拿不到消息 → 自己 Sleep()
→ 鼠标一动,SendMessage() 把它叫醒 → 高级别任务优先跑 → 鼠标不卡了
性能对账(14.3):
主任务不休眠版:主计数 1000 时 TaskB 数到 2991
主任务休眠版: 主计数 1004 时 TaskB 数到 4190 → 4190/2991 ≈ 1.40 倍
理论推算:4 个任务均分 → 每任务 2.5s;主任务睡掉后 3 个任务各 3.33s → 3.33/2.5 ≈ 1.33 倍
「这有些出入,但我们姑且认为事实就是如此」——实测与推算对不上账,照实写2. 顶层全景
14.1 休眠/唤醒:TaskManager 加 running_ 运行队列;Sleep(真)= SwitchTask(true) 把自己换下去
14.2 消息唤醒:每任务一个 msgs_ 队列;SendMessage = Push + (休眠中就 Wakeup);
ReceiveMessage 返回 std::optional(空队列 = nullopt,主任务见 nullopt 就睡)
14.3 测量:截图 + 目测 + 理论对账(1.40 vs 1.33)
14.4(并入本章)级别:running_ 从一条队列变 kMaxLevel+1 条;current_level_ +
level_changed_ 信号;高级别有活低级别不运行;Java GC 的「必须立刻做完又耗时」无解梗
14.5 空闲任务:TaskIdle() 只有 hlt,注册在 0 级 —— 运行队列永不为空,代码不用改
图说:Sleep/Wakeup 是动词,级别是形容词,空闲任务是兜底副词。3. 核心原理
3.1 运行队列:tasks_ 与 running_ 的分家
之前 tasks_ 既是名册又是队列;现在拆开:tasks_ 记所有任务(含睡的), running_ 是 FIFO 运行队列,只装能跑的1。SwitchTask() 从「数组下标 +1」 变成「队头移到队尾」——附带一个新参数 current_sleep=true 时不移回队尾, 当前任务就此离队(自己把自己睡下去,书里的图 14.1 画得很清楚)2。 Sleep() 的三种情形:睡别人(队列中部,erase 就好);睡自己(在队头,必须 SwitchTask(true)); 已经不在队列(早睡了,无事发生)。新任务默认不在队列里,NewTask() 之后必须 Wakeup() 一次3。
3.2 消息即闹钟:每任务一个队列
main_queue 全局一个,意味着只有主任务能被消息叫醒。推广成「每任务一个 msgs_」后,
SendMessage = 入队 + 若对方睡着就 Wakeup;ReceiveMessage 返回 std::optional<Message>——
队列空就返回「无效值」nullopt,而不是编一个魔法数(-1 分不清是错误还是数据)4。
主循环因此变成最简形态:msg = ReceiveMessage(); if (!msg) Sleep(); else 处理;——
无事就睡,来事即醒,这套原语此后贯穿全书(应用等按键、管道等数据,全是它)5。
3.3 级别:高优先级不是「多分时间」,是「独占」
另一种思路是给优先级高的任务更长的时间片(如 10 级得 0.1s、1 级得 0.01s)—— 作者否了它,因为鼠标要的是尽快开始,不是跑得更久6。 定下的语义:任务分级别,每个级别一条运行队列;有高级别任务可运行时,低级别一律不运行7。 实现三件套:running_ 变成数组;current_level_ 记当前正在跑哪一级; level_changed_ 是个「下次切换时重新定级」的信号位(比如高级别任务被唤醒, 要通知调度器「回头别从我这一级接着跑」)8。 约定的使用纪律:高级别任务必须短小,办完即睡——否则低级别全部饿死,跟协作式一个死法9。 书中顺手留了个著名的无解案例:Java 垃圾回收「既耗时又必须立刻做完」,是调度上最糟糕的组合10。
3.4 空闲任务:永不空的队列
所有任务都睡了,运行队列空了,SwitchTask() 就会抓瞎。 作者否掉了「判空特判」11,选了更优雅的解:注册一个永远不醒的空闲任务(TaskIdle), 循环体只有 hlt,放在最低的 0 级——队列永不为空,现有代码一个字不用改12。 红利立刻兑现:按 S 把 TaskB 和主任务都睡掉,系统依然正常; 不按 S 时 TaskB 的计数快了近 4 倍——空闲任务优先级最低,抢不到别人, 但别人睡着时它独占 hlt,省电且无害13。
3.5 测量:一次诚实的不对账
先把测量方法本身摊开。 工具简陋到只剩截图和秒表:主窗口的计数器每 0.01 秒跳 1(它 本身就是定时器 tick 的计数),所以「主计数到 1000」≈ 10 秒——到点掐屏,读 TaskB 窗口的数。 两个版本各掐一次:主任务不休眠时 TaskB 数到 2991;主任务会休眠时数到 419014。 理论账也摆出来对:不休眠版 4 个任务均分 CPU,每个任务 10 秒里拿 2.5 秒;休眠版主任务 几乎不耗 CPU,剩下 3 个任务各拿约 3.33 秒,3.33 ÷ 2.5 ≈ 1.33 倍——和实测的 1.40 倍 对不上。「这有些出入,但我们姑且认为事实就是如此」:方向(休眠让出了 CPU)可信, 精确倍数存疑,这个分寸感本身就是测量课的一部分15。
级别机制的完整账本。 14.4 节把 running_ 从一条队列改成 kMaxLevel+1 条(本书 4 条, 级别 0–3),另加两个状态变量16:
current_level_:当前正在跑的任务属于哪一级。初值是最高级——主任务天然是最高级, 这是「鼠标永远优先」的来源17;level_changed_:一个「下次切换时重新定级」的信号。比如某个高级别任务被唤醒, 调度器必须知道「别从当前这一级接着轮了,回头要从高往低重查」18。
Sleep/Wakeup 都要跟着改:睡自己时如果自己是当前级队头,要 SwitchTask(true) 把自己摘出去;
唤醒一个更高级别的任务时,置 level_changed_ 让下一轮切换重新定级19。
使用纪律再强调一遍:高级别任务必须短小、办完即睡——否则低级别全部饿死,
跟协作式多任务一个死法20。验证照旧靠「放慢到可见」:光标闪烁的定时器到点唤醒主任务,
主任务抢在其他任务前面处理,「超时到处理」的延迟明显变短21。
3.6 测量之后:记账的边界
方向性结论之外,书里还留了一句更普适的话:「可以断言,休眠主任务增加了 TaskB 的处理时间」9——断言的是方向,不是倍数。
3.7 验证的另一面:数字也会说话
级别机制落成后的验收,书里给了两组观察。第一组是体验:鼠标移动「非常顺畅」,文本框 光标该闪的时候准点闪22——这是「高级别任务被准时叫醒」的直接体感。第二组是数字: 主窗口的计数器以前总停在 1304 这种「差 4」的数上,引入级别后变成了 1300 整——差的那 4 是旧调度里「光标定时器到点 → 主任务被排到队尾 → 前面任务先跑」的累积延迟;级别机制 让主任务到点即跑,延迟消失,读数自然归整23。把「1304 → 1300」当证据用,是这本书 的招牌手法:不堆形容词,摆一个能复现的数字。
再补一笔环境账:这套测量全在 QEMU 模拟器里做,作者提醒「测量时请尽可能确保运行环境 良好」——宿主机 load 高一个格,4190 就会变成别的数。所以这组数字的可信区间是 「1.3–1.4 倍之间」,不是「1.40 倍整」24。
3.8 休眠的收益账(14.2 节的两笔小账)
消息唤醒机制落地后,书里顺手记了两笔账。第一笔是结构账:此前按键、定时器、图层 操作全挤在主循环一个上下文里,主循环的代码越长,事件互相拖延的可能越大;每任务一个 队列之后,「等消息」成了所有任务的统一姿态,主循环瘦成『ReceiveMessage → 分发』两行 骨架25。第二笔是功耗账的预告:主任务会睡之后,空转的 CPU 时间片从「浪费」变成 「让给别人或 hlt」——第 16 章删掉 TaskB 时,这笔账会兑现成 QEMU 占用从 100% 掉到 百分之几26。
3.9 对照实验的设计感(14.3 节再补一笔)
值得再夸一次的是对照的干净:两个版本唯一变量是「主任务睡不睡」,任务数、切换周期、 测量窗口(10 秒)全都不动。读者完全可以自己动手复现:改一行 Sleep、掐两张屏、对一次 比值——书里甚至把理论推算的每一步都写了出来(4 个任务 × 2.5s → 3 个任务 × 3.33s), 就是为了让读者能亲手查出「1.40 vs 1.33」的那点出入来自哪里27。这种「把实验设计 摊开给你抄」的写法,比结论本身更可带走。
3.10 与休眠同源的一个设计忠告
14.2 节在讲完消息唤醒后补了一段看似离题的话:主任务也不能成为繁重的处理器—— 「高优先级任务应在短时间内处理完成」28。这是休眠-唤醒体系的暗约束:唤醒机制 保证高级别任务优先开始,却不保证它尽快结束;一个赖着不睡的高级别任务, 比没有休眠机制之前的主循环更霸道(以前起码 20ms 轮一次,现在它可以独占)。 所以级别机制天然偏向「事件驱动、短促响应」的任务风格——鼠标、键盘、定时器全是 这种负载,这套调度也是为它们调的29。
3.11 测量课的小结
把 14.3 节的姿势收成一个可复用的清单:① 先定测量窗口(这里是 10 秒,借主计数器 当秒表);② 只动一个变量(睡不睡);③ 实测之外再算一笔理论账,两相对照;④ 对不上的 部分如实写,不圆谎30。四步里没有一步需要高级工具——截图、计数器、除法就够—— 缺的是「先测量,再优化」的纪律本身。全书后面每一次性能改动(第 15、22、24 章), 都是这套清单的重演31。
4. 作者的判断与证据
判断:调度问题优先用「休眠」解决,而不是优先级。 两条路在 14.1 开头并列, 先做休眠(不浪费),后做级别(保响应)——次序背后是「先别浪费,再谈偏好」。
判断:空闲任务优于判空特判。 「有一个更好的解决方案」——给系统一个永远成立的假设 (队列非空),换所有代码的简洁。这是「用不变量消灭分支」的标准示范。
证据: S/W 按键的冻结/复活截图;鼠标流畅度;以及那组 1.40/1.33 的诚实对账。
5. 边界与局限
- 级别是抢占式的近似,不是公平调度:高级别任务疯狂醒着,低级别照样饿死; 没有反馈、没有动态升降级(MLFQ 那套在 OSTEP)。
- Sleep/Wakeup 没有配对检查:重复 Wakeup 会重复入队(同一任务出现两次), 本书代码不防;真正的等待队列要处理引用计数。
- SendMessage 的队列是定长 std::deque,满了会抛异常——而内核禁用了异常; 教学系统假设消息不爆量。
- 单核假设依旧:所有调度决定都建立在「同一时刻只有一个在跑」上。
6. 可带走的
本章主线例子,一行写完: 主任务没消息就 Sleep;按键消息 SendMessage 把它叫醒;高级别有活低级别让路;全睡时空闲任务 hlt 兜底;TaskB 提速 1.4 倍实测。
- 运行队列 = 可运行任务的 FIFO;tasks_ 管名册,running_ 管调度,职责分开;
- Sleep 自己 =
SwitchTask(true)(出队不换入);Sleep 别人 = 直接 erase;睡了再睡 = 无事发生; - 消息即闹钟:Push + 条件 Wakeup;optional 表达「可能拿不到」,别用魔法数;
- 优先级 = 分级运行队列 + 高独占;高级别必须短小即睡,否则等于新形式的霸占;
- 空闲任务(0 级 + hlt)让「队列非空」成为不变量——消灭特判,顺手省电;
- 测量精度不够时,方向性结论 + 诚实的不对账胜过编造精确倍数。
7. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 两种解法与 Java GC 案例 | 14.1 休眠(osbook_day14a) | text/95-ch14-01-14-1-osbook-day14a.txt:6(搜「两种方法可以解决」) · text/95-ch14-01-14-1-osbook-day14a.txt:22(搜「垃圾回收」) |
| 运行队列与任务状态 | 14.1 休眠(osbook_day14a) | text/95-ch14-01-14-1-osbook-day14a.txt:27(搜「运行队列」) |
| SwitchTask(true) 语义 | 14.1 休眠(osbook_day14a) | text/95-ch14-01-14-1-osbook-day14a.txt:55(搜「添加到末尾」) |
| Sleep 三种情形 | 14.1 休眠(osbook_day14a) | text/95-ch14-01-14-1-osbook-day14a.txt:73(搜「Sleep()的实现」) |
| NewTask 后要 Wakeup | 14.1 休眠(osbook_day14a) | text/95-ch14-01-14-1-osbook-day14a.txt:116(搜「只能被创建而不会执行」) |
| S/W 键休眠实验 | 14.1 休眠(osbook_day14a) | text/96-ch14-02-14-2-osbook-day14b.txt:5(搜「按下S键」) |
| 消息即闹钟设计 | 14.2 事件发生时唤醒(osbook_day14b) | text/96-ch14-02-14-2-osbook-day14b.txt:45(搜「唤醒」) · text/96-ch14-02-14-2-osbook-day14b.txt:55(搜「SendMessage」) |
| std::optional ReceiveMessage | 14.2 事件发生时唤醒(osbook_day14b) | text/96-ch14-02-14-2-osbook-day14b.txt:61(搜「ReceiveMessage」) · text/96-ch14-02-14-2-osbook-day14b.txt:76(搜「主任务就会休眠」) |
| 1.40 倍实测与 1.33 推算 | 14.3 性能测量 | text/97-ch14-03-14-3.txt:22(搜「2991」) · text/97-ch14-03-14-3.txt:35(搜「1.40倍」) · text/97-ch14-03-14-3.txt:59(搜「1.33」) |
| 时间片方案被否 | 14.3 性能测量 | text/96-ch14-02-14-2-osbook-day14b.txt:34(搜「0.01s」) |
| 级别语义 | 14.3 性能测量 | text/97-ch14-03-14-3.txt:87(搜「三个级别」) · text/97-ch14-03-14-3.txt:100(搜「短时间内运行完成并休眠」) |
| running_ 数组与 friend | 14.3 性能测量 | text/97-ch14-03-14-3.txt:108(搜「kMaxLevel」) · text/97-ch14-03-14-3.txt:123(搜「friend TaskManager」) |
| 空闲任务设计 | 14.5 休眠任务(osbook_day14d) | text/98-ch14-05-14-5-osbook-day14d.txt:23(搜「空闲任务」) · text/98-ch14-05-14-5-osbook-day14d.txt:32(搜「0级」) |
| 空闲任务的红利 | 14.5 休眠任务(osbook_day14d) | text/98-ch14-05-14-5-osbook-day14d.txt:59(搜「4倍」) · text/98-ch14-05-14-5-osbook-day14d.txt:65(搜「うまく防ぐ」——日文残页,中文 :57 搜「奇怪」) |
Footnotes
-
出处:「14.1 休眠(osbook_day14a)」(
text/95-ch14-01-14-1-osbook-day14a.txt:6(搜「两种方法可以解决」);text/95-ch14-01-14-1-osbook-day14a.txt:22(搜「垃圾回收」))。 ↩ -
出处:「14.1 休眠(osbook_day14a)」(
text/95-ch14-01-14-1-osbook-day14a.txt:27(搜「运行队列」))。 ↩ -
出处:「14.1 休眠(osbook_day14a)」(
text/95-ch14-01-14-1-osbook-day14a.txt:55(搜「添加到末尾」))。 ↩ -
出处:「14.1 休眠(osbook_day14a)」(
text/95-ch14-01-14-1-osbook-day14a.txt:73(搜「Sleep()的实现」))。 ↩ -
出处:「14.1 休眠(osbook_day14a)」(
text/96-ch14-02-14-2-osbook-day14b.txt:5(搜「按下S键」))。 ↩ -
出处:「14.2 事件发生时唤醒(osbook_day14b)」(
text/96-ch14-02-14-2-osbook-day14b.txt:45(搜「唤醒」);text/96-ch14-02-14-2-osbook-day14b.txt:55(搜「SendMessage」))。 ↩ -
出处:「14.2 事件发生时唤醒(osbook_day14b)」(
text/96-ch14-02-14-2-osbook-day14b.txt:61(搜「ReceiveMessage」);text/96-ch14-02-14-2-osbook-day14b.txt:76(搜「主任务就会休眠」))。 ↩ -
出处:「14.2 事件发生时唤醒」(
text/96-ch14-02-14-2-osbook-day14b.txt:76,搜「主任务就会休眠」)。 ↩ -
出处:「14.3 性能测量」(
text/97-ch14-03-14-3.txt:22(搜「2991」);text/97-ch14-03-14-3.txt:35(搜「1.40倍」);text/97-ch14-03-14-3.txt:59(搜「1.33」))。 ↩ ↩2 -
出处:「14.3 性能测量」(
text/96-ch14-02-14-2-osbook-day14b.txt:34(搜「0.01s」))。 ↩ -
出处:「14.5 休眠任务(osbook_day14d)」(
text/98-ch14-05-14-5-osbook-day14d.txt:23(搜「空闲任务」);text/98-ch14-05-14-5-osbook-day14d.txt:32(搜「0级」);text/98-ch14-05-14-5-osbook-day14d.txt:59(搜「4倍」))。 ↩ -
出处:「14.3 性能测量」(
text/97-ch14-03-14-3.txt:87(搜「三个级别」);text/97-ch14-03-14-3.txt:100(搜「短时间内运行完成并休眠」))。 ↩ -
出处:「14.3 性能测量」(
text/97-ch14-03-14-3.txt:108(搜「kMaxLevel」);text/97-ch14-03-14-3.txt:123(搜「friend TaskManager」))。 ↩ -
出处:「14.3 性能测量」(
text/97-ch14-03-14-3.txt:22,搜「2991」;:33,搜「4190」)。 ↩ -
出处:「14.3 性能测量」(
text/97-ch14-03-14-3.txt:35,搜「1.40倍」;:59,搜「1.33」;:64,搜「姑且认为」)。 ↩ -
出处:「14.3 性能测量」(
text/97-ch14-03-14-3.txt:108,搜「kMaxLevel」;:112,搜「数组」;:123,搜「friend TaskManager」)。 ↩ -
出处:「14.3 性能测量」(
text/97-ch14-03-14-3.txt:115,搜「初始值设置为kMaxLevel」;:131,搜「主任务将具有最高优先级」)。 ↩ -
出处:「14.3 性能测量」(
text/97-ch14-03-14-3.txt:117,搜「level_changed_」;:119,搜「查看current_level_」)。 ↩ -
出处:「14.3 性能测量」(
text/97-ch14-03-14-3.txt:135,搜「适应级别机制」;:145,搜「设为true」;:165,搜「旋转」;:171,搜「审查」)。 ↩ -
出处:「14.3 性能测量」(
text/97-ch14-03-14-3.txt:100,搜「短时间内运行完成并休眠」)。 ↩ -
出处:「14.5 休眠任务」(
text/98-ch14-05-14-5-osbook-day14d.txt:11,搜「1300」——日文残页,主任务优先使超时处理延迟变短的验证)。 ↩ -
出处:「14.5 休眠任务」(
text/98-ch14-05-14-5-osbook-day14d.txt:10,搜「非常にスムーズ」——日文残页;中文同页「鼠标移动顺畅」)。 ↩ -
出处:「14.5 休眠任务」(
text/98-ch14-05-14-5-osbook-day14d.txt:11,搜「1304」;:14,搜「短くなった」——日文残页,超时到事件处理变短的证据)。 ↩ -
出处:「14.3 性能测量」(
text/97-ch14-03-14-3.txt:8,搜「休眠」;text/69-ch09-05-9-5-osbook-day09c.txt:8,搜「确保运行环境良好」——测量环境提醒的出处同第 9 章)。 ↩ -
出处:「14.2 事件发生时唤醒」(
text/96-ch14-02-14-2-osbook-day14b.txt:74,搜「主循环」;:79,搜「主任务就会休眠」)。 ↩ -
出处:「16.6 省电」(
text/109-ch16-06-16-6-osbook-day16f.txt:11,搜「100%」;:11,搜「百分之几」——这笔账的兑现见第 16 章拆解)。 ↩ -
出处:「14.3 性能测量」(
text/97-ch14-03-14-3.txt:15,搜「非常原始」;:52,搜「2.5秒」;:59,搜「3.33」;:64,搜「出入」)。 ↩ -
出处:「14.2 事件发生时唤醒」(
text/96-ch14-02-14-2-osbook-day14b.txt:43,搜「不能成为繁重的处理器」;:45,搜「并不局限于主任务」)。 ↩ -
出处:「14.2 事件发生时唤醒」(
text/96-ch14-02-14-2-osbook-day14b.txt:39,搜「休眠模式」;:41,搜「暂停状态」——主任务平时休眠、事件来即醒的运行模型)。 ↩ -
出处:「14.3 性能测量」(
text/97-ch14-03-14-3.txt:15,搜「非常原始」;:35,搜「1.40倍」;:64,搜「出入」;:67,搜「可以断言」)。 ↩ -
出处:「14.3 性能测量」(
text/97-ch14-03-14-3.txt:11,搜「更快」——「TaskB 中的计数器会转得更快。让我们来测试一下」的原文语气)。 ↩