0x00
操作系统是由中断驱动的
0x01 何为中断
整个操作系统的主体可以看做一个死循环,可以粗陋地看作如下形式
1 2 3 4
| while(1) { wait_and_handle_interrupt(); }
|
所谓中断就是告诉 CPU 某件事发生了,你处理一下。那么来自 CPU 外部就是外部中断:外部中断的来源是计算机的其他硬件,所以又称为硬件中断(比如按下键盘)。来自 CPU 内部就是内部中断:可分为软中断和异常。要么来自软件(比如系统调用),要么就是 CPU 出错了(例如除0错误)。
0x02 处理中断
那么操作系统如何处理中断?
0x021 IDT
要处理它首先得让操作系统知道什么是中断:定义一个 IDT (Interrupt Descriptor Table:中断描述符表)。CPU 使用一个专门的寄存器 IDTR 存放内存中 IDT 的位置。这意味着 IDT 需要操作系统自行准备好放在内核内存里。

可使用特权指令 lidt 和 sidt 来读写这个寄存器。
什么又是中断描述符?如下所示

1 2 3 4 5 6 7 8
| struct IntrDesc{ uint16_t func_offset_low_word; uint16_t selector; uint8_t zero; uint8_t attribute; uint16_t func_offset_high_word; };
|
此前提到过四中”门”,所谓”门”就是类似这样的程序入口。中断描述符也就是中断门描述符。通过图中的 type 字段确定是那种门(调用门,任务门,陷阱门或是中断门)
一目了然。有了这个结构操作系统就知道应该到哪里去处理一个中断。对于一个中断发生时,根据其对应的中断向量号(每个中断源对应一个中断向量号,粗浅理解为在 IDT 中的索引)找到对应的中断描述符,然后找到其在内核中的处理程序即可。
0x022 处理过程
软件部分:
由于中断描述符是 8 个字节, 所以处理器用中断向量号乘以 8 后,再与 IDTR 中的中断描述符表地址相加,所求的地址之和便是该中断 向量号对应的中断描述符。
如果是用户程序发起的中断,则先检查用户 CPL 是否小于等于中断描述符 DPL (在其13-14比特位,处于定义的 attribute 字段中,注意不是 selector 的 RPL),即用户特权级是否满足这个中断描述符所需。
检查中断描述符的 selector 中的 RPL 是否小于等于当前 cpu 的 CPL。也就是要求中断只能提升特权级。如果是硬件中断或者异常,则不需要第一个检查,直接检查这个。
将中断描述符中的代码段选择子加载到 cs 寄存器,将中断描述符中的中断处理程序偏移地址加载到 eip。跳转到终端处理程序。
这个过程需要压栈旧的 cs、eip 等上下文以便于恢复。注意如果中断提升了特权级需要使用不同的特权栈,那么就还要在新的栈中保存旧的栈地址。如下图。

中断处理程序结束后使用特权指令 iret 返回。这个指令作用是从当前栈顶处依次弹出 32 位数据分别到寄存器 EIP、CS、EFLAGS,如果发生了特权级提升,返回时还会弹出 esp 和 ss。
还值得一提的是有些中断还会压栈中断错误码,但是iret不会弹出中断错误码。所以在中断处理程序中需要手动用栈指针跨过错 误码或将其弹出。
整个处理过程如下所示

硬件部分
对于硬件部分的过程不多赘述,一来这是记录操作系统软件实现,二来参考书中的硬件也有些过时了。但还是贴一个流程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| 设备产生中断请求(IRQ) ↓ 8259A 接收到 IRQ 信号 ↓ 是否被屏蔽?(IMR寄存器) ↓ ┌────是────→ 忽略中断 ❌ ↓ 否 ↓ 是否优先级最高?(IRR + ISR 判断) ↓ ┌────否────→ 等待(排队)⏳ ↓ 是 ↓ 8259A 向 CPU 发送 INTR 信号 ↓ CPU 响应(发出 INTA) ↓ 8259A 返回中断向量号 ↓ CPU: 查 IDT → 跳转中断处理程序 ↓ 执行中断处理程序 ↓ 发送 EOI(End Of Interrupt) ↓ 8259A 清除 ISR 标志 ↓ 允许下一个中断进入
|
0x03 实现
kernel.S 提供内核处理各个中断的入口,也就是中断描述符中的中断处理程序,主要是分发路由功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| [bits 32] %define ERROR_CODE nop ; 若在相关的异常中CPU已经自动压入了错误码,这里不做操作 %define ZERO push 0 ; 定义默认的压入错误码操作
extern put_str extern intrHandler_table
section .data
global intr_entry_table intr_entry_table: %macro VECTOR 2 ;这里定义一个多行宏作为 intr_handler,后面的2是指传递两个参数,里面的%1,%2代表参数 section .text intr%1entry: ; ============= 默认压入错误码或者空操作,保证最后都会需要跨过一个4字节来进行iret %2 ; ============= 保存上下文 push ds push es push fs push gs pushad ;压入通用寄存器 ; ============= 发送中断结束信号 mov al, 0x20 ; 中断结束命令EOI,这里R为0,SL为0,EOI为1 mov dx, 0xa0 ; 中断控制器从片端口 out dx, al ; 向从片发送 mov dx, 0x20 ; 中断控制器主片端口 out dx, al ; 向主片发送 ; ============= 压入中断向量号并调用中断处理函数 push %1 call [intrHandler_table + %1*4] jmp intr_exit ; ============= intr_entry_table数组(编译后会将相同的section合并成一个段) section .data dd intr%1entry ;存储各个中断入口程序的地址, %endmacro ;多行宏结束标志
section .text global intr_exit intr_exit: ; =========== 恢复上下文 add esp, 4 ; 跳过中断号 popad pop gs pop fs pop es pop ds add esp, 4 ; 跳过error_code ; =========== 返回 iretd
VECTOR 0x00,ZERO ... VECTOR 0x1e,ERROR_CODE VECTOR 0x1f,ZERO VECTOR 0x20,ZERO
|
interrupt.c 主要是准备好 IDT 和中断处理程序的实际实现(暂时只处理时钟中断以测试)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
| #include "interrupt.h" #include "global.h" #include "stdint.h" #include "io.h"
#define IDT_DESC_CNT 0x21 #define PIC_M_CTRL 0x20 #define PIC_M_DATA 0x21 #define PIC_S_CTRL 0xA0 #define PIC_S_DATA 0xA1
struct IntrDesc{ uint16_t func_offset_low_word; uint16_t selector; uint8_t zero; uint8_t attribute; uint16_t func_offset_high_word; };
static struct IntrDesc IDT[IDT_DESC_CNT];
char* intr_name[IDT_DESC_CNT]; intrHandler_ptr intrHandler_table[IDT_DESC_CNT];
extern intrHandler_ptr intr_entry_table[IDT_DESC_CNT];
static void initPIC(void) { outb(PIC_M_CTRL, 0x11); outb(PIC_M_DATA, 0x20); outb(PIC_M_DATA, 0x04); outb(PIC_M_DATA, 0x01);
outb(PIC_S_CTRL, 0x11); outb(PIC_S_DATA, 0x28); outb(PIC_S_DATA, 0x02); outb(PIC_S_DATA, 0x01);
outb(PIC_M_DATA, 0xfe); outb(PIC_S_DATA, 0xff);
put_str("pic init done!\n"); }
static void makeIntrDesc(struct IntrDesc* p_gdesc, uint8_t attr, intrHandler_ptr function) { p_gdesc->func_offset_low_word = (uint32_t)function & 0x0000FFFF; p_gdesc->selector = SELECTOR_K_CODE; p_gdesc->zero = 0; p_gdesc->attribute = attr; p_gdesc->func_offset_high_word = ((uint32_t)function & 0xFFFF0000) >> 16; }
static void initIDT(void) { for(int i = 0; i < IDT_DESC_CNT; i++) { makeIntrDesc(&IDT[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]); } put_str("init IDT done\n"); }
static void intrHandler_default(uint64_t vec_nr) { if(vec_nr == 0x27 || vec_nr == 0x2f) return; put_str("int vector : 0x"); put_uint(vec_nr); put_char('\n'); }
static void initIntrHandlerTable(void) { for(int i = 0; i < IDT_DESC_CNT; i++) { intrHandler_table[i] = intrHandler_default; intr_name[i] = "unknown"; } intr_name[0] = "#DE Divide Error"; intr_name[1] = "#DB Debug Exception"; intr_name[2] = "NMI Interrupt"; intr_name[3] = "#BP Breakpoint Exception"; intr_name[4] = "#OF Overflow Exception"; intr_name[5] = "#BR BOUND Range Exceeded Exception"; intr_name[6] = "#UD Invalid Opcode Exception"; intr_name[7] = "#NM Device Not Available Exception"; intr_name[8] = "#DF Double Fault Exception"; intr_name[9] = "Coprocessor Segment Overrun"; intr_name[10] = "#TS Invalid TSS Exception"; intr_name[11] = "#NP Segment Not Present"; intr_name[12] = "#SS Stack Fault Exception"; intr_name[13] = "#GP General Protection Exception"; intr_name[14] = "#PF Page-Fault Exception"; intr_name[16] = "#MF x87 FPU Floating-Point Error"; intr_name[17] = "#AC Alignment Check Exception"; intr_name[18] = "#MC Machine-Check Exception"; intr_name[19] = "#XF SIMD Floating-Point Exception"; }
void initInterrupt(void) { put_str("initInterrupt start\n"); initIDT(); initIntrHandlerTable(); initPIC();
uint64_t idt_operand = (sizeof(IDT)-1) | ((uint64_t)((uint32_t)IDT << 16)); asm volatile("lidt %0" : : "m" (idt_operand)); put_str("init interrupt all done\n"); }
|
使用 qemu 运行如下

可以看到时钟中断的触发(时钟中断的向量号正是 0x20)