导引:点亮什么、为什么、设计图
这一章我们要点亮什么
落到一个看得见的效果上:big kernel 跑起来后,串口开始每秒稳定吐一行——
[TICK] uptime: 1s
[TICK] uptime: 2s
[TICK] uptime: 3s不是 kprintf 死循环打出来的,而是真正的硬件中断:PIT 每秒触发 100 次 IRQ0,第 100、200、300……次各打一行。内核的执行流被一个外部信号反复「打断」又「接住」,而它全程没崩。这对一个裸奔内核来说,是从「只能被自己主动 int」跨到「能响应外部世界」的质变。
顺带我们还要确认一件事:上一章的异常网没坏。所以在开中断之前,main 会再 int $3 一次,证明 IDT、PIC、IRQ 这套新东西装上去之后,老的异常处理照样能正常返回。
为什么现在需要它
010 的内核是「聋」的。它听得见 CPU 自己抛的异常(#BP、#PF 这类),但听不见任何硬件——没有时钟、没有键盘、没有串口输入。更要命的是,它主动拒绝听见:main 末尾的 idle loop 是 cli; hlt,把中断整个关死,然后停机。这是一种诚实的自保,因为那时候哪怕只开一条 IRQ,都会立刻 Double Fault。
为什么一开就崩?因为 x86 的硬件中断向量默认会撞进 CPU 的异常区。8259 PIC 上电默认把 IRQ0-15 映射到 INT 8-15(0x08-0x0F),而 8-15 这段正好是 #DF、#TS、#GP、#PF 这些异常的家。PIT 一旦开始嘀嗒,IRQ0 就被当成 INT 0x08 推给 CPU,CPU 一看「这是 Double Fault 的向量」,可又没有真正的 #DF 语义去匹配,直接三重故障重启。
所以这章的真正主题不是「加个定时器」,而是「先把 16 条硬件中断从异常区里搬出来」。这个搬迁动作叫 PIC remap,是整章的地基。搬完之后,IRQ0-15 落到 0x20-0x2F 这段没人用的向量上,再去配 PIT、开中断,才安全。
设计图
先看硬件长什么样。PC 上是两片 8259A 级联,主片接 CPU,从片通过主片的 IRQ2 接上来,一共 15 条可用 IRQ 线(IRQ2 被级联占了):
┌──────────────────────┐
CPU INTR ◄────┤ Master PIC │ IRQ0 PIT 计时器
│ cmd 0x20 / data 0x21│ IRQ1 键盘
│ │ IRQ2 ──┐ 级联线
│ │ IRQ3-7 │
└───────────────────────┘ │
▼
┌──────────────────────┐
│ Slave PIC │ IRQ8 RTC
│ cmd 0xA0/data 0xA1 │ IRQ9-15
└──────────────────────┘remap 之后,IRQ 号和 CPU 看到的中断向量号不再是同一个东西。我们要让硬件 IRQ 经过 PIC 翻译,落到 0x20 起的这一段:
硬件 IRQ PIC 翻译后 CPU 看到的 INT 向量
───────── ────────────────────────────────
IRQ0 (PIT) → INT 0x20 ┐
IRQ1 (键盘) → INT 0x21 │ Master 片
IRQ2 (级联) → INT 0x22 │ IRQ0-7 → 0x20-0x27
... ┘
IRQ8 (RTC) → INT 0x28 ┐
... │ Slave 片
IRQ15 → INT 0x2F ┘ IRQ8-15 → 0x28-0x2F一次 IRQ0 从触发到返回的完整链路,是这章所有代码要串起来的东西:
PIT 计数到 0 ──► 拉 IRQ0
│
Master PIC ──► 向 CPU 发 INT 0x20
│
CPU 压栈(SS/RSP/RFLAGS/CS/RIP),跳 IDT[0x20]
│
irq0_stub(汇编):保存通用寄存器,传 frame,call pit_irq0_handler
│
PIT::irq0_handler:tick++,每秒打一行,PIC::send_eoi(0)
│
stub 恢复寄存器 ──► iretq ──► 回到被打断处注意链路最后那段 send_eoi(0)——它是这条链能不能持续运转的关键,下面专门讲。