中断——Interrupt
what
中断指一个由CPU外部(或者内部)产生的信号,用以通知CPU发生了某种特定的事件,如键盘输入、IO任务完成等。
why
以最简单的文件读写操作为例,与忙等(Busy waiting)相比,基于中断(Interrupt-based)的系统能更有效地利用有限的计算资源。没有底层硬件的支持,也就没法实现基于中断的操作系统。因此,现代CPU都采用基于中断的体系架构。
which
- 中断既可以产生于CPU外部,也可以产生于CPU内部。
CPU内部也可以产生中断,用以通知操作系统发生了某些特殊的事件,如页错误(Page Fault)等。
- 中断既可以由硬件发出,也可以由软件发出。
通过指令
int num
即可产生中断号为num的中断。软件发出的中断最常见的例子就是实现系统调用。
IDT简介
IDT DESCRIPTOR
和GDT一样,我们需要一个结构来告诉CPU,IDT的位置及其大小:
字段 | 大小(bytes) | 说明 |
---|---|---|
limit | 0~1 | 描述了IDT的大小(所占的字节数-1) |
base | 2~5 | IDT开始的32位线性地址 |
和GDT不一样的是:
- IDT中的第一个(index为0)的表项会被使用
- 256个表项描述了对应的256种不同的中断
- Limit字段可以
(!=256)*8 - 1
,即IDT表项的数量可以不为256.但是超过的的部分将被系统忽略,而如果对应的中断在触发时没有对应的IDT表项则将抛出GPT(General Protection Fault,通用保护错误)。
IDT 表项
IDT的表项相对GDT简单,从下述数据结构就可以很清晰的知道其组成:
struct IDTDescr{
uint16_t offset_1; // offset 的低16位
uint16_t selector; // 对应GDT(或LDT)中的一个代码段的选择子
uint8_t zero; // 未使用,设为0
uint8_t type_attr; // 类型和属性
uint16_t offset_2; // offset 的高16位
};
其中字段type_attr描述了中断的种类以及权限等信息,在此不做展开。我们将设置我么的所有中断为32bit的ring0特权级下的中断门,对应的type_attr值为0x8e.
编写中断处理程序ISR
发生中断时,CPU会根据中断号从IDT中加载对应的中断处理程序ISR(Interrupt Service Routine)。而中断发生前的状态的保存于中断处理完之后状态的恢复,需要由硬件和操作系统相互配合来完成。
为什么要为每个中断实现不同的中断处理程序?
为什么不能实现一个统一的中断处理程序,所有的IDT表项都指向它,由它根据具体的中断号而确定相应的操作?
这是因为x86架构在设计时,CPU会根据中断号从IDT中寻找处理程序,而这个中断号并不会以某种参数的形式传递给相应的中断处理程序。因此,我需要为每个中断实现特有的中断处理程序。如有以下中断程序程序和中断号:
中断号 | 中断处理程序 |
---|---|
0 | isr0() |
1 | isr1() |
... | ... |
255 | isr255() |
因此,当isr0被调用时,我们的系统就知道发生了中断0;当isr255被调用时,发生了中断255。
但是如果完完全全为每一个中断实现一个不同的中断处理器,在设计上显得有些蹩脚,同时会有大量重复的代码。因此我们可以将所有中断处理程序中相同的代码抽取出来,作为一个独立的部分。所有的中断处理程序在完成特定的处理之后(即将中断号、错误号压栈以后)调用这个共同的代码逻辑而完成对中断的相同的操作(保存中断发生前的状态)。
有些中断没有错误号?
为了使所有的中断都能使用一致的数据结构,我们可以给没有错误号的中断,增加一个哑错误号(Dummy Error Code)
需要保存哪些数据?
CPU在发生中断时,硬件会自动地为我们保存(压栈)eip,ec,eflags,esp,ss寄存器的内容。而软件(操作系统)需要:
- 保存中断号,以及错误号
这样我们的中断处理程序才知道具体发生了什么中断
- 通用寄存器
中断处理程序也是程序,也会使用到通用寄存器,为了能在中断处理完成之后,我们必须能恢复这些通用寄存器的值。
- 中断前数据段选择子
硬件只为我们保存了代码段选择子(CS寄存器),我们需要自己保存中断前数据段的选择子。
综上,我们需要保存的数据可以由如下数据结构表示:
typedef struct {
uint32_t ds; //Saved data segment selector
uint32_t edi, esi, ebp, useless_esp, ebx, edx, ecx, eax; // Pushed by pusha.
uint32_t int_no, err_code; // Interrupt number and error code
uint32_t eip, cs, eflags, esp, ss; // Pushed by the processor automatically.
} registers_t;
实现中断处理程序
笔者采用两段式中断处理程序,即汇编+C。
汇编代码部分:
.macro ISR_WITH_ERROR_CODE num
.global isr\num
.type isr\num, @function
isr\num:
cli #clear interrupt
pushl $\num
jmp isr_comman_stub
.endm
.macro ISR_NO_ERROR_CODE num
.global isr\num
.type isr\num, @function
isr\num:
cli #clear interrupt
pushl $0
pushl $\num
jmp isr_comman_stub
.endm
ISR_NO_ERROR_CODE 0
ISR_NO_ERROR_CODE 1
ISR_NO_ERROR_CODE 2
...
isr_comman_stub:
pusha #Pushes edi,esi,ebp,esp,ebx,edx,ecx,eax
movw %ds, %eax
pushl %eax #Save ds
mov $0x10, %ax #Load kernel data segment descriptor
mov %ax, %ds
mov %ax, %es
mov %ax, %fs
mov %ax, %gs
pushl %esp #Pointer to struct registers_t as argument
call isr_dispatcher
popl %esp
popl %eax # Restore data segment
mov %ax, %ds
mov %ax, %es
mov %ax, %fs
mov %ax, %gs
popa
addl $8, %esp #Cleans up the pushed error code and pushed ISR number
iret #pops 5 things at once: CS, EIP, EFLAGS, SS, and ESP. **EFLAGS contais wheter we should set interrupt
C代码示例:
//Dispath an interrupt to a i_handler
void isr_dispatcher(registers_t *regs);
//An interrupt handler. It is a pointer to a function which takes a pointer
//to a structure containing register values.
typedef void (*i_handler)(registers_t *);
//Allows us to register an interrupt handler.
void register_i_handler(int num, i_handler h);
运行效果
测试C代码:
void kernel_main(void)
{
printf("Hello, kernel World!\n");
printf("Sending int 3...\n");
asm volatile ("int $0x3");
printf("Sending int 4...\n");
asm volatile ("int $0x4");
}
运行截图
参考文献
- http://wiki.osdev.org/Interrupts
- http://wiki.osdev.org/Interrupt_Service_Routines
- https://en.wikipedia.org/wiki/Programmable_Interrupt_Controller?oldformat=true
- http://wiki.osdev.org/PIC
- http://wiki.osdev.org/APIC
- http://wiki.osdev.org/PS2_Keyboard
- http://www.jamesmolloy.co.uk/tutorial_html/4.-The%20GDT%20and%20IDT.html
- http://wiki.osdev.org/IDT