跳转至

休眠与性能 — 没活干的任务靠边站,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_ 是个「下次切换时重新定级」的信号位(比如高级别任务被唤醒, 要通知调度器「回头别从我这一级接着跑」)13。 约定的使用纪律:高级别任务必须短小,办完即睡——否则低级别全部饿死,跟协作式一个死法8。 书中顺手留了个著名的无解案例:Java 垃圾回收「既耗时又必须立刻做完」,是调度上最糟糕的组合9。

3.4 空闲任务:永不空的队列

所有任务都睡了,运行队列空了,SwitchTask() 就会抓瞎。 作者否掉了「判空特判」12,选了更优雅的解:注册一个永远不醒的空闲任务(TaskIdle), 循环体只有 hlt,放在最低的 0 级——队列永不为空,现有代码一个字不用改10。 红利立刻兑现:按 S 把 TaskB 和主任务都睡掉,系统依然正常; 不按 S 时 TaskB 的计数快了近 4 倍——空闲任务优先级最低,抢不到别人, 但别人睡着时它独占 hlt,省电且无害11。

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 的处理时间」8——断言的是方向,不是倍数。

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 倍实测。

  1. 运行队列 = 可运行任务的 FIFO;tasks_ 管名册,running_ 管调度,职责分开;
  2. Sleep 自己 = SwitchTask(true)(出队不换入);Sleep 别人 = 直接 erase;睡了再睡 = 无事发生;
  3. 消息即闹钟:Push + 条件 Wakeup;optional 表达「可能拿不到」,别用魔法数;
  4. 优先级 = 分级运行队列 + 高独占;高级别必须短小即睡,否则等于新形式的霸占;
  5. 空闲任务(0 级 + hlt)让「队列非空」成为不变量——消灭特判,顺手省电;
  6. 测量精度不够时,方向性结论 + 诚实的不对账胜过编造精确倍数。

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 搜「奇怪」)

  1. 出处:「14.1 休眠(osbook_day14a)」(text/95-ch14-01-14-1-osbook-day14a.txt:6(搜「两种方法可以解决」); text/95-ch14-01-14-1-osbook-day14a.txt:22(搜「垃圾回收」))。 ↩

  2. 出处:「14.1 休眠(osbook_day14a)」(text/95-ch14-01-14-1-osbook-day14a.txt:27(搜「运行队列」))。 ↩

  3. 出处:「14.1 休眠(osbook_day14a)」(text/95-ch14-01-14-1-osbook-day14a.txt:55(搜「添加到末尾」))。 ↩

  4. 出处:「14.1 休眠(osbook_day14a)」(text/95-ch14-01-14-1-osbook-day14a.txt:73(搜「Sleep()的实现」))。 ↩

  5. 出处:「14.1 休眠(osbook_day14a)」(text/96-ch14-02-14-2-osbook-day14b.txt:5(搜「按下S键」))。 ↩

  6. 出处:「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」))。 ↩

  7. 出处:「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(搜「主任务就会休眠」))。 ↩

  8. 出处:「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」))。 ↩↩

  9. 出处:「14.3 性能测量」(text/96-ch14-02-14-2-osbook-day14b.txt:34(搜「0.01s」))。 ↩

  10. 出处:「14.3 性能测量」(text/97-ch14-03-14-3.txt:87(搜「三个级别」); text/97-ch14-03-14-3.txt:100(搜「短时间内运行完成并休眠」))。 ↩

  11. 出处:「14.3 性能测量」(text/97-ch14-03-14-3.txt:108(搜「kMaxLevel」); text/97-ch14-03-14-3.txt:123(搜「friend TaskManager」))。 ↩

  12. 出处:「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倍」))。 ↩

  13. 出处:「14.2 事件发生时唤醒」(text/96-ch14-02-14-2-osbook-day14b.txt:76,搜「主任务就会休眠」)。 ↩

  14. 出处:「14.3 性能测量」(text/97-ch14-03-14-3.txt:22,搜「2991」;:33,搜「4190」)。 ↩

  15. 出处:「14.3 性能测量」(text/97-ch14-03-14-3.txt:35,搜「1.40倍」;:59,搜「1.33」;:64,搜「姑且认为」)。 ↩

  16. 出处:「14.3 性能测量」(text/97-ch14-03-14-3.txt:108,搜「kMaxLevel」;:112,搜「数组」;:123,搜「friend TaskManager」)。 ↩

  17. 出处:「14.3 性能测量」(text/97-ch14-03-14-3.txt:115,搜「初始值设置为kMaxLevel」;:131,搜「主任务将具有最高优先级」)。 ↩

  18. 出处:「14.3 性能测量」(text/97-ch14-03-14-3.txt:117,搜「level_changed_」;:119,搜「查看current_level_」)。 ↩

  19. 出处:「14.3 性能测量」(text/97-ch14-03-14-3.txt:135,搜「适应级别机制」;:145,搜「设为true」;:165,搜「旋转」;:171,搜「审查」)。 ↩

  20. 出处:「14.3 性能测量」(text/97-ch14-03-14-3.txt:100,搜「短时间内运行完成并休眠」)。 ↩

  21. 出处:「14.5 休眠任务」(text/98-ch14-05-14-5-osbook-day14d.txt:11,搜「1300」——日文残页,主任务优先使超时处理延迟变短的验证)。 ↩

  22. 出处:「14.5 休眠任务」(text/98-ch14-05-14-5-osbook-day14d.txt:10,搜「非常にスムーズ」——日文残页;中文同页「鼠标移动顺畅」)。 ↩

  23. 出处:「14.5 休眠任务」(text/98-ch14-05-14-5-osbook-day14d.txt:11,搜「1304」;:14,搜「短くなった」——日文残页,超时到事件处理变短的证据)。 ↩

  24. 出处:「14.3 性能测量」(text/97-ch14-03-14-3.txt:8,搜「休眠」;text/69-ch09-05-9-5-osbook-day09c.txt:8,搜「确保运行环境良好」——测量环境提醒的出处同第 9 章)。 ↩

  25. 出处:「14.2 事件发生时唤醒」(text/96-ch14-02-14-2-osbook-day14b.txt:74,搜「主循环」;:79,搜「主任务就会休眠」)。 ↩

  26. 出处:「16.6 省电」(text/109-ch16-06-16-6-osbook-day16f.txt:11,搜「100%」;:11,搜「百分之几」——这笔账的兑现见第 16 章拆解)。 ↩

  27. 出处:「14.3 性能测量」(text/97-ch14-03-14-3.txt:15,搜「非常原始」;:52,搜「2.5秒」;:59,搜「3.33」;:64,搜「出入」)。 ↩

  28. 出处:「14.2 事件发生时唤醒」(text/96-ch14-02-14-2-osbook-day14b.txt:43,搜「不能成为繁重的处理器」;:45,搜「并不局限于主任务」)。 ↩

  29. 出处:「14.2 事件发生时唤醒」(text/96-ch14-02-14-2-osbook-day14b.txt:39,搜「休眠模式」;:41,搜「暂停状态」——主任务平时休眠、事件来即醒的运行模型)。 ↩

  30. 出处:「14.3 性能测量」(text/97-ch14-03-14-3.txt:15,搜「非常原始」;:35,搜「1.40倍」;:64,搜「出入」;:67,搜「可以断言」)。 ↩

  31. 出处:「14.3 性能测量」(text/97-ch14-03-14-3.txt:11,搜「更快」——「TaskB 中的计数器会转得更快。让我们来测试一下」的原文语气)。 ↩