硬件产生中断之后,需要通过门描述符来寻找中断的处理程序入口。门描述符和段描述符一样,8个字节。门描述符大体分为:段偏移、段选择子以及DPL。段选择子用于在GDT中寻找到门段基址,DPL用于控制当前进程中断或异常访问权限。当发生中断时,将门描述符所指向的段基地放入%cs,将段偏移放入%eip,转入相应服务。
门描述符结构如下:
任务门描述符:用于在发生中断时调度相应进程
中断门描述符:描述中断处理程序所在段选择子和段内偏移值。据此修改EIP及关闭中断Eflags的IT标识位
陷阱门描述符:与中断门描述符一样,但不关中断
中断描述符表IDT(Interrupt Descriptor Table)用于存放256个门描述符,对应256个中断向量。其中IA32规定前0~31号固定向量用于异常。寄存器idtr为门描述符表IDT的物理地址。根据向量寻找基对应门描述符,并将中断或异常程序加载过程下图所示:
LINUX初始化中断描述符表分两步:初步初始化,以及最终初始化。初步初始化在head.S文件:
中断描述符表初步初始化1)声明256个门描述符的IDT表空间。
idt_descr: .word IDT_ENTRIES*8-1 # idt contains 256 entries .long idt_table2)设置指向IDT表地址的寄存器ldtr。
lgdt early_gdt_descr lidt idt_descr ljmp $(__KERNEL_CS),$1f 1: movl $(__KERNEL_DS),%eax # reload all the segment registers2)初始化256个门描述符。对于每个门描述符,段选择子都指向内核段,段偏移都指向igore_int函数,该函数只打印一句话:“哥们,别急,俺还没被真正初始化勒!~”。
/* * setup_idt * * sets up a idt with 256 entries pointing to * ignore_int, interrupt gates. It doesn't actually load * idt - that can be done only after paging has been enabled * and the kernel moved to PAGE_OFFSET. Interrupts * are enabled elsewhere, when we can be relatively * sure everything is ok. * * Warning: %esi is live across this function. */ setup_idt: lea ignore_int,%edx movl $(__KERNEL_CS << 16),%eax movw %dx,%ax /* selector = 0x0010 = cs */ movw $0x8E00,%dx /* interrupt gate - dpl=0, present */ lea idt_table,%edi mov $256,%ecx rp_sidt: movl %eax,(%edi) movl %edx,4(%edi) addl $8,%edi dec %ecx jne rp_sidt 中断描述符表最终初始化中断描述符表最终初始化分为两部分:异常和中断,分别在函数trap_init(void)和init_IRQ(void)中实现,都由系统初始化入口函数start_kernel()所调用。对于特定异常,其处理异常函数预先已经设定好;但对于特定异步中断,由于需要捕获设备I/O中断的程序数目不定,所以得采用特定数据结构来对irq及其action进行描述。
trap_init:
do_IRQ:
1)异常部分最终初始化
由于IA32规定异常所对应的固定向量,所以直接调用set_trap_gate()、set_intr_gate()、set_system_gate()、set_system_intr_gate()、set_task_gate()、set_intr_gate_ist()设置异常处理函数、初始相应异常。而这些函数,都只是_set_gate宏的封装而已。异常处理函数由宏定义DO_ERROR生成。其调用关系如下:
在trap_init(void)函数中,调用在set_intr_gate_ist()设置“stack exception”的12号中断门描述符。set_intr_gate_ist是设置中断描述符函数_set_gate的一层封装,给_set_gate传入异常处理函数stack_segment作为门描述符的偏移地址,传入作为段选择子,传入GATE_INTERRUPT表示中断门描述符类型,传入DPL = 0表示只能由内核态访问、而不能由用户调用。
_set_gate函数中,其调用pack_gate()组装成一个门描述符格式,并调用write_idt_entry写入IDT表中相应描述符。stack_segment函数压入do_stack_segment函数地址并跳转到error_code汇编代码进行处理。具体代码(/arch/x86/include/asm/desc.h)如下:
void (void) { (12, &, ); . . (); /* 初始化该平台x86的特定设备 */ } static inline void (int , void * , unsigned ) { ((unsigned) > 0xFF); ( , , , 0, , ); } static inline void (int , unsigned , void * , unsigned , unsigned , unsigned ) { ; (& , , (unsigned long) , , , ); /* * does not need to be atomic because it is only done once at * setup time */ ( , , & ); } static inline void ( * , unsigned char , unsigned long , unsigned , unsigned , unsigned short ) { -> = ( << 16) | ( & 0xffff); -> = ( & 0xffff0000) | (((0x80 | | ( << 5)) & 0xff) << 8); } static inline void ( * , int , const * ) { (& [ ], , sizeof(* )); } 然异常12号处理者stack_segment,何许人也?在/arch/x86/kernel/entry_32.S中定义如下: ENTRY(stack_segment) RING0_EC_FRAME pushl $do_stack_segment CFI_ADJUST_CFA_OFFSET 4 jmp error_code CFI_ENDPROC END(stack_segment)有的异常,例如stack_segment,系统由硬件产生错误码并压入栈中。另外一些异常,系统将不产生异常,例如overflow,为了统一中断、产生错误码的异常和不产生错误码的异常的栈内存布局,故“人为地”pushl $0。
ENTRY(overflow) RING0_INT_FRAME pushl $0 CFI_ADJUST_CFA_OFFSET 4 pushl $do_overflow CFI_ADJUST_CFA_OFFSET 4 jmp error_code CFI_ENDPROC END(overflow)在error_code中,首先保存中断寄存器上下文等,其次调用do_stack_segment进行处理,最后调用ret_from_exception进行善后工作,代码如下:
error_code: /* the function address is in %gs's slot on the stack */ pushl %fs CFI_ADJUST_CFA_OFFSET 4 /*CFI_REL_OFFSET fs, 0*/ pushl %es CFI_ADJUST_CFA_OFFSET 4 /*CFI_REL_OFFSET es, 0*/ pushl %ds CFI_ADJUST_CFA_OFFSET 4 /*CFI_REL_OFFSET ds, 0*/ pushl %eax CFI_ADJUST_CFA_OFFSET 4 CFI_REL_OFFSET eax, 0 pushl %ebp CFI_ADJUST_CFA_OFFSET 4 CFI_REL_OFFSET ebp, 0 pushl %edi CFI_ADJUST_CFA_OFFSET 4 CFI_REL_OFFSET edi, 0 pushl %esi CFI_ADJUST_CFA_OFFSET 4 CFI_REL_OFFSET esi, 0 pushl %edx CFI_ADJUST_CFA_OFFSET 4 CFI_REL_OFFSET edx, 0 pushl %ecx CFI_ADJUST_CFA_OFFSET 4 CFI_REL_OFFSET ecx, 0 pushl %ebx CFI_ADJUST_CFA_OFFSET 4 CFI_REL_OFFSET ebx, 0 cld movl $(__KERNEL_PERCPU), %ecx movl %ecx, %fs UNWIND_ESPFIX_STACK /* 用于处理系统栈不是32位的情况 */ GS_TO_REG %ecx /* 把%gs存入%ecx中 */ movl PT_GS(%esp), %edi # get the function address movl PT_ORIG_EAX(%esp), %edx # get the error code movl $-1, PT_ORIG_EAX(%esp) # no syscall to restart REG_TO_PTGS %ecx /* 把%gs的值存入栈的%gs中,即处理代码指针位置 */ SET_KERNEL_GS %ecx movl $(__USER_DS), %ecx movl %ecx, %ds movl %ecx, %es TRACE_IRQS_OFF movl %esp,%eax # pt_regs pointer call *%edi jmp ret_from_exception CFI_ENDPROC END(page_fault)在error_code中执行在1338行call *edi之前,栈的内存布局如下图。
在error_code处理“栈异常”处理时,call *edi = call do_stack_segment,do_stack_segment函数由DO_ERROR宏生成。error_code给do_stack_segment函数传入regs参数时,由ABI规范规定,eax为第一个参数 struct pt_regs regs。pt_regs与栈内存布局相同,其结构体如下:
struct { long ; long ; long ; long ; long ; long ; long ; int ; int ; int ; int ; /* 对应%gs,对于异常而言是处理器代码 */ long ; /* 对应于内存布局中的error code或vector */ long ; int ; long ; long ; int ; };stack_segment函数是“栈异常”的异常处理函数,由DO_ERROR宏产生:
#ifdef CONFIG_X86_32 (12, , "stack segment", ) #endif #define ( , , , ) \ void ##name(struct * , long ) \ { \ if ( ( , , , , , ) \ == ) \ return; \ ( ); \ (, , , , , ); \ }可见,do_stack_segment最后调用do_trap进行异常处理。
2)中断描述符表中断部分最终初始化
由start_kernel()调用init_IRQ(void)进行中断最终初始化。其操作流程如下:
1) 将CPU0 IRQ0…IRQ15的vectors设置为0…15。对于CPU0的vecotr_irq而言,若其IRQ是PIC中断,则其vector早已由PIC固定,所以设置其IRQ0…IRQ15的vector为0…15。若是I/O APIC,由于其vector分配是可由OS动态设置,所以vector 0…15可能会被重新分配;
2) 本质上调用native_init_IRQ()函数对irq_desc、以及中断部分门描述符进行初始化,并针对CONFIG_4KSTACKS配置、协处理器模拟浮点运算等进行配置;
a、 调用init_ISA_irqs()初始化8259A可断控制器,并对相应中断请求线IRQ进行初始化、使其对应中断控制器irq_desc的操作函数为8259A操作接口函数;
b、 调用apic_intr_init()函数针对采取I/O APIC中断处理器的情况,对APIC中断处理器进行初始化工作;
c、 将调用set_intr_gate为系统中每根中断请求线IRQ地应的中断向量号设置了相应的中断门描述门,其中断处理函数定义在interrupt数组中;
d、 在PC平台下set_irq(2, &rq2)对从8259A中断控制器的第三根IRQ请求做特殊处理;
e、 irq_ctx_init函数用于在配置CONFIG_4KSTACK的情况下配置当前CPU的中断栈相关项。LINUX内核在未配置CONFIG_4KSTACK时,共享所中断进程的内核栈,内核栈为两页,即8K。在2.6版本时,增加了CONFIG_4KSTACK选项,将栈大小从两页减小至一页,为了应对栈的减少,故中断处理程序拥有自己的中断处理程序线,为原先共享栈的一半,即4K,每个CPU拥有一个中断栈。
void (void) { int ; /* * On cpu 0, Assign IRQ0_VECTOR..IRQ15_VECTOR's to IRQ 0..15. * If these IRQ's are handled by legacy interrupt-controllers like PIC, * then this configuration will likely be static after the boot. If * these IRQ's are handled by more mordern controllers like IO-APIC, * then this vector space can be freed and re-used dynamically as the * irq's migrate etc. */ for ( = 0; < -> ; ++) (vector_irq, 0)[ + ] = ; /* intr_init()本质上调用native_init_IRQ()初始化。用x86_init.irqs.intr_init()函数对irq_desc、以及中断部分门描述符进行初始化。x86_init是 类型,集成针对x86特定平台的各种初始化工作,包含初始化PCI总线、中断、异常等,其中irqs就是针对中断进行初始化操作。*/ . . (); }/arch/x86/include/asm/x86_init.h
/** * struct x86_init_ops - functions for platform specific setup * */ struct { struct ; struct ; struct ; struct ; }; /** * struct x86_init_irqs - platform specific interrupt setup * @pre_vector_init: init code to run before interrupt vectors * are set up. * @intr_init: interrupt init code * @trap_init: platform specific trap setup */ struct { void (* )(void); void (* )(void); void (* )(void); };/arch/x86/kernel/x86_init.c
/* * The platform setup functions are preset with the default functions * for standard PC hardware. */ struct = { . = { . = , . = , . = , }, }; void (void) { int ; /* Execute any quirks before the call gates are initialised: */ /* 调用init_ISA_irqs()初始化irq_desc[0…NR_IRQS] = {.status = IRQ_DISABLED, .action = NULLL, .depth = 1}。并且对于irq0 … 15, 将其handler = &i8259A_irq_type*/ . . (); (); /* * Cover the whole vector space, no vector can escape * us. (some of these will be overridden and become * 'special' SMP interrupts) */ 调用set_intr_gate()为每根IRQ对应的中断向量号设置了相应的中断门描述符 */ for ( = ; < ; ++) { /* IA32_SYSCALL_VECTOR could be used in trap_init already. */ if (! ( , )) ( , [ - ]); } /* 系统若非I/O APIC,则IRQ3用于8259A从片的级联,故进行特殊处理 */ if (! ) (2, & ); #ifdef CONFIG_X86_32 /* * External FPU? Set up irq13 if so, for * original braindamaged IBM FERR coupling. */ 在处理器集成协处理器,而该协处理器没有浮点处理单元的情况下设置针对浮点计算异常的处理函数fpu_irq,该函数用软件模拟的方法来进行浮点数运算 */ if ( . && ! ) ( , & ); /* 在内核选中CONFIG_4KSTACKS时,为系统中每一CPU设置中断、异常处理函数所需的内核态栈;在没有选中该选项的情况下,irq_ctx_init是个空语句 */ ( ()); #endif } void (void) { int ; 若存在LOCAL_APIC,则对Local APIC模块进行设置 */ #if (CONFIG_X86_64) || (CONFIG_X86_LOCAL_APIC) (); #endif 初始化中断控制器8259A */ -> (0); /* * 16 old-style INTA-cycle interrupts: */ 系统中断老式处理方式是由两片8259A级联而成,每片可有8个IRQ,故两片最多初始化16个IRQ,但由于IRQ 2用于级联,故实际仅有15个有效IRQ。 */ for ( = 0; < -> ; ++) { struct * = ( ); -> = ; -> = ; -> = 1; ( , & , , "XT"); } }interrupt数组
interrupt数组符号由汇编代码定义,其源码如下:
/arch/x86/kernel/entry_32.S
/* * Build the entry stubs and pointer table with some assembler magic. * We pack 7 stubs into a single 32-byte chunk, which will fit in a * single cache line on all modern x86 implementations. */ .section .init.rodata,"a" ENTRY(interrupt) .text .p2align 5 .p2align CONFIG_X86_L1_CACHE_SHIFT ENTRY(irq_entries_start) RING0_INT_FRAME vector=FIRST_EXTERNAL_VECTOR /* =0x20,0~31号内部中断 */ .rept (NR_VECTORS-FIRST_EXTERNAL_VECTOR+6)/7 .balign 32 /* 32字节对齐 */ .rept 7 .if vector < NR_VECTORS .if vector <> FIRST_EXTERNAL_VECTOR /* 按照CFA规则修改前一个offset,以达4字节对齐。CFA标准(Call Frame Information), help a debugger create a reliable backtrace through functions. */ CFI_ADJUST_CFA_OFFSET -4 .endif 1: pushl $(~vector+0x80) /* Note: always in signed byte range ,[-256 ~ -1] */ CFI_ADJUST_CFA_OFFSET 4 /* 按CFA规则4字节对齐 */ .if ((vector-FIRST_EXTERNAL_VECTOR)%7) <> 6 jmp 2f .endif .previous .long 1b .text vector=vector+1 .endif .endr /* end of rep 7 */ 2: jmp common_interrupt .endr /* end of rep (NR_VECTORS – FIRST_...) */ END(irq_entries_start) .previous END(interrupt)ENTRY(interrupt)通过伪指令.rept、.endr,将在代码段产生(NR_VECTORS - FIRST_EXTERNAL_VECTOR)个跳转到common_interrupt的汇编代码片段,起始地址是irq_entries_start;在数据段产生一个中断数组的符号interrupt,用于记录产生代码段中每个中断向量处理的汇编代码片段地址,在C语言中将interrupt符号作为中断数组变量导入:
extern void (* [ - ])(void);ENTRY(interrupt)编译之后,所生成的代码段和数据段内存布局如下:
ENTRIY(interrupt)汇编代码段主要由两个rept构成,外层rept循环(NR_VECTORS-FIRST_EXTERNAL_VECTOR+6)/7次,而每次内层rept循环7次,内层循环所产生的代码以32字节对齐,内层rept循环产生的代码如上图irq_entries_start中以粗黑方框表示。
以两层rept循环生成jmp common_interrupt的目的在于:
在内循环内,前6次循环产生的代码指令为:push和short jmp,而第7次产生的代码指令为:push和long jmp。push占2字节,short jmp占2字节,long jmp占5字节,故采取此种方式内层rept循环7次产生的代码大小为:6 * (2 + 2) + 2 + 5 = 31 字节。而外层循环以32字节对齐,相比于老版本每次都为push和long jmp而言,所以利用short jmp节省了内存开销。参见:
每个中断门描述符在将vector压入后都跳转到common_interrupt进行处理。common_interrupt在保存中断现场之后,跳转到do_IRQ进行中断函数处理,最后调用iret_from_intr进行中断返回、恢复中断上下文。
common_interrupt: addl $-0x80,(%esp) /* Adjust vector into the [-256,-1] range */ SAVE_ALL /* 宏定义,负责完成宏定义中断现场的保护工作 */ TRACE_IRQS_OFF movl %esp,%eax call do_IRQ jmp ret_from_intr ENDPROC(common_interrupt) .macro SAVE_ALL cld /* 清除系统标志寄存器EFLAGS中的方向标志位DF,使%si、%di寄存器的值在每次字符串指令操作后自动+1,使自字符串从低地址到高地址方向处理 */ PUSH_GS pushl %fs CFI_ADJUST_CFA_OFFSET 4 /*CFI_REL_OFFSET fs, 0;*/ pushl %es CFI_ADJUST_CFA_OFFSET 4 /*CFI_REL_OFFSET es, 0;*/ pushl %ds CFI_ADJUST_CFA_OFFSET 4 /*CFI_REL_OFFSET ds, 0;*/ pushl %eax CFI_ADJUST_CFA_OFFSET 4 CFI_REL_OFFSET eax, 0 pushl %ebp CFI_ADJUST_CFA_OFFSET 4 CFI_REL_OFFSET ebp, 0 pushl %edi CFI_ADJUST_CFA_OFFSET 4 CFI_REL_OFFSET edi, 0 pushl %esi CFI_ADJUST_CFA_OFFSET 4 CFI_REL_OFFSET esi, 0 pushl %edx CFI_ADJUST_CFA_OFFSET 4 CFI_REL_OFFSET edx, 0 pushl %ecx CFI_ADJUST_CFA_OFFSET 4 CFI_REL_OFFSET ecx, 0 pushl %ebx CFI_ADJUST_CFA_OFFSET 4 CFI_REL_OFFSET ebx, 0 movl $(__USER_DS), %edx movl %edx, %ds movl %edx, %es movl $(__KERNEL_PERCPU), %edx movl %edx, %fs SET_KERNEL_GS %edx .endmcommon_interrupt在调用do_IRQ之前中断栈内存布局如下: