键盘按下一个键,CPU 是怎么知道的?

不是轮询——CPU 不会没事一直问"键盘有没有按键"。而是靠中断(Interrupt,硬件或软件触发的信号,让 CPU 暂停当前任务去处理紧急事件)。发生了什么事,硬件主动通知 CPU,CPU 暂停手头的事,跳去处理,处理完再回来。

这一章要让内核有能力响应中断。没有这个机制,内核什么都干不了。


中断的分类

x86 的 256 个中断号分三段:

范围 类型 例子
0~31 CPU 异常 除零、缺页、非法指令
32~47 硬件中断 时钟、键盘
48~255 软件中断 系统调用

CPU 异常是 CPU 自己触发的,比如访问了不该访问的内存,CPU 就抛一个缺页异常(Page Fault,访问未映射的虚拟地址时 CPU 触发的异常,操作系统据此做内存按需分配)。

硬件中断是外设触发的,经过 PIC(Programmable Interrupt Controller,可编程中断控制器,负责管理外设中断请求并转发给 CPU)发给 CPU。


IDT:中断的路由表

要处理中断,首先要告诉 CPU:每种中断发生时,去哪个函数处理?

这张路由表叫 IDT(Interrupt Descriptor Table,中断描述符表,256 项,每项对应一个中断号及其处理函数地址)。跟之前的 GDT 一样,是内存里的一张表,用 lidt 指令告诉 CPU 位置。

每个表项里存的是处理函数的地址,还有权限信息。CPU 一旦收到中断,就查这张表,跳到对应函数去执行。


中断处理的细节

CPU 进入中断处理函数之前,会自动把当前的执行状态压栈:rip(下一条指令地址)、csrflagsrspss。处理完之后用 iretq(64 位中断返回指令)恢复状态,接着跑。

有一个坑:某些异常 CPU 会自动压入错误码(比如缺页异常),某些不会(比如除零)。为了让处理函数收到的栈结构一致,对没有错误码的异常,要手动压一个 0 占位。这件事用汇编宏做,每种异常一行,干净利落。


第一个测试:故意除以零

IDT 装好之后,最简单的验证方式是故意触发一个异常,看内核能不能捕获到:

int a = 1, b = 0;
__asm__ volatile ("div %1" : : "a"(a), "r"(b));

运行之后输出:

[EXCEPTION] Division By Zero (int=0x00 rip=0x0000000000010245)

CPU 触发除零异常,内核捕获到了,打印出中断号和出错的指令地址,然后挂起。没有崩溃,没有乱跑——内核有了处理异常的能力。


接入硬件中断:PIC

CPU 异常是 CPU 内部的事,硬件中断是外设的事,需要 PIC 参与。

x86 有两块级联的 8259A PIC 芯片,默认把 IRQ0~15 映射到中断号 0~15——和 CPU 异常的号码冲突了。所以必须重映射:

主 PIC IRQ0~7  → 中断 32~39
从 PIC IRQ8~15 → 中断 40~47

初始化顺序有讲究,不能乱:

1. idt_init()    先建好中断表
2. pic_init()    重映射 PIC
3. pit_init()    设置时钟频率(100Hz)
4. sti           最后才开中断

最后这一步很重要——在一切准备好之前不能开中断,否则中断进来找不到处理函数,直接三重错误重启。


时钟中断:每秒 100 次

PIT(Programmable Interval Timer,可编程间隔定时器,经典的 x86 定时器芯片,基础频率约 1.19MHz)设好分频值,每秒触发 100 次 IRQ0 时钟中断。

每次时钟中断处理完,有一件事必须做:向 PIC 发 EOI(End of Interrupt,中断结束信号,告诉 PIC 这次中断已处理完,可以继续发下一个)。漏掉这个,PIC 认为中断还没处理完,后续所有中断都会被屏蔽,时钟停止,键盘失效。

跑起来之后,串口每秒输出一行:

tick 0x0000000000000064   ← 第100次,1秒
tick 0x00000000000000c8   ← 第200次,2秒
tick 0x000000000000012c   ← 第300次,3秒

内核有了心跳。


这一章之后

有了时钟中断,后面可以做进程调度——每隔一段时间打断当前进程,切换到另一个进程运行。这是多任务操作系统的核心机制。

但在此之前,下一章先解决内存问题:内核怎么管理和分配内存。

源码在这里:github.com/tongpengfei/learn-with-ai