笔记重点:
尽管时代在变化,中断系统的基本原理却是变化不大,对新版linux的中断系统的分析和理解起到指引作用,中断系统的细节包括,中断发生前的上下文保存以及中断处理完成后的上下文恢复;中断的统一调用过程中CPU的异常处理是,当CPU响应异常或者中断时,处理器使用异常或者中断的向量作为IDT表的索引,引用索引中的门描述符指向的中断程序,进行处理。硬件对中断的支持表现在CPU中有专门的中断向量表寄存器IDTR,并使用专门的指令在设置和读取这个寄存器的内容(LIDT和SIDT),以及8259A这样的中断控制器处理外部中断的优先级;中断系统的中断来源三个方面,分析linux中的中断系统的原理是从以下三个方面的透彻理解入手的:
- 一个方面是来自CPU执行流中的内部中断和异常
- 一方面有外部设备通过8259A中断控制器向CPU的INTR脚请求中断
- 还有一个方面被称为软中断的系统调用
1. 三种中断之内部中断处理
这里要讲的就是来自CPU中的异常和中断,是不经过8259A中断处理器的,在书中的术语叫异常,以区别外部设备的中断;在笔记重点中便提到了来自CPU内部的中断或者异常的处理路径,就是处理器直接使用异常或者中断向量作为IDT表的索引,查询索引中的门描述所指向的异常处理程序,调用处理程序,而这些异常向量的定义,通常就是CPU厂商自己定义的,范围在0~31,俗称约定,这个地方我理解了很久:!参照以下表格定义就不难理解:
中断号 | 名称 | 类型 | 信号 | 说明 |
---|---|---|---|---|
0 | Devide error | 故障 | SIGFPE | 当进行除以零的操作时产生 |
1 | Debug | 陷阱故障 | SIGTRAP | |
2 | nmi | 硬件 | 由不可屏蔽中断NMI产生 | |
3 | Breakpoint | 陷阱 | SIGTRAP | 由断点指令int3产生,与debug处理相同 |
4 | Overflow | 陷阱 | SIGSEGV | eflags的溢出标志OF 引起 |
5 | Bounds check | 故障 | SIGSEGV | 寻址到有效地址意外时引起 |
6 | Invalid Opcode | 故障 | SIGILL | CPU执行时发现一个无效的指令操作码 |
7 | Device not available | 故障 | SIGSEGV | |
8 | Double fault | 异常中止 | SIGSEGV | 双故障出错 |
9 | Coprocessor segment overrun | 异常中止 | SIGFPE | 协处理器段超出 |
10 | Invalid TSS | 故障 | SIGSEGV | CPU切换时发觉TSS无效 |
11 | Segment not present | 故障 | SIGBUS | 描述符所指的段不存在 |
12 | Stack segment | 故障 | SIGBUS | 堆栈段不存在或者寻址越出堆栈段 |
13 | General protection | 故障 | SIGSEGV | 没有符合80386保护机制的操作引起 |
14 | Page fault | 故障 | SIGSEGV | 页不再内存 |
15 | Reserved | |||
16 | Coprocessor error | 故障 | SIGPE | 协处理器发出的出错信号引起 |
那么中断向量表是如何被找到的?
中断向量表在CPU中有专门的中断向量表寄存器IDTR,并使用专门的指令在设置和读取这个寄存器的内容(LIDT和SIDT),因此触发中断向量表的处理程序之前,就需要设置好中断处理程序,以下是代码过程:
1 | // 这里主要是设置异常处理函数到中断向量表中 |
divide_error函数的定义在kernel/asm.s中,在汇编中的符号是_divide_error
kernel/asm.s主要定义异常处理函数的通用框架,divide_error函数便在其中1
2
3
4
5
6
7
8
9_divide_error:
pushl $_do_divide_error
jmp no_error_code
_debug:
pushl $_do_int3 # _do_debug
jmp no_error_code
_nmi:
pushl $_do_nmi
jmp no_error_code
_divide_error函数实际将do_divide_error传入到no_error_code进行处理;而no_error_code函数首先做了上下文保存,再实际调用了do_divide_error函数来做具体工作,然后恢复上下文的工作。
这个do_divide_error函数实际又回到了kernel/trap.c中定义;1
2
3
4void do_divide_error(long esp, long error_code)
{
die("divide error",esp,error_code);
}
至此trap_init函数完成了异常中断处理函数的在中断向量表的设置,以及展示中断处理函数的定义过程;CPU在内部执行流程中触发异常时,便会通过异常向量号在中断向量表中寻找处理函数进行处理,以执行divide_error中断程序为例,异常发生到执行转到divide_error之前,被理解为CPU的内部处理
附注两个重要地方的说明:
no_error_code是异常处理的通用框架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
31no_error_code:
xchgl %eax,(%esp) #这里是将eax内容保存到%esp指向的地方,再把传入的函数传给eax
pushl %ebx #上下文保存已经开始
pushl %ecx
pushl %edx
pushl %edi
pushl %esi
pushl %ebp
push %ds
push %es
push %fs
pushl $0 # "error code"
lea 44(%esp),%edx
pushl %edx
movl $0x10,%edx
mov %dx,%ds
mov %dx,%es
mov %dx,%fs
call *%eax #调用异常处理程序
addl $8,%esp
pop %fs #上下文开始恢复
pop %es
pop %ds
popl %ebp
popl %esi
popl %edi
popl %edx
popl %ecx
popl %ebx
popl %eax
iret
set_trap_gate函数设置中断向量函数1
2
3
4
5
6
7
8
9
10
11
12
13
14//linux/include/asm/system.h : line:22
- %0 是指 : ((short) (0x8000+(dpl<<13)+(type<<8)))
- %1 是指 : (((char ) (gate_addr)))
- %2 是指 : ((4+(char ) (gate_addr)))
- edx : “d” ((char *) (addr))
- eax : “a” (0x00080000)
eax首先设置为0x00080000,是设置为内核的段选择符;第一句movw %%dx,%%ax;edx中包含的中断处理程序的地址,设置到eax的低16位上;第二句movw %0,%%dx;将(0x8000+(dpl<<13)+(type<<8)))值设置到edx;第三句和第四句是设置中断向量表的值;代码需要结合书中120页中断门描述门的格式来理解
2. 8259A中断处理
理解8259A的中断处理过程,从理解一段代码开始:
1 | mov al,#0x11 #(ICW1设置)中断请求边沿触发多片8259级联并最需发送ICW4 |
读取以上代码有困难的话,需要复习一下8259A中断控制器的相关知识,这是微机接口原理的主要内容,不过阅读上面代码的重点是告诉我们8259的中断向量是从0x20开始的,要记住这一点,不然时钟中断,硬盘中断等的中断向量号是怎么来的,你就不从知晓,可以参考一下列表:
现在来看具体的中断处理向量的设置,它们分散在不同的地方
时钟中断向量设置**
timer_interrupt这就是操作系统的心跳函数
linux/kernel/sched.c1
2
3
4
5
6void sched_init(void)
{
...
set_intr_gate(0x20,&timer_interrupt);
...
}硬盘中断向量设置**
linux/kernel/blk_drv/hd.c1
2
3
4
5
6void hd_init(void)
{
...
set_intr_gate(0x2E,&hd_interrupt);
...
}- 键盘中断向量设置**
linux/kernel/chr_drv/console.c1
2
3
4
5
6void con_init(void)
{
...
set_trap_gate(0x21,&keyboard_interrupt);
...
}
- 键盘中断向量设置**