linux系统中的中断系统就是对关于中断的汇编指令集的一个包装,将所有的中断功能进行集中处理,为各个中断建立相应的处理程序,本文主要目的是记录linux0.11下面的中断系统实现方式。
中断初始化过程主要有:在内存中建立中断描述符表、中断项的初始化及中断描述项与对应的处理程序的关联。下面先说明中断描述符表的建立。从代码上看主要就是下面几段。所有的中断基本上都是基于代码-1和代码-2所建立的数据结构,代码-3是两个数据结构的初始化过程。
linux系统运行在保护模式下时会依赖几个基本的硬件相关的数据结构,它们包括全局描述符表、局部描述符表及中断描述符表。其中全局描述符表的基地址由一个寄存器来保存即:GDTR(全局描述符表寄存器),中断描述符表的基地址由相应的寄存器来保存即:IDTR(中断描述符表寄存器),这两个寄存器的存取都由相应的汇编指令。下面只说明有关中断描述符表寄存器的指令: lidt(加载IDT)和中断寄存器结构。
IDTR(中断寄存器)的结构。IDTR由两部分组成:指示中断描述符基地址的32位段基址和指出中断描述符长度的16位界限,共48位。代码-1就是这一结构所需要的数据结构。第92行就是加载中断寄存器的代码,head.s采用AT&T汇编码格式,所以lidt idt_descr中的标示符代表内存地址而非立即数据寻址。
IDT(中断描述符表)结构。代码-1中_idt的标示符的基址表示中断描述符表所在的基地址,而在其相应内存中开辟空间的声明代码为:代码-2。填充结构为256项每一项为8个字节,并将所有的空间填充为0。
下面对代码-3作详细说明。
/* 代码-1 中断描述表寄存器所需要的结构 (出自boot/head.s) */
220 .align 2
221 .word 0
222 idt_descr:
223 .word 256*8-1 # idt contains 256 entries
224 .long _idt
/* 代码-2 中断描述表所占用的数据结构 (出自boot/head.s) */
232 _idt: .fill 256,8,0 # idt is uninitialized
/* 代码-3 中断数据结构的初始化和中断描述寄存器初始化(出自boot/head.s) */
67 /*
68 * setup_idt
69 *
70 * sets up a idt with 256 entries pointing to
71 * ignore_int, interrupt gates. It then loads
72 * idt. Everything that wants to install itself
73 * in the idt-table may do so themselves. Interrupts
74 * are enabled elsewhere, when we can be relatively
75 * sure everything is ok. This routine will be over-
76 * written by the page tables.
77 */
78 setup_idt:
79 lea ignore_int,%edx
80 movl $0x00080000,%eax
81 movw %dx,%ax /* selector = 0x0008 = cs */
82 movw $0x8E00,%dx /* interrupt gate - dpl=0, present */
83
84 lea _idt,%edi
85 mov $256,%ecx
86 rp_sidt:
87 movl %eax,(%edi)
88 movl %edx,4(%edi)
89 addl $8,%edi
90 dec %ecx
91 jne rp_sidt
92 lidt idt_descr
93 ret
94
先说明中断描述符表中每一项的结构。
+-------------------+-----+-------+---+----------+
| OFFSET31--16 |P|DPL|01110 |000|(NOT USED)|
+-------------------+-----+-------+---+----------+
| SELECTOR | OFFSET 15--0 |
+-------------------+----------------------------+
图-1
每一个中断描述符项都由上面的一个结构组成,由8个字节组成,其中OFFSET15--0和OFFSET31--16是用来标定某一个代码段上中断程序入口的偏移地址;SELECTOR表示GDT或LDT中的可执行代码段描述符。SELECTOR的结构为:
+-------------+--+-----+
| INDEX15---3 |TI| RPL |
+-------------+--+-----+
图-2
BIT15到BIT3为是指向某一描述符表中其中一项的索引。TI表示此SELECTOR指向GDT还是指向LDT,如果TI=0则表示此选择子指向一全局描述符表,否则指向局部描述符表。中断描述符表中每一项是指向某一段描述符的一项这主要由SELECTOR来实现,而其中的偏移量(32位偏移量)主要是表示在SELECTOR所选定的代码段项中断处理程序的入口偏移。
中断执行过程如下图所示。
+---------------------------+
| IDTR |----+ INT5
+---------------------------+ |
# 图-3 中断描述符表寄存器 |
# |
+---------------------------+ <--+
| 中断描述符1 |
+---------------------------+
| 中断描述符2 |
+---------------------------+
| 中断描述符3 |
+---------------------------+
| 中断描述符4 |
+-------------+--+---+------+
| INDEX15---3 |TI|RPL|其它项 |----+ [TI=1 INDEX=3]
+-------------+--+---+------+ |
| 中断描述符6 | |
+---------------------------+ |
# 图-4中断描述符表 |
# |
+---------------------------+ |
| 局部代码段描述符1 | |
+---------------------------+ |
| 局部代码段描述符2 | |
+---------+-----------------+ <--+
| 基地址BASE|限长度LIMIT |----+[LIMIT=0X20 ]
+---------+-----------------+ |[BASE=0X1234]
| 局部代码段描述符4 | |
+---------------------------+ |
# 图-5局部代码段描述符表 |
# |
+---------------+ |
| 线性内存 | |
+---------------+---0X1234 |
| 线性内存 | <--------------+
+---------------+
| 线性内存 |
+---------------+
| 线性内存 |
+---------------+
| 线性内存 |
+---------------+
| 线性内存 |
+---------------+
# 图-6 线性地址
调用过程是:程序触发INT指令或CPU内部出现错误时触发中断指令,下面以程序用INT指令触发中断处理过程为例说明中断调用所涉及到的系统数据结构及系统寄存器及其引用过程(INT 5)。
首先程序调用INT指令,CPU完成对现有运行环境入栈工作,然后根据中断描述符表寄存器中中断描述符表地址找到中断描述符表。再根据INT指令后的中断向量来确实中断向量表中的项(指向第五项)。再由中断描述符表第五项中的选择子[TI=1 INDEX=3]来确实局部描述符表中的项[3]再由局部描述符表项中的基地址来确实中断处理程序所在的线性内存中的地址,然后再由分页机制完成物理内存的定位。确实好中断处理程序后就将其加载入CPU执行,执行完成后返回中断以前执行的环境,继承中断以前的执行。
下面针对上面所写的内容看linux0.11代码中是如何初始化各种中断数据结构的。先看代码-3的79--83行
79 lea ignore_int,%edx
80 movl $0x00080000,%eax
81 movw %dx,%ax /* selector = 0x0008 = cs */
82 movw $0x8E00,%dx /* interrupt gate - dpl=0, present */
79行中的ingnore_int是作者为中断描述符表设置的一个空的处理程序没有实现代码代码为下面的代码-4
/* 代码-4默认中断处理程序[出自boot/head.s] */
146 /* This is the default interrupt "handler" :-) */
147 int_msg:
148 .asciz "Unknown interrupt\n\r"
149 .align 2
150 ignore_int:
151 pushl %eax
152 pushl %ecx
153 pushl %edx
154 push %ds
155 push %es
156 push %fs
157 movl $0x10,%eax
158 mov %ax,%ds
159 mov %ax,%es
160 mov %ax,%fs
161 pushl $int_msg
162 call _printk
163 popl %eax
164 pop %fs
165 pop %es
166 pop %ds
167 popl %edx
168 popl %ecx
169 popl %eax
170 iret
171
79 lea ignore_int, %edx 将中断处理函数地址加载到寄存器edx中,
80 movl $0x00080000,%eax 将数据放入寄存器eax中。
81 movw %dx,%ax 将edx的低16字节[dx]放入eax中的低16字节[ax]中,这样eax中的高16字节中就是0x0008,低16个字节就是ignore_int地址的低16字节,所以eax中就是图-1中的低32位数据。
82 movw $0x8E00,%dx 再将0x8E00放入edx的低16字节中,这样edx就形成了图-1中的高32位数据。
下面说明84--93,这部分代码主要用来初始化中断描述符表及中断描述符寄存器。
84 lea _idt,%edi 将中断描述符表基地址放入edi中,作为初始化的初始地址。
85 mov $256,%ecx 中断描述符表一共有256项,所以将十进制的256放入ecx寄存器
86 rp_sidt: 循环的开始位置。
87 movl %eax,(%edi) 将eax寄存器中的内容放入edi指定的内存中,
88 movl %edx,4(%edi) 将edx寄存器中的内容放入edi+4指定的内存中
89 addl $8,%edi 将指针edi增加8以指向下一个中断描述符表项起始地址。
90 dec %ecx 计数器减1
91 jne rp_sidt 判定然后跳转
92 lidt idt_descr 填充中断描述符寄存器。
93 ret
设置中断、陷阱门
当linux系统在head.s中完成中断系统的初步的初始化以后,系统在/init/main.c中继续进行中断系统的初始化工作,这部分工作主要是对head.s中设置的ignore_int中断处理函数进行替换,换上真正处理中断请求的函数。
这部分工作主要在main.c中完成。下面看main.c中是如何完成中断系统的进一步初始化的。
/* 代码-1main.c中的中断初始化[出自/init/main.c] */
126 mem_init(main_memory_start,memory_end);
127 trap_init();
128 blk_dev_init();
129 chr_dev_init();
130 tty_init();
131 time_init();
132 sched_init();
133 buffer_init(buffer_memory_end);
134 hd_init();
135 floppy_init();
136 sti();
其中第127行是陷阱门的初始化代码,第132行是时钟中断门和系统调用中断门的初始化代码。下面先说明127行代码如下:
/* 代码-2trap_init函数定义[出自/kernel/traps.c] */
181 void trap_init(void)
182 {
183 int i;
184
185 set_trap_gate(0,÷_error);
186 set_trap_gate(1,&debug);
187 set_trap_gate(2,&nmi);
188 set_system_gate(3,&int3); /* int3-5 can be called from all */
189 set_system_gate(4,&overflow);
190 set_system_gate(5,&bounds);
191 set_trap_gate(6,&invalid_op);
192 set_trap_gate(7,&device_not_available);
193 set_trap_gate(8,&double_fault);
194 set_trap_gate(9,&coprocessor_segment_overrun);
195 set_trap_gate(10,&invalid_TSS);
196 set_trap_gate(11,&segment_not_present);
197 set_trap_gate(12,&stack_segment);
198 set_trap_gate(13,&general_protection);
199 set_trap_gate(14,&page_fault);
200 set_trap_gate(15,&reserved);
201 set_trap_gate(16,&coprocessor_error);
202 for (i=17;i<48;i++)
203 set_trap_gate(i,&reserved);
204 set_trap_gate(45,&irq13);
205 outb_p(inb_p(0x21)&0xfb,0x21);
206 outb(inb_p(0xA1)&0xdf,0xA1);
207 set_trap_gate(39,¶llel_interrupt);
208 }
以第185行为例说明每一个中断的初始化过程。 set_trap_gate为一个设置中断描述符表的宏定义,其中的÷_error为取得中断处理函数divide_error的地址。下面先说明各个中断设置宏的定义,再说明各个中断处理函数的定义格式。
set_trap_gate及set_system_gate定义在/include/asm/system.h
/* 代码-3set_trap_gate及set_system_gate宏定义 */
22 #define _set_gate(gate_addr,type,dpl,addr) \
23 __asm__ ("movw %%dx,%%ax\n\t" \
24 "movw %0,%%dx\n\t" \
25 "movl %%eax,%1\n\t" \
26 "movl %%edx,%2" \
27 : \
28 : "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
29 "o" (*((char *) (gate_addr))), \
30 "o" (*(4+(char *) (gate_addr))), \
31 "d" ((char *) (addr)),"a" (0x00080000))
32
33 #define set_intr_gate(n,addr) \
34 _set_gate(&idt[n],14,0,addr)
35
36 #define set_trap_gate(n,addr) \
37 _set_gate(&idt[n],15,0,addr)
38
39 #define set_system_gate(n,addr) \
40 _set_gate(&idt[n],15,3,addr)
下面说明代码 _set_gate宏定义。
/* 代码-4 _set_gate宏定义[出自/include/asm/system.h] */
22 #define _set_gate(gate_addr,type,dpl,addr) \
23 __asm__ ("movw %%dx,%%ax\n\t" \
24 "movw %0,%%dx\n\t" \
25 "movl %%eax,%1\n\t" \
26 "movl %%edx,%2" \
27 : \
28 : "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
29 "o" (*((char *) (gate_addr))), \
30 "o" (*(4+(char *) (gate_addr))), \
31 "d" ((char *) (addr)),"a" (0x00080000))
这段代码整体上是c语言的宏定义,宏定义内部嵌入了AT&T格式的汇编代码。AT&T和c语言的混合编程的语法在
这里。
+-------------------+-----+-------+---+----------+
| OFFSET31--16 |P|DPL|TYPE |000|(NOT USED)|
+-------------------+-----+-------+---+----------+
| SELECTOR | OFFSET 15--0 |
+-------------------+----------------------------+
# 图-1中断描述符
先说明图-1中的各个位意义。P代表SELECTOR所指向的段是否存在,如果为1表示存在其为一个BIT位,DPL表示描述符权限级别,分为四级0-3。由2BIT组成。TYPE由5BIT组成,表示此描述符是中断门还是陷阱门或是任务门。00101表示是任务门、01110表示中断门、01111表示陷阱门。
[linux中没有用到任务门的概念]
通过中断门的转移和通过陷阱门的转移之间的差别只是对IF标志的处理。对于中断门,在转移过程中把IF置为0,使得在处理程序执行期间屏蔽掉INTR中断 (当然,在中断处理程序中可以人为设置IF标志打开中断,以使得在处理程序执行期间允许响应可屏蔽中断);对于陷阱门,在转移过程中保持IF位不变,即如 果IF位原来是1,那么通过陷阱门转移到处理程序之后仍允许INTR中断。因此,中断门最适宜于处理中断,而陷阱门适宜于处理异常。
下面解释四个输入参数。
参数1: "i" ((short) (0x8000+(dpl<<13)+(type<<8)))[在嵌入代码中表示为%0]。0x8000表示描述符中的P为1;dpl<<13将程序所输入的权限级别向左移动13位正好是DPL所在的位置;type<<8将程序所输入的类型值向左移动8位正好是描述符中TYPE所在的位置。其中参数值14代表中断门、15参数值代表陷阱门。所以些输入参数就是设置描述符的高四字节的低16位。
参数2:"o" (*((char *) (gate_addr))),表示描述符在内存中的低32位的起始地址。
参数3:"o" (*(4+(char *) (gate_addr))),表示描述符在内存中的高32位的起始地址。
参数4: "d" ((char *) (addr)),将中断处理函数的地址放入到寄存器edx中。
参数5:"a" (0x00080000),将立即数(0x00080000)放入寄存器eax中。
下面说明汇编代码。
23 __asm__ ("movw %%dx,%%ax\n\t" \ 将edx寄存器的低16位放入eax的低16位中,也即:将中断处理函数地址的低16位放入eax的低16位中。这样eax就形成了描述符所需要的低32位数据
24 "movw %0,%%dx\n\t" \ 将第一个参数放入到edx寄存器的低16位中,这样edx就形成了描述符所需要的高32位数据。
25 "movl %%eax,%1\n\t" \ 存入描述符的低32位数据
26 "movl %%edx,%2" \ 存入描述符的高32位数据
代码-3中的如下定义就不再说明了主要的功能都是由_set_gate宏来完成的。
33 #define set_intr_gate(n,addr) \
34 _set_gate(&idt[n],14,0,addr)
35
36 #define set_trap_gate(n,addr) \
37 _set_gate(&idt[n],15,0,addr)
38
39 #define set_system_gate(n,addr) \
40 _set_gate(&idt[n],15,3,addr)
下面看代码-2中的中断处理函数定义方式,下面以数据divide_error为例说明其定义结构。
/* 代码-5 中断处理函数的声明[出自/kernel/trap.c] */
43 void divide_error(void);
44 void debug(void);
45 void nmi(void);
/* 代码-6中断处理函数[第一阶段]定义 [出自/kernel/asm.s] */
14 .globl _divide_error,_debug,_nmi,_int3,_overflow,_bounds,_invalid_op
15 .globl _double_fault,_coprocessor_segment_overrun
16 .globl _invalid_TSS,_segment_not_present,_stack_segment
17 .globl _general_protection,_coprocessor_error,_irq13,_reserved
18
19 _divide_error:
20 pushl $_do_divide_error
21 no_error_code:
22 xchgl %eax,(%esp)
23 pushl %ebx
24 pushl %ecx
25 pushl %edx
26 pushl %edi
27 pushl %esi
28 pushl %ebp
29 push %ds
30 push %es
31 push %fs
32 pushl $0 # "error code"
33 lea 44(%esp),%edx
34 pushl %edx
35 movl $0x10,%edx
36 mov %dx,%ds
37 mov %dx,%es
38 mov %dx,%fs
39 call *%eax
40 addl $8,%esp
41 pop %fs
42 pop %es
43 pop %ds
44 popl %ebp
45 popl %esi
46 popl %edi
47 popl %edx
48 popl %ecx
49 popl %ebx
50 popl %eax
51 iret
/* 代码-7中断处理函数[第二阶段]定义[出自/kernel/traps.c] */
97 void do_divide_error(long esp, long error_code)
98 {
99 die("divide error",esp,error_code);
100 }
调用过程是代码-6中的定义会调用代码-7中所定义的do_XXXX_XXXX函数。实际的工作由一系列的do_XXXX_XXXX来完成。