操作系统实现(三):中断
上一篇提到当访问的页表和页不在内存中时会触发 Page Fault 异常,操作系统需要在异常处理函数中分配内存页并设置好相应的分页表项。异常是一种中断类型,注册异常处理函数就是注册中断处理函数,中断处理函数注册在一个叫 IDT(Interrupt Descriptor Table) 的地方。
IDT
中断处理函数在实模式下注册在 IVT(Interrput Vector Table) 中,在保护模式下注册在 IDT(Interrupt Descriptor Table) 。IDT是包含 256 项的表,表项的结构如下:
struct idt_entry
{
uint16_t offset_0;
uint16_t selector;
uint8_t zero;
uint8_t type_attr;
uint16_t offset_1;
};
其中 selector 是 GDT 的代码段选择器,offerset_0 和 offset_1 分别表示中断处理函数 offset 地址的 0~15bits 和 16~31bits ,type_attr 的结构如下:
7 0
+---+---+---+---+---+---+---+---+
| P | DPL | S | GateType |
+---+---+---+---+---+---+---+---+
P表示是否存在,DPL 表示描述符的最低调用权限,GateType 定义了中断类型,32 位的中断类型分别是:
Interrupt Gate 和 Trap Gate 相似,区别在前者执行中断处理函数前后会自动关闭和开启中断。
准备好 IDT ,设置好 IDTR 寄存器就把 IDT 都设置好了。IDTR 寄存器结构如下:
struct idtr
{
uint16_t limit;
struct idt_entry *base;
};
limit 是整个表的大小 -1 字节,base 指向 IDT 表,设置 IDTR 寄存器的指令是 lidt。
异常和硬件中断
了解 IDT 的结构了之后,我们可以设置异常和硬件中断的 ISR(Interrupt Service Routine)。对于异常,我们只要知道有哪些异常会触发,触发的逻辑是什么样,实现合适的异常处理函数即可(这里是异常列表)。对于硬件中断,需要通过一个硬件完成—— PIC(Programmable Interrupt Controller)。
PIC 分为 Master 和 Slave ,每个 PIC 都有一个命令端口和一个数据端口,通过这两个端口可以读写 PIC 的寄存器。每个 PIC 都可连 8 个输入设备,x86下 Slave 需要通过 line 2 连接到 Master 上才能响应输入设备,连接的输入设备有中断请求的时候会产生 IRQ(Interrupt Request),Master 产生 IRQ 0 ~ IRQ 7,Slave 产生 IRQ 8 ~ IRQ 15。保护模式下可以设定 PIC 产生的中断对应的 ISR 所在 IDT 中的 offset,通常设置为从 0x20 开始,到 0x2F 结束(0x0 到 0x1F 被异常占用)。
PIC 的端口号如下表:
PIC | IO Port |
---|---|
Master Command | 0x20 |
Master Data | 0x21 |
Slave Command | 0xA0 |
Slave Data | 0xA1 |
PIC 产生的标准 IRQ 如下表:
IRQ | Description |
---|---|
0 | Programmable Interrupt Timer Interrupt |
1 | Keyboard Interrupt |
2 | Cascade (used internally by the two PICs. never raised) |
3 | COM2 (if enabled) |
4 | COM1 (if enabled) |
5 | LPT2 (if enabled) |
6 | Floppy Disk |
7 | LPT1 / Unreliable "spurious" interrupt (usually) |
8 | CMOS real-time clock (if enabled) |
9 | Free for peripherals / legacy SCSI / NIC |
10 | Free for peripherals / SCSI / NIC |
11 | Free for peripherals / SCSI / NIC |
12 | PS2 Mouse |
13 | FPU / Coprocessor / Inter-processor |
14 | Primary ATA Hard Disk |
15 | Secondary ATA Hard Disk |
PIC 初始化的时候,要设置 Master 和 Slave 通过 line 2 相连,同时设置好 IRQ 对应的 ISR 在 IDT 中的起始中断号。PIC 提供一个 IMR(Interrupt Mask Register) 寄存器来标识中断是否屏蔽,设置 bit 位会屏蔽对应的 IRQ。当 IMR 未设置,并且 CPU 的中断打开,如果有设备中断请求发生,那么 ISR 将会执行。ISR 执行完毕之后要通知 PIC 中断处理完成,需要向 PIC 的命令端口写入一个 EOI(End Of Interrupt) 命令(0x20),中断请求如果来自 Slave,那么需要先往 Slave 命令端口写入 EOI,再向 Master 命令端口写入 EOI。
Spurious IRQs
由于 CPU 与 PIC 之间的竞争条件可能会产生 IRQ 7(Master 产生) 和 IRQ 15(Slave 产生) 的 Spurious IRQs。为了处理这种情况,我们要知道什么时候是无效的 IRQ,通过判断 IRR(Interrupt Request Register) 寄存器的值可以获知哪些 IRQ 发生了,这个寄存器的每个 bit 表示相应的 IRQ 是否发生。在 IRQ 7 和 IRQ 15 的 ISR 中先读取 IRR,然后判断对应的 bit 位是否被设置,如果没有设置,那么表示当前是一个 Spurious IRQ,不需要处理,也不需要写入 EOI,直接返回即可(如果是 Slave PIC 产生的,需要往 Master PIC 写入 EOI,由于 Master 不知道 Slave 产生的 IRQ 是不是 Spurious 的)。
PIT
现代操作系统都有抢占式多任务能力,通常是通过设置一个硬件 Timer,一个进程的执行时间到了之后切换成另一个进程执行,这个硬件 Timer 是 PIT(Programmable Interval Timer)。PIT 有多个 channel 和多种工作 mode,其中 channel 0 连接到 PIC 会产生 IRQ 0,mode 2 和 mode 3 是常用的工作模式。操作系统初始化的时候设置好 PIT,同时设置好 PIT 产生的 IRQ 0 的 ISR,在这个 ISR 中操作系统就可以执行多任务的调度。
中断处理结束
IDT 中设置的 ISR 返回时不能使用普通的函数返回指令 ret,需要使用一条特殊的返回指令 iret。在了解了这些之后,我们有了响应外部设备的能力,可以接入外部输入设备了,下一步接入键盘。