下面的分析是基于3.6.10内核版本。
中断描述符
硬中断产生之后通过门描述符来寻找中断处理程序的入口。中断描述符表的每个表项由8字节组成,叫做一个门描述符。主要包括三种门描述符:陷阱门、中断门和系统门。
这里看下中断门描述符的结构:
中断门描述主要包括中断如理程序所在的段选择符和段内偏移及一些标志位。低4字节的bit16~bit31为段选择符,bit0~15为段内偏移量的低16位,高4字节的bit31~16为段内偏移量的高16位。
中断描述符初始化
◆在arch/x86/kernel/head_64.S中
.section .bss, "aw", @nobits .align L1_CACHE_BYTES ENTRY(idt_table) .skip IDT_ENTRIES * 16 |
上面的汇编代码的意思是变量idt_table在.bss段内,预留空间大小为256*16字节,并且每个字节的值都为0。
◆在arch/x86/kernel/cpu/common.c中
struct desc_ptr idt_descr = { NR_VECTORS * 16 - 1, (unsigned long) idt_table }; |
IDT的大小为256*16字节,地址在idt_table
◆在arch/x86/kernel/cpu/common.c的cpu_init函数中
load_idt((const struct desc_ptr *)&idt_descr); |
将IDT表加载到IDTR寄存器中。中断描述符表IDT可以驻留在线性地址空间的任何地方,使用IDTR寄存器定位IDT表的位置。
下面看下初始化的整个流程:
1、 在start_kernel函数中会调用trap_init函数来初始化陷阱门和系统调用门,init_IRQ函数来初始化中断门描述符,调用early_irq_init初始化与硬件无关的中断门相关事项,在后面会分析此函数。
2、 init_IRQ函数通过x86_init操作集的intr_init调用native_init_IRQ函数
apic_intr_init(); i = FIRST_EXTERNAL_VECTOR; for_each_clear_bit_from(i, used_vectors, NR_VECTORS) { /* IA32_SYSCALL_VECTOR could be used in trap_init already. */ set_intr_gate(i, interrupt[i - FIRST_EXTERNAL_VECTOR]); } |
apic_intr_init用来初始化与APIC中断控制器相关的中断门描述符的。FIRST_EXTERNAL_VECTOR值为0x20,IDT表中外部中断向量从32开始。在前面trap_init函数中对所有的陷阱门描述符和系统调用门描述符都设置了used_vectors数组中的相应位,剩下没有设置的就是用作外部中断的中断门描述符了。Interrupt数组保存了这些外部中断描述符门的处理程序的地址。
3、alloc_intr_gate函数首先调用alloc_system_vector设置中断向量对应在used_vectors数组中的位,然后调用set_intr_gate函数设置一个中断描述符表项。
static inline void set_intr_gate(unsigned int n, void *addr) { BUG_ON((unsigned)n > 0xFF); _set_gate(n, GATE_INTERRUPT, addr, 0, 0, __KERNEL_CS); } |
首先判断中断向量号是否大于255,然后调用_set_gate函数。
static inline void _set_gate(int gate, unsigned type, void *addr, unsigned dpl, unsigned ist, unsigned seg) { pack_gate(&s, type, (unsigned long)addr, dpl, ist, seg); write_idt_entry(idt_table, gate, &s); } |
pack_gate函数主要是填充中断门描述符表项的8字节的相应位。中断处理程序地址填充为段内偏移,段选择符为__KERNEL_CS,特权级DPL为0,用户态程序不能访问中断门。所有中断程序的访问都必须经过中断门,这样中断的处理就限制在内核态了。
中断入口
在前面中断描述符初始化中看到一般外部硬中断的处理程序地址存放在Interrupt数组中。下面看下Interrupt:(在arch/x86/kernel/entry_64.S中)
.section .init.rodata,"a" ENTRY(interrupt) .section .entry.text .p2align 5 .p2align CONFIG_X86_L1_CACHE_SHIFT ENTRY(irq_entries_start) INTR_FRAME vector=FIRST_EXTERNAL_VECTOR .rept (NR_VECTORS-FIRST_EXTERNAL_VECTOR+6)/7 .balign 32 .rept 7 .if vector < NR_VECTORS .if vector <> FIRST_EXTERNAL_VECTOR CFI_ADJUST_CFA_OFFSET -8 .endif 1: pushq_cfi $(~vector+0x80) //将中断向量号取反加上0x80后压栈 .if ((vector-FIRST_EXTERNAL_VECTOR)%7) <> 6 jmp 2f .endif .previous //使汇编器返回到前面的数据段进行汇编 .quad 1b //存储着标签1处的地址 .section .entry.text vector=vector+1 .endif .endr 2: jmp common_interrupt .endr CFI_ENDPROC END(irq_entries_start)
.previous END(interrupt) .previous |
1、 变量Interrupt数组位于.init.rodata数据段中,变量irq_entries_start位于.entry.text代码段中。
2、 下面来看下irq_entries_start干了些什么。宏INTR_FRAME用来初始化中断时的栈帧状态。Vector初始值为FIRST_EXTERNAL_VECTOR(即为32),在外循环中会重复(NR_VECTORS-FIRST_EXTERNAL_VECTOR+6)/7 = (256 – 32 + 6) / 7 = 32次。
3、 内循环重复7次,宏pushq_cfi使用pushq指令将(~vector+0x80)的值压栈,然后做一些检查之后就跳转到common_interrupt函数。
上面的代码相当于是把NR_VECTORS-FIRST_EXTERNAL_VECTOR个中断处理程序的地址存在变量Interrupt数组开始的数据段中,处理程序的代码存储在变量irq_entries_start开始的代码段中。common_interrupt函数是这些中断处理程序的公共的入口,只是每个中断压栈的参数中断向量号不同而已。
common_interrupt函数分析:
common_interrupt: XCPT_FRAME addq $-0x80,(%rsp) /* Adjust vector to [-256,-1] range */ interrupt do_IRQ /* 0(%rsp): old_rsp-ARGOFFSET */ ret_from_intr: |
common_interrupt函数使用宏interrupt来调用do_IRQ函数,当然在进入do_IRQ函数之前,做了压栈保存现场的操作,以及设置gs寄存器值等。
进入do_IRQ函数之后的处理流程这里就不分析了。