中断分为外部中断和软中断,外部中断由硬件产生,软件无法预知外部中断发生的时机,软中断(陷阱trap)由软件触发,系统调用就是通过软中断的一种(INT 0x80)来实现的,异常通常是因为发生了类似于0作除数或者缺页异常这样的错误引发,异常的种类有cpu规定。
x86 cpu支持256个不同的中断向量,在实模式中,内存开头的1K字节作为中断向量表,每个向量占4个字节,两个字节段地址两个字节的位移,这样就能组成相应服务程序的入口地址。
保护模式下,为了实现模式切换和优先级的判断,中断向量表中的内容被扩展为门的概念,意思是当中断或者异常发生时,要首先穿过这道门才能到达相应服务程序。
一、门
x86 cpu一共有四种门:任务门(task gate)、中断门(interrupt gate)、陷阱门(trap gate)、调用门(call gate)。其中调用门不跟中断向量表关联。
门的大小为64位,四种门的结构大同小异:
16b 1b 2b 1b 1b 3b 8b 16b 16b
+-----------------+-+-----+-+-+---------+----------+------------+-----------+
| 位移的高16位 |P| DPL |0|D| 类型码 | 空闲 | 段选择码 | 位移低16位 |
+-----------------+-+-----+-+-+---------+----------+------------+-----------+
任务门只负责切换任务,不需要段内偏移,所以任务门的最高16位和最低16位空闲不用,对于其他三种门,这两部分共同指示了服务程序的入口地址。
P标志位表示在内存
DPL是优先级
D标志位 1=32位,0=16位
类型码:110中断门, 111陷阱门,100调用门,101任务门
另外,通过中断门进入中断服务程序时,cpu会自动将中断关闭,也就是将EFLAGS中的IF标志位清0,防止中断的嵌套,而通过陷阱门进入服务程序则不会改变IF标志位。
如果中断是由外部产生,或者cpu产生,则DPL优先级的检查被免除,可以直接进门。
否则,要先用cpu的cpl与这个门的dpl相比较,cpl必须小于门的dpl才能过,也就是优先级要比门高才能越过这个门槛,进门以后有需要比较目标段的dpl与cpl进行比较,目标段dpl必须小于cpl,也就是说通过门进行的任务切换只能提高运行等级,不能降低等级。
中断服务流程
中断向量V ---->向量表IDT---->中断门--(段选择码)-->段描述表GDT
|段 |段
|内 |基
|偏 |地
|移 |址
|------------------->+------------>服务程序
二、中断
中断向量表的初始化
linux内核在初始化阶段完成了对页面虚拟管理的初始化以后,便调用了trap_init和init_IRQ两个函数进行了中断机制的初始化。
trap_init主要针对一些系统保留的中断向量的初始化,而init_IRQ则是用于外设的中断。
trap_init
void __init trap_init(void)
{
#ifdef CONFIG_EISA
if (isa_readl(0x0FFFD9) == 'E'+('I'<<8)+('S'<<16)+('A'<<24))
EISA_bus = 1;
#endif
set_trap_gate(0,÷_error);
set_trap_gate(1,&debug);
set_intr_gate(2,&nmi);
set_system_gate(3,&int3); /* int3-5 can be called from all */
set_system_gate(4,&overflow);
set_system_gate(5,&bounds);
set_trap_gate(6,&invalid_op);
set_trap_gate(7,&device_not_available);
set_trap_gate(8,&double_fault);
set_trap_gate(9,&coprocessor_segment_overrun);
set_trap_gate(10,&invalid_TSS);
set_trap_gate(11,&segment_not_present);
set_trap_gate(12,&stack_segment);
set_trap_gate(13,&general_protection);
set_trap_gate(14,&page_fault);
set_trap_gate(15,&spurious_interrupt_bug);
set_trap_gate(16,&coprocessor_error);
set_trap_gate(17,&alignment_check);
set_trap_gate(18,&machine_check);
set_trap_gate(19,&simd_coprocessor_error);
set_system_gate(SYSCALL_VECTOR,&system_call);
/*
* default LDT is a single-entry callgate to lcall7 for iBCS
* and a callgate to lcall27 for Solaris/x86 binaries
*/
set_call_gate(&default_ldt[0],lcall7);
set_call_gate(&default_ldt[4],lcall27);
/*
* Should be a barrier for any external CPU state.
*/
cpu_init();
#ifdef CONFIG_X86_VISWS_APIC
superio_init();
lithium_init();
cobalt_init();
#endif
}
程序设置了中断向量表的前19个cpu预留的异常表项,然后是系统调用向量,SYSCALL_VECTOR在include/asm-i386/hw_irq.h中定义为0x80
#define SYSCALL_VECTOR 0x80
可以看到,向量表项的初始化使用了四个函数set_trap_gate、set_intr_gate、set_system_gate、以及set_call_gate,其中调用门linux本身并不使用,只是为了兼容在支持调用门的系统上编译的应用程序而设置了两个调用门。
void set_intr_gate(unsigned int n, void *addr)
{
_set_gate(idt_table+n,14,0,addr);
}
static void __init set_trap_gate(unsigned int n, void *addr)
{
_set_gate(idt_table+n,15,0,addr);
}
static void __init set_system_gate(unsigned int n, void *addr)
{
_set_gate(idt_table+n,15,3,addr);
}
这些函数统统调用_set_gate实现,只是第二三个参数不同,这两个参数分别对应了中断门中的D标志位加类型码type和优先级dpl。
参数 | D | 类型码 |
14 | 1 | 110 | intr
15 | 1 | 111 | trap
中断的dpl一律设置为最高等级0,因为当中断是由外部或cpu异常产生的,中断门的dpl被忽略不计可以自然过门,而用户程序尝试使用INT 2这样的指令来进入不可屏蔽中断的服务程序时,由于用户空间的运行级别为3,由用户软件产生的中断就会被拒之门外。
系统调用的dpl设置为3是为了用户空间能够使用系统调用进入系统空间。
#define _set_gate(gate_addr,type,dpl,addr) \
do { \
int __d0, __d1; \
__asm__ __volatile__ ("movw %%dx,%%ax\n\t" \
"movw %4,%%dx\n\t" \
"movl %%eax,%0\n\t" \
"movl %%edx,%1" \
:"=m" (*((long *) (gate_addr))), \
"=m" (*(1+(long *) (gate_addr))), "=&a" (__d0), "=&d" (__d1) \
:"i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
"3" ((char *) (addr)),"2" (__KERNEL_CS << 16)); \
} while (0)
_set_gate将addr的高16位放在门的最高16位(48-63位),低16位放在门的最低16位(0-15位)
将代码段地址放在了段选择码的位置(16-31位)
P标志位+dpl+D标志位+类型码放在(40-47位)刚好填充了中断门的所有内容。
init_IRQ
void __init init_IRQ(void)
{
int i;
#ifndef CONFIG_X86_VISWS_APIC
init_ISA_irqs();
#else
init_VISWS_APIC_irqs();
#endif
/*
* Cover the whole vector space, no vector can escape
* us. (some of these will be overridden and become
* 'special' SMP interrupts)
*/
for (i = 0; i < NR_IRQS; i++) {
int vector = FIRST_EXTERNAL_VECTOR + i;
if (vector != SYSCALL_VECTOR)
set_intr_gate(vector, interrupt[i]);
}
#ifdef CONFIG_SMP
...
#endif
#ifdef CONFIG_X86_LOCAL_APIC
...
#endif
/*
* Set the clock to HZ Hz, we already have a valid
* vector now:
*/
outb_p(0x34,0x43); /* binary, mode 2, LSB/MSB, ch 0 */
outb_p(LATCH & 0xff , 0x40); /* LSB */
outb(LATCH >> 8 , 0x40); /* MSB */
#ifndef CONFIG_VISWS
setup_irq(2, &irq2);
#endif
/*
* External FPU? Set up irq13 if so, for
* original braindamaged IBM FERR coupling.
*/
if (boot_cpu_data.hard_math && !cpu_has_fpu)
setup_irq(13, &irq13);
}
init_IRQ的主要内容就是一个循环设置了所有外部中断的中断向量表项,还有对系统时钟的初始化(outb那三行),这时候中断向量中并没有实际的服务程序,所以即使打开并产生了时钟中断,也只是在common_interrupt中空跑一趟。
我们看到循环中从FIRST_EXTERNAL_VECTOR开始初始化除SYSCALL_VECTOR以外的NR_IRQS-1个外部中断,并且将他们的服务程序注册成interrupt[i]。
/*
* 16 8259A IRQ's, 208 potential APIC interrupt sources.
* Right now the APIC is mostly only used for SMP.
* 256 vectors is an architectural limit. (we can have
* more than 256 devices theoretically, but they will
* have to use shared interrupts)
* Since vectors 0x00-0x1f are used/reserved for the CPU,
* the usable vector space is 0x20-0xff (224 vectors)
*/
#define NR_IRQS 224
/*
* IDT vectors usable for external interrupt sources start
* at 0x20:
*/
#define FIRST_EXTERNAL_VECTOR 0x20
#define SYSCALL_VECTOR 0x80
i386支持256个中断向量,0x20以前的向量有cpu预留使用,所以外部中断使用0x20-0xff的224个中断向量,还有0x80作为系统调用专用的中断号自然要去掉。
interrupt[i]是一组函数指针,定义在arch/i386/kernel/i8259.c
#define IRQ(x,y) \
IRQ##x##y##_interrupt
#define IRQLIST_16(x) \
IRQ(x,0), IRQ(x,1), IRQ(x,2), IRQ(x,3), \
IRQ(x,4), IRQ(x,5), IRQ(x,6), IRQ(x,7), \
IRQ(x,8), IRQ(x,9), IRQ(x,a), IRQ(x,b), \
IRQ(x,c), IRQ(x,d), IRQ(x,e), IRQ(x,f)
void (*interrupt[NR_IRQS])(void) = {
IRQLIST_16(0x0),
#ifdef CONFIG_X86_IO_APIC
IRQLIST_16(0x1), IRQLIST_16(0x2), IRQLIST_16(0x3),
IRQLIST_16(0x4), IRQLIST_16(0x5), IRQLIST_16(0x6), IRQLIST_16(0x7),
IRQLIST_16(0x8), IRQLIST_16(0x9), IRQLIST_16(0xa), IRQLIST_16(0xb),
IRQLIST_16(0xc), IRQLIST_16(0xd)
#endif
};
最终,interrupt[i]被展开为IRQ0x00_interrupt~IRQ0x0f_interrupt等函数指针,而这些函数指针同样定义在这个文件
#define BI(x,y) \
BUILD_IRQ(x##y)
#define BUILD_16_IRQS(x) \
BI(x,0) BI(x,1) BI(x,2) BI(x,3) \
BI(x,4) BI(x,5) BI(x,6) BI(x,7) \
BI(x,8) BI(x,9) BI(x,a) BI(x,b) \
BI(x,c) BI(x,d) BI(x,e) BI(x,f)
/*
* ISA PIC or low IO-APIC triggered (INTA-cycle or APIC) interrupts:
* (these are usually mapped to vectors 0x20-0x2f)
*/
BUILD_16_IRQS(0x0)
BUILD_IRQ()定义在include/asm-i386/hw_irq.h,是一段汇编代码
#define BUILD_IRQ(nr) \
asmlinkage void IRQ_NAME(nr); \
__asm__( \
"\n"__ALIGN_STR"\n" \
SYMBOL_NAME_STR(IRQ) #nr "_interrupt:\n\t" \
"pushl $"#nr"-256\n\t" \
"jmp common_interrupt");
nr的内容位0x00~0x0f,这段汇编翻译一下就是IRQ0x00_interrupt到IRQ0x0f_interrupt这16个标签都先将一个nr-256的数字压栈然后跳转到common_interrupt执行,这个地方之所以要减去一个256是为了区分系统调用的调用号,系统调用发生时,堆栈中的这个位置存放对应的系统调用号,而系统调用与中断共用与中断服务会共用一部分子程序,所以需要进行区分。
common_interrupt由arch/i386/kernel/i8259.c中的BUILD_COMMON_IRQ()定义,最终实现在include/asm-i386/hw_irq.h
#define BUILD_COMMON_IRQ() \
asmlinkage void call_do_IRQ(void); \
__asm__( \
"\n" __ALIGN_STR"\n" \
"common_interrupt:\n\t" \
SAVE_ALL \
"pushl $ret_from_intr\n\t" \
SYMBOL_NAME_STR(call_do_IRQ)":\n\t" \
"jmp "SYMBOL_NAME_STR(do_IRQ));
common_interrupt重要的操作是一个SAVE_ALL宏定义,用作我们提到中断时常说的保护现场,其次就是将一个ret_from_intr的标签地址压栈,这样就能使得下面即将跳转执行的do_IRQ返回时,直接返回到ret_from_intr处继续执行。
首先SAVE_ALL把中断发生前所有寄存器的内容都保存到栈中,然后将段寄存器DS和ES用于指向内核的__KERNEL_DS。
#define SAVE_ALL \
"cld\n\t" \
"pushl %es\n\t" \
"pushl %ds\n\t" \
"pushl %eax\n\t" \
"pushl %ebp\n\t" \
"pushl %edi\n\t" \