定时器 — 从「数秒表的圈数」到一毫秒的心跳
定时器 — 从「数秒表的圈数」到一毫秒的心跳
这一章讲什么: 操作系统需要「时间」:定时切换任务、光标闪烁、应用睡 3 秒—— 这些都要求同时跑很多个定时器。本章的路线:Local APIC 定时器改周期模式、 每次中断给一个 tick 计数;tick 之上用优先级队列造任意多个逻辑定时器,超时发消息; 最后发现 tick 的单位「一次计数是多少秒」每台机器都不同,于是启动标定工程: 从 UEFI 的配置表里挖出 ACPI 的 RSDP(校准所需的官方时钟的地址线索藏在后面几张表里)。
1. 先看现象:一颗心跳如何变成一张时刻表
主线例子,两个定时器同时跑:
硬件层:Local APIC 定时器(每个 CPU 核只有一个)
→ 周期模式:Initial Count 减到 0 就发一次中断(向量 kLAPICTimer),自动重装
计数层:TimerManager::Tick() —— 每次中断 ++tick_(volatile!)
逻辑层:timers_ 是优先级队列,装着任意多个 Timer{超时时刻, 通知值}
Tick() 时:队头若超时 → 发消息 {kTimerTimeout, arg.timer} 进 main_queue → 弹出
→ 再看新队头,循环
应用层示例:注册「200 次后超时,值=1」和「600 次后超时,值=2」;
收到值>0 的通知后立刻再注册一个「100 次后超时」→ 周期定时器;
图说:一个硬件定时器 = 一个永远排队的哨兵;
逻辑定时器随便开,成本只是队列里多一个元素。2. 顶层全景
11.1 重构:main.cpp 瘦身,InitializeXXX() 分家;自建 ArrayQueue 退役换标准库队列
11.2 定时器中断:周期模式 + LVT Timer 开中断 + IDT 注册 + 处理程序推消息
11.3 精确定时:TimerManager 数 tick;volatile 专栏;读 tick 要 cli/sti 夹住
11.4 多定时器:Timer 类 + priority_queue(less = 超时近者优先)+ 看门狗 + 超时消息
11.5 校准前哨:Local APIC 周期因机而异(QEMU 几秒 100 次,真机几十秒)
→ 顺 UEFI 配置表找 ACPI RSDP(表链:RSDP → XSDT → FADT → PM 定时器口)
图说:本章交付「多定时器 + 超时通知」,留下「tick = 多少毫秒」给下章。3. 核心原理
3.1 一个硬件定时器,多个逻辑定时器
每个 CPU 核只有一个 Local APIC 定时器,需求却是「N 个定时器同时跑」。 答案是分两层:硬件定时器以短周期发中断(周期模式:减到 0 自动重装 Initial Count, 并沿设定的向量发中断);每次中断 tick_++,tick 就是系统的原始时间轴1。 中断向量 kLAPICTimer 要选在 0x21–0xff 里没被占用的号2。 处理程序依旧是「理想形态」:Tick() 完就 NotifyEndOfInterrupt()—— 忘了 EOI,第二次中断就不会再响3。
3.2 volatile 与中断并发:同一个陷阱第二次现身
tick_ 在中断处理程序里改、在主循环里读。编译器看不见中断何时来,
它可能认定「这个循环里 tick_ 不会变」而把 CurrentTick() 优化成固定值——
while (begin + 10 > CurrentTick()); 就变成死循环4。
volatile 的第二次登场:声明 tick_ 易变,读写不许优化5。
更进一步:连「读 tick_」这个动作本身都要 cli/sti 夹住——
读到一半来中断,tick_ 被改,比较结果不可信;这是「异步环境(两边各按各的节奏来,时机不由你安排)中的同步处理」,
本书不展开但立了规矩:在中断处理程序之外读会被中断改写的变量,要防中断6。
3.3 优先级队列与超时通知
逻辑定时器 Timer{timeout_, value_};多个定时器用 std::priority_queue 管理:
不按入队顺序出队,而是「优先级最高者」先出。优先级自己定义:
超时越近越优先——operator< 写成 lhs.Timeout() > rhs.Timeout()
(优先队列用 less 比较,值「大」者优先,所以「超时近」要表现为「大」)7。
Tick() 里的超时处理因此极省:只看队头,没超时直接退出(后面的肯定也没超时);
超时了就发消息 {kTimerTimeout, {timeout, value}} 进 main_queue 再弹队头,循环8。
消息结构 Message 加了 arg 成员(type 不同用法不同,kTimerTimeout 用 arg.timer)9。
看门狗(watchdog):构造 TimerManager 时塞一个超时值为 unsigned long 最大值的定时器垫底—— 保证队列永不为空,队头操作永远不碰空队列10。
3.4 校准问题的提出与 RSDP 寻宝
到这里,tick 的单位仍是未知数,而且因机型差得离谱:作者的 QEMU 几秒就数满 100 次, 真机却要几十秒11。要换算成毫秒,得用一个频率已知的定时器来标定 Local APIC—— 选定 ACPI PM 定时器(电源管理标准 ACPI 配的时钟,几乎人人都有)12。
用它的第一步是找到它的寄存器端口,而端口地址记录在一张叫 FADT 的表里; FADT 的位置列在 XSDT 里;XSDT 的位置又列在 RSDP(Root System Description Pointer) 里 ——「名为指针,其实是个结构体,也躺在内存地址空间里,UEFI BIOS 知道它在哪」13。 问 UEFI 要的路:主函数第二参数 SystemTable → ConfigurationTable(配置表数组)→ 逐个比 VendorGuid 找 ACPI 2.0 的 GUID(gEfiAcpiTableGuid)→ 那个元素的 VendorTable 就是 RSDP 指针14。引导加载器把它作为新参数传给内核,内核侧做 IsValid() 校验 (签名、校验和),acpi::Initialize() 目前是「相当无用的函数」,但架子搭好了15。 整条表链 RSDP → XSDT → FADT → PM 定时器,是下一章的行程。
4. 作者的判断与证据
判断:逻辑定时器就该建在「一个硬件定时器 + 软件计数」上,而不是试图开多个硬件定时器。 硬件资源就一份,软件层把它复用成任意份——这是操作系统把稀缺硬件虚拟成丰富资源的标准动作。
判断:校准不能靠手调,要找官方时钟。 「最好使用已知工作频率的定时器来测量」—— QEMU 与真机几十倍的差异,让任何写死的 initial_count 都不可移植16。
证据与坦白: 定时器成功的证据是控制台周期打印;同时书中对并发问题保持诚实: 「这可能还不够……本书并不涉及完全防止数据冲突的方法」的同款口吻再次出现在 tick_ 读取处 (cli 只挡中断,挡不住未来多核)17。
5. 边界与局限
- tick 还不是毫秒:本章结束时的系统只有「多少次计数」,「1 毫秒 = 多少次计数」 要等第 12 章用 ACPI PM 定时器标定。
- 优先级队列是标准库的,依赖动态内存:内存管理器(第 8 章)和堆(第 9 章 sbrk)都得上岗, 缺一环都编译不过——本章也是「各章积木咬合度」的一次验收。
- 定时器中断里干了排序外的活(发消息),好在消息 Push 是 O(1);百万定时器的场景不在本书射程。
- 没有处理 tick 溢出:unsigned long 在可见未来数不完,教学系统不管。
6. 可带走的
本章主线例子,一行写完: Local APIC 周期中断 → tick_++(volatile)→ priority_queue 里 N 个逻辑定时器,队头超时发消息、重注册即周期定时器。
- 一个硬件定时器 + 软件 tick = 任意多个逻辑定时器——虚拟化的又一次微缩演示;
- 周期模式:减到 0 发中断并自动重装;EOI 不能忘,否则中断失声;
- 中断改、主循环读的变量必须 volatile,读它的临界区用 cli/sti 夹住;
- 超时处理用优先级队列(超时近者优先),队头没超时后面就不用看;
- 看门狗定时器(最大值垫底)让队头操作永不见空队列——小工程里很常用的一招;
- 消息结构 = type + arg 联合体,为多种通知预留形状;
- 校准的思路:拿频率已知的时钟当尺子,量出本地时钟的周期;
- ACPI 表链一层套一层(RSDP→XSDT→FADT),入口从 UEFI 配置表按 GUID 领。
7. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| main.cpp 重构 | 11.1 整理源码(osbook_day11a) | text/79-ch11-01-11-1-osbook-day11a.txt:4(搜「乱糟糟」) · text/79-ch11-01-11-1-osbook-day11a.txt:11(搜「标准库中的队列」) |
| 周期模式与定时器中断 | 11.2 定时器中断(osbook_day11b) | text/80-ch11-02-11-2-osbook-day11b.txt:9(搜「循环模式」) · text/80-ch11-02-11-2-osbook-day11b.txt:17(搜「Current Count」) |
| 中断向量取值范围 | 11.2 定时器中断(osbook_day11b) | text/80-ch11-02-11-2-osbook-day11b.txt:25(搜「0x21和0xff」) |
| EOI 提醒 | 11.2 定时器中断(osbook_day11b) | text/80-ch11-02-11-2-osbook-day11b.txt:42(搜「NotifyEndOfInterrupt」) |
| 需要 1ms 分辨率 | 11.3 精确定时(osbook_day11c) | text/81-ch11-03-11-3-osbook-day11c.txt:31(搜「数秒」;日文残页 :31 搜「1 ミリ秒」) |
| TimerManager 与 volatile | 11.3 精确定时(osbook_day11c) | text/81-ch11-03-11-3-osbook-day11c.txt:50(搜「TimerManager类」) · text/81-ch11-03-11-3-osbook-day11c.txt:55(搜「volatile修饰符」) |
| cli/sti 读 tick_ | 11.3 精确定时(osbook_day11c) | text/81-ch11-03-11-3-osbook-day11c.txt:87(搜「cli和sti」;:92,搜「异步环境中的同步处理」) |
| 死循环专栏 | 11.3 精确定时(osbook_day11c) | text/82-ch11-04-11-4-osbook-day11d.txt:5(搜「死循环」) |
| 优先级队列与 less | 11.4 多个定时器和超时通知(osbook_day11d) | text/82-ch11-04-11-4-osbook-day11d.txt:33(搜「优先级队列」) · text/82-ch11-04-11-4-osbook-day11d.txt:46(搜「超时越长,优先级越低」) |
| Tick 超时处理 | 11.4 多个定时器和超时通知(osbook_day11d) | text/82-ch11-04-11-4-osbook-day11d.txt:79(搜「优先级最高的定时器」) |
| 看门狗 | 11.4 多个定时器和超时通知(osbook_day11d) | text/82-ch11-04-11-4-osbook-day11d.txt:60(搜「看门狗」) |
| 两定时器演示 | 11.4 多个定时器和超时通知(osbook_day11d) | text/82-ch11-04-11-4-osbook-day11d.txt:95(搜「它们分别在200次和」) · text/83-ch11-05-11-5-acpi-pm-rsdp-osbook-day11e.txt:8(搜「100次循环」) |
| QEMU 与真机的周期差 | 11.5 ACPI PM定时器和RSDP(osbook_day11e) | text/83-ch11-05-11-5-acpi-pm-rsdp-osbook-day11e.txt:28(搜「几秒」) |
| ACPI PM 定时器选型 | 11.5 ACPI PM定时器和RSDP(osbook_day11e) | text/83-ch11-05-11-5-acpi-pm-rsdp-osbook-day11e.txt:37(搜「Advanced Configuration and Power Interface」) |
| RSDP→XSDT→FADT 表链 | 11.5 ACPI PM定时器和RSDP(osbook_day11e) | text/83-ch11-05-11-5-acpi-pm-rsdp-osbook-day11e.txt:43(搜「FADT」) · text/83-ch11-05-11-5-acpi-pm-rsdp-osbook-day11e.txt:44(搜「RSDP」) |
| 配置表找 GUID | 11.5 ACPI PM定时器和RSDP(osbook_day11e) | text/83-ch11-05-11-5-acpi-pm-rsdp-osbook-day11e.txt:68(搜「VendorGuid」) · text/83-ch11-05-11-5-acpi-pm-rsdp-osbook-day11e.txt:81(搜「gEfiAcpiTableGuid」) |
Footnotes
-
出处:「11.1 整理源码(osbook_day11a)」(
text/79-ch11-01-11-1-osbook-day11a.txt:4(搜「乱糟糟」);text/79-ch11-01-11-1-osbook-day11a.txt:11(搜「标准库中的队列」))。 ↩ -
出处:「11.2 定时器中断(osbook_day11b)」(
text/80-ch11-02-11-2-osbook-day11b.txt:9(搜「循环模式」);text/80-ch11-02-11-2-osbook-day11b.txt:17(搜「Current Count」))。 ↩ -
出处:「11.2 定时器中断(osbook_day11b)」(
text/80-ch11-02-11-2-osbook-day11b.txt:25(搜「0x21和0xff」))。 ↩ -
出处:「11.2 定时器中断(osbook_day11b)」(
text/80-ch11-02-11-2-osbook-day11b.txt:42(搜「NotifyEndOfInterrupt」))。 ↩ -
出处:「11.3 精确定时(osbook_day11c)」()。 ↩
-
出处:「11.3 精确定时(osbook_day11c)」(
text/81-ch11-03-11-3-osbook-day11c.txt:50(搜「TimerManager类」);text/81-ch11-03-11-3-osbook-day11c.txt:55(搜「volatile修饰符」))。 ↩ -
出处:「11.3 精确定时(osbook_day11c)」()。 ↩
-
出处:「11.3 精确定时(osbook_day11c)」(
text/82-ch11-04-11-4-osbook-day11d.txt:5(搜「死循环」))。 ↩ -
出处:「11.4 多个定时器和超时通知(osbook_day11d)」(
text/82-ch11-04-11-4-osbook-day11d.txt:33(搜「优先级队列」);text/82-ch11-04-11-4-osbook-day11d.txt:46(搜「超时越长,优先级越低」))。 ↩ -
出处:「11.4 多个定时器和超时通知(osbook_day11d)」(
text/82-ch11-04-11-4-osbook-day11d.txt:79(搜「优先级最高的定时器」))。 ↩ -
出处:「11.4 多个定时器和超时通知(osbook_day11d)」(
text/82-ch11-04-11-4-osbook-day11d.txt:60(搜「看门狗」))。 ↩ -
出处:「11.4 多个定时器和超时通知(osbook_day11d)」(
text/82-ch11-04-11-4-osbook-day11d.txt:95(搜「它们分别在200次和」);text/83-ch11-05-11-5-acpi-pm-rsdp-osbook-day11e.txt:8(搜「100次循环」))。 ↩ -
出处:「11.5 ACPI PM定时器和RSDP(osbook_day11e)」(
text/83-ch11-05-11-5-acpi-pm-rsdp-osbook-day11e.txt:28(搜「几秒」))。 ↩ -
出处:「11.5 ACPI PM定时器和RSDP(osbook_day11e)」(
text/83-ch11-05-11-5-acpi-pm-rsdp-osbook-day11e.txt:37(搜「Advanced Configuration and Power Interface」))。 ↩ -
出处:「11.5 ACPI PM定时器和RSDP(osbook_day11e)」(
text/83-ch11-05-11-5-acpi-pm-rsdp-osbook-day11e.txt:43(搜「FADT」);text/83-ch11-05-11-5-acpi-pm-rsdp-osbook-day11e.txt:44(搜「RSDP」))。 ↩ -
出处:「11.5 ACPI PM定时器和RSDP(osbook_day11e)」(
text/83-ch11-05-11-5-acpi-pm-rsdp-osbook-day11e.txt:68(搜「VendorGuid」);text/83-ch11-05-11-5-acpi-pm-rsdp-osbook-day11e.txt:81(搜「gEfiAcpiTableGuid」))。 ↩ -
出处:「11.4 多个定时器和超时通知(osbook_day11d)」(
text/82-ch11-04-11-4-osbook-day11d.txt:95(搜「它们分别在200次和」);text/83-ch11-05-11-5-acpi-pm-rsdp-osbook-day11e.txt:8(搜「100次循环」))。 ↩