0%

《linux 0.12内核完全剖析》--中断处理分析

笔记重点:
尽管时代在变化,中断系统的基本原理却是变化不大,对新版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
2
3
4
5
6
7
8
9
10
11
12
// 这里主要是设置异常处理函数到中断向量表中
// set_trap_gate就是设置中断向量第几号的处理函数
// linux/kernel/trap.c : line:185
void trap_init(void)
{
int i;
set_trap_gate(0,&divide_error);
set_trap_gate(1,&debug);
set_trap_gate(2,&nmi);
...
set_trap_gate(39,&parallel_interrupt);
}

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
4
void 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
31
no_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
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \
"movw %0,%%dx\n\t" \
"movl %%eax,%1\n\t" \
"movl %%edx,%2" \
: \
: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
"o" (*((char *) (gate_addr))), \
"o" (*(4+(char *) (gate_addr))), \
"d" ((char *) (addr)),"a" (0x00080000))

#define set_trap_gate(n,addr) \
_set_gate(&idt[n],15,0,addr)

  • %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
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
mov	al,#0x11		#(ICW1设置)中断请求边沿触发多片8259级联并最需发送ICW4
out #0x20,al ! send it to 8259A-1
.word 0x00eb,0x00eb #0x00eb跳转到下一句的机器码
out #0xA0,al ! and to 8259A-2
.word 0x00eb,0x00eb
mov al,#0x20 #(ICW2设置)主片中断号范围从0x20开始
out #0x21,al
.word 0x00eb,0x00eb
mov al,#0x28 #从片中断号范围从0x28开始
out #0xA1,al
.word 0x00eb,0x00eb
mov al,#0x04 #(ICW3设置)设置主芯片
out #0x21,al
.word 0x00eb,0x00eb
mov al,#0x02 #设置从芯片
out #0xA1,al
.word 0x00eb,0x00eb
mov al,#0x01 #(ICW4设置):普通EOI,非缓冲切需发送指令来复位的模式
out #0x21,al
.word 0x00eb,0x00eb
out #0xA1,al
#8259A中断控制器初始化结束

.word 0x00eb,0x00eb
mov al,#0xFF #屏蔽所有中断请求
out #0x21,al
.word 0x00eb,0x00eb
out #0xA1,al

读取以上代码有困难的话,需要复习一下8259A中断控制器的相关知识,这是微机接口原理的主要内容,不过阅读上面代码的重点是告诉我们8259的中断向量是从0x20开始的,要记住这一点,不然时钟中断,硬盘中断等的中断向量号是怎么来的,你就不从知晓,可以参考一下列表:
这里写图片描述

现在来看具体的中断处理向量的设置,它们分散在不同的地方

  • 时钟中断向量设置**
    timer_interrupt这就是操作系统的心跳函数
    linux/kernel/sched.c

    1
    2
    3
    4
    5
    6
    void sched_init(void)
    {
    ...
    set_intr_gate(0x20,&timer_interrupt);
    ...
    }
  • 硬盘中断向量设置**
    linux/kernel/blk_drv/hd.c

    1
    2
    3
    4
    5
    6
    void hd_init(void)
    {
    ...
    set_intr_gate(0x2E,&hd_interrupt);
    ...
    }
    • 键盘中断向量设置**
      linux/kernel/chr_drv/console.c
      1
      2
      3
      4
      5
      6
      void con_init(void)
      {
      ...
      set_trap_gate(0x21,&keyboard_interrupt);
      ...
      }
3. 系统调用(软中断)