键盘按下一个键,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(下一条指令地址)、cs、rflags、rsp、ss。处理完之后用 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秒
内核有了心跳。
这一章之后
有了时钟中断,后面可以做进程调度——每隔一段时间打断当前进程,切换到另一个进程运行。这是多任务操作系统的核心机制。
但在此之前,下一章先解决内存问题:内核怎么管理和分配内存。