3、硬件定时器:PIT、HPET、LAPIC定时器、TSC

好,咱们进入硬件定时器的世界。说实话,这部分内容在微内核里特别关键。你想想看,没有定时器,操作系统就像个没有闹钟的人——不知道什么时候该起床,什么时候该干活。

我个人习惯把硬件定时器分成两类:一类是“老黄牛”,稳定可靠但精度一般;另一类是“精密仪器”,速度快但配置复杂。咱们今天要聊的四个定时器,正好覆盖了这两个阵营。

3.1 PIT(可编程间隔定时器)—— 老当益壮

PIT 是 x86 平台上最古老的定时器之一。我记得刚入行时,第一份工作就是在一个老旧的嵌入式板子上调 PIT。那时候觉得这东西真简单,后来才发现,简单也有简单的妙处。

PIT 的核心是 8253/8254 芯片。它有三个独立的计数器通道,每个都是 16 位的递减计数器。时钟源频率是 1.193182 MHz,这个数字很奇怪对吧?其实是因为它来自系统时钟的 1/3 分频。

咱们看看它的工作模式:

模式 名称 典型用途
模式 0 中断终止计数 一次性延时
模式 2 速率发生器 周期性中断(最常用)
模式 3 方波发生器 扬声器音调

在微内核里,PIT 通常用来做系统 tick。比如设置模式 2,初始值设为 11932,这样就能产生 100Hz 的中断。嗯,这里要注意:PIT 的精度有限,100Hz 的 tick 对于现代系统来说太粗糙了。

关键点:PIT 的计数器是递减的,计数到 0 时触发中断。每次中断后,计数器会自动重载初始值。

3.2 HPET(高精度事件定时器)—— 现代替代者

HPET 是 Intel 和 Microsoft 联合推出的,目的就是替代 PIT 和 RTC 定时器。它的精度比 PIT 高得多,主频通常在 10MHz 以上。

我曾经在一个项目中,需要做微秒级的定时。PIT 根本做不到,HPET 就派上了用场。它的结构是这样的:

  • 一个 64 位的主计数器(递增)
  • 最多 32 个比较器(timer)
  • 每个比较器可以配置为周期性或一次性

HPET 的配置稍微复杂些。你得先通过 ACPI 表找到它的基地址,然后操作 MMIO 寄存器。我建议你先把主计数器使能,再配置比较器。

// HPET 寄存器操作示例
volatile uint64_t *hpet_base = (uint64_t *)0xFED00000;

// 使能主计数器
hpet_base[0x10 / 8] |= 0x1;

// 配置 Timer 0:周期性,中断使能
hpet_base[0x100 / 8] = (1 << 3) | (1 << 2) | (1 << 1);

// 设置比较值
hpet_base[0x108 / 8] = current_value + interval;

避坑指南:我曾经在 HPET 上踩过一个坑——忘记设置比较器的 32 位模式标志。结果 64 位比较器在 32 位系统上工作异常,中断频率完全不对。排查了整整两天才找到原因。

3.3 LAPIC 定时器 —— 每个 CPU 自己的闹钟

LAPIC(本地高级可编程中断控制器)定时器是每个 CPU 核心自带的。它的好处很明显:不需要外部硬件,而且每个核心可以独立配置。

说白了,LAPIC 定时器就是给多核系统准备的。你想想看,如果所有核心都用同一个 PIT,那调度起来多麻烦。LAPIC 定时器让每个核心都有自己的 tick。

它的工作方式比较特别:

  • 基于总线频率或 TSC 分频
  • 计数器是 32 位的递减计数器
  • 支持一次性模式和周期性模式

配置 LAPIC 定时器需要操作 LAPIC 的 MMIO 寄存器:

// LAPIC 定时器初始化
void lapic_timer_init(uint32_t ticks) {
    // 设置分频系数为 1
    lapic_write(LAPIC_TIMER_DIV, 0x0B);
    
    // 设置初始计数
    lapic_write(LAPIC_TIMER_INIT, ticks);
    
    // 配置为周期性模式,使能中断
    lapic_write(LAPIC_TIMER_LVT, 0x20032);
}

个人经验:LAPIC 定时器的频率在不同 CPU 上可能不一样。我建议你在初始化时先测量一下实际频率,而不是硬编码。方法很简单:用 PIT 或 HPET 作为参考,测量 LAPIC 定时器在一秒内的计数变化。

3.4 TSC(时间戳计数器)—— 最快的计时器

TSC 是 CPU 内部的一个 64 位计数器,每个时钟周期递增一次。它的速度极快,读取开销只有几十个纳秒。在微内核里,TSC 常用来做高精度的时间测量。

不过,TSC 有个历史问题:早期的 TSC 频率会随 CPU 频率变化(比如 SpeedStep 技术)。后来 Intel 引入了 恒定 TSC(Invariant TSC),保证频率不变。再后来,又有了 始终开启的 TSC(Always Running TSC),即使 CPU 进入休眠状态也会继续计数。

读取 TSC 的指令是 RDTSC:

static inline uint64_t rdtsc(void) {
    uint32_t lo, hi;
    __asm__ volatile ("rdtsc" : "=a"(lo), "=d"(hi));
    return ((uint64_t)hi << 32) | lo;
}

你可能会问:TSC 能直接当定时器用吗?答案是:可以,但需要配合中断。TSC 本身不会产生中断,你得用 LAPIC 定时器或者 HPET 来触发中断,然后在中断处理函数里读取 TSC 的值。

实用技巧:我个人习惯用 TSC 做性能分析。比如测量某个系统调用的耗时,在调用前后各读一次 TSC,差值就是时钟周期数。再除以 CPU 频率,就能得到纳秒级的精度。

3.5 四种定时器的对比

咱们用一张表总结一下:

定时器 精度 中断能力 多核支持 典型用途
PIT ~1ms 有 共享 系统 tick、兼容性
HPET ~100ns 有 共享 高精度定时、替代 PIT
LAPIC 定时器 ~1μs 有 每核独立 进程调度、时间片
TSC ~1ns 无 每核独立 性能测量、时间戳

在微内核里,我通常这样搭配使用:

  • 用 LAPIC 定时器做进程调度的时间片
  • 用 TSC 做高精度的时间测量
  • HPET 作为备用,处理一些需要微秒级精度的定时任务
  • PIT 基本只用在启动初期,或者作为兼容性兜底

好了,硬件定时器这块就聊到这儿。下一节咱们看看时钟中断是怎么产生的,以及微内核怎么处理这些中断。嗯,这部分内容会更有意思。