Cited from http://blog.chinaunix.net/u2/73528/showart_1132752.html
linux内核的中断描述符表IDT是一个全局的数据,在i386平台上被定义为:
struct desc_struct idt_table[256] __attribute__((__section__(".data.idt"))) = { {0, 0}, };
(摘自arch/kernel/i386/traps.c)其中每一个表项均是一个desc_struct结构,该结构被定以为:
struct desc_struct {
unsigned long a,b;
};
(摘自/inlcude/asm-i386/processor.h)。可以看出IDT表共256个表项,每一个表项是8个字节,在idt_table数组被定义时静态初始化为0。
关于系统的启动请参见http://blog.chinaunix.net/u2/73528/showart_1132645.html,在setup32_up()函数中,会调用setup_idt()函数初始化IDT,该函数使用AT&T汇编写成,在/arch/i386/kernel/head.S中:
1 setup_idt: 2 lea ignore_int,%edx 3 movl $(__KERNEL_CS << 16),%eax 4 movw %dx,%ax /* selector = 0x0010 = cs */ 5 movw $0x8E00,%dx /* interrupt gate - dpl=0, present */ 6 7 lea idt_table,%edi 8 mov $256,%ecx 9 rp_sidt: 10 movl %eax,(%edi) 11 movl %edx,4(%edi) 12 addl $8,%edi 13 dec %ecx 14 jne rp_sidt 15 ..... 16 ret |
在第2行将ignore_int函数的地址放入edx寄存器中,第三行将段选择符 加载进eax寄存器的高16位中,第4行是将ignore_int()函数地址的低16位放入eax寄存器的低16位(ax)中,这样就在eax寄存器中组成了idt的低4字节的值,也就是赋给desc_struct->a的值。接下来第5行是将0x8E00放入edx寄存器的低16位中,刚才edx中存放的ignore_int()函数的地址,这样就在edx寄存器中形成了idt的高4字节的值,也就是赋给desc_struct->b的值。第7行将idt_table数组的首地址放入edi寄存器中,第8行是初始化循环计数器ecx为256。第9行是把eax中的内容(32字节)放入edi所指的内存中(也就是idt_table[256-(%ecx)]->a),第11行是将edx中的内容放入edi+4所指的内存中(也就是idt_table[256-(%ecx)]->b)。这样就对idt_table[256-(%ecx)]进行了初始化。第12行是给edi加8使其指向idt_table[256-(%ecx)+1],13行自减计数器ecx,在14行判断如果ecx不为0则跳转至rp_sidt处继续初始化idt_table数组的下一个数据项,直到ecx为0,也就是idt_table数组全部被初始化。
以下以图说明:
500)this.width=500;" border=0<
图所示的结构对应idt_table数组中的一个表项,也就是一个idt_desc类型的数据。上面说的idt_struct->a对应0到31共32位,idt_struct->b对应32到63共32位。其中16-31共16位是中断处理程序所在的段选择符,0-15位和48-64位组合起来形成32位偏移量,也就是中断处理程序所在段(由16-31位给出)的段内偏移。40-43位共4位表示描述符的类型,45-46两位标识描述符的访问特权级(DPL,Descriptor Privilege Level),47位标识段是否在内存中。如果为1则表示段当前不再内存中。此处对每一个数据项的初始化为:
500)this.width=500;" border=0<
32到47位就是0x8E00,其中43到40位为1110,表明此次对idt表的初始化是将其全部初始化为中断门描述符,相应的如果是0101则是任务门描述符,1111表示时陷阱门描述符。
47位为1。
要说明的是ignore_int()中断处理程序实际上是一个“空”处理。随后对IDT再次进行初始化的时候,会使其最终初始化为有效的处理程序,也就是在start_kernel()函数中。当系统跳转到start_kernel()函数时,内核才进行真正的初始化。其中调用trap_init()函数对IDT进行最终初始化。调用set_trap_gate()将其初始化为陷阱门,调用set_intr_gate()将其初始化为中断门,调用set_system_gate()将其初始化为访问特权级为3的陷阱门,调用set_task_gate()将其初始化为任务门,调用set_system_intr_gate()将其初始化为访问特权级为3的中断门。
除了set_task_gate(),其它4个函数的接口都一样,行如:
void set_xxx_gate(unsigned int n,void *addr)
{
_set_gate(n,DESCTYPE_XXX,addr,__KERNEL_CS);
}
这样的形式,其中参数n给出要初始化的IDT表项(idt_table数组的下标),addr给出对应的处理程序的地址。在这些函数的内部均是调用_set_gate()。下面就看看_set_gate()函数:
static inline void _set_gate(int gate, unsigned int type, void *addr, unsigned short seg) { __u32 a, b; pack_gate(&a, &b, (unsigned long)addr, seg, type, 0); write_idt_entry(idt_table, gate, a, b); } static inline void pack_gate(__u32 *a, __u32 *b, unsigned long base, unsigned short seg, unsigned char type, unsigned char flags) { *a = (seg << 16) | (base & 0xffff); *b = (base & 0xffff0000) | ((type & 0xff) << 8) | (flags & 0xff); } #define write_idt_entry(dt, entry, a, b) write_dt_entry(dt, entry, a, b) static inline void write_dt_entry(struct desc_struct *dt, int entry, u32 entry_low, u32 entry_high) { dt[entry].a = entry_low; dt[entry].b = entry_high; } |
以上函数均摘自include/asm-i386/desc.h。这些代码的功能和上面摘出的汇编的功能是一样的。首先看_set_gate()函数,参数gate和addr当然是set_xxx_gate()传给的中断号和处理程序的地址,type参数指明要将idt_table[gate]初始化为什么类型的描述符,这些类型被定义为:
#define DESCTYPE_TASK 0x85 /* present, system, DPL-0, task gate */
#define DESCTYPE_INT 0x8e /* present, system, DPL-0, interrupt gate */
#define DESCTYPE_TRAP 0x8f /* present, system, DPL-0, trap gate */
#define DESCTYPE_DPL3 0x60 /* DPL-3 */
这些最终对应每一个IDT表项的32到47位。_set_gate()最后一个参数seg给出了处理程序所在段的段选择符,该4个函数seg实参都为__KERNEL_CS。pack_gate()是将_set_gate()的参数组合为两个无符号的32位数a,b,分贝对应idt_struct结构的两个数据项a,b,这样在write_dt_entry()中就可以使用这两个数对idt_table[gate]初始化了。和以上4个set_xxx_gate()稍有区别的是set_task_gate():
static void __init set_task_gate(unsigned int n, unsigned int gdt_entry)
{
_set_gate(n, DESCTYPE_TASK, (void *)0, (gdt_entry<<3));
}
在trap_init()函数中,set_task_gate()被调用:
set_task_gate(8,GDT_ENTRY_DOUBLEFAULT_TSS);
表示内核有严重的非法操作时才会触发该中断,然后执行doublefault_fn()异常处理函数。
要说明的是trap_init()函数也只是对IDT的前20项(0-19)和128号(SYSCALL_VECTOR))进行了最终初始化,而20到31共12项是系统保留位使用,那么剩下的
23项又是在什么地方初始化?如何初始化的呢?
在start_kernel()调用trap_init()之后会调用init_IRQ()初始话化剩下的223项。
start_kernel()
--->...
--->trap_init()
--->rcu_init()
--->init_IRQ() /*arch/i386/kernel/paravirt.c*/
|--->paravirt_ops.init_IRQ
|--->native_init_IRQ() /*arch/i386/kernel/i8259.c*/
可以看出通过一级一级调用最后调用的是native_init_IRQ()函数,该函数其中的一段是对IDT剩下的223项进行初始化的:
void __init native_init_IRQ(void) { int i; .../*省略*/ for (i = 0; i < (NR_VECTORS - FIRST_EXTERNAL_VECTOR); i++) { int vector = FIRST_EXTERNAL_VECTOR + i; if (i >= NR_IRQS) break; if (vector != SYSCALL_VECTOR) set_intr_gate(vector, interrupt[i]); } ... /*省略*/ } |
可以看出调用set_intr_gate()将剩下的233项全部初始化为中断门。其中的NR_VECTOR为256,FIRST_EXTERNAL_VECTOR为0x20(也就是32),所以循环共进行224次,其中当要初始化的中断号等于SYSCALL_VECTOR(系统调用中断号)时,就跳过,所以也就是前面说的初始化剩下的223项。现在的关键是给这些中断的处理程序是什么?这又要说到interrupt数组了。该数组定义在arch/x86_64/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)
/* for the irq vectors */ static void (*interrupt[NR_VECTORS - FIRST_EXTERNAL_VECTOR])(void) = { 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), IRQLIST_16(0xe), IRQLIST_16(0xf) }; |
现在结合native_init_IRQ()分析一下。当进行第一次循环也就是i=0的时候,vector=32,所以set_intr_gate()的调用是:
set_intr_gate(32,interrupt[0]);
对应的是:
IRQLIST_16(0x2)
--->IRQ(0x2,0)
|--->IRQ0x20_interrupt()
这样就将IRQ0x20_interrupt()设置为0x20中断的处理程序了。
下来我们看看这些IRQn_interrupt()(n=0x20~0xff)是如何建立的。
#define BUILD_IRQ(nr) \
asmlinkage void IRQ_NAME(nr); \
__asm__( \
"\n.p2align\n" \
"IRQ" #nr "_interrupt:\n\t" \
"push $~(" #nr ") ; " \
"jmp common_interrupt");
common_interrupt是一个汇编标号,在它里面会调用do_IRQ()函数去处理中断。这里又要说到irq_desc数组了。
从注册中断的角度来分析,编程人员在内核模块里调用request_irq()函数注册一个中断,那么最终中断处理程序被注册在irq_desc数组里了。该数组大小是256个,每一个都是
struct irq_desc类型的。关于struct irq_desc类型具体参见/include/linux/irq.h。
从中断处理过程来讲,当一个中断n产生后,代码执行跳转到IDT(idt_table[n])处,取出”中断处理程序“(并非都是真正的中断处理程序)段基址和段内偏移,组合成为”中断处理程序”入口地址并跳到此处执行。如果0x00<=n<0x20,那么该地址就是真正的中断处理程序,执行之后就从中断返回。如果0x20<=n<=255,则跳转到IRQn_interrupt处,继而跳转到connon_interrupt处,调用do_IRQ()执行中断处理。这里大概说一下do_IRQ()的处理过程。
中断处理进入do_IRQ()函数后,如果给出的中断号是合法(irq<0xff)的,则调用 generic_handle_irq(irq);贴出generic_handle_irq()函数的代码:
static inline void generic_handle_irq(unsigned int irq) { struct irq_desc *desc = irq_desc + irq; #ifdef CONFIG_GENERIC_HARDIRQS_NO__DO_IRQ desc->handle_irq(irq, desc); #else if (likely(desc->handle_irq)) desc->handle_irq(irq, desc); else __do_IRQ(irq); #endif } |
可以看出如果irq_desc[n]->handle_irq为空则调用__do_IRQ()函数处理。要说明的是该数据项在静态初始化的时候是不为空的,一旦注册了一个真正的中断,则该项就为空了,所以是调用__do_IRQ()执行中断处理。在__do_IRQ()函数里,才真正调用注册在irq_desc[n]里的中断处理程序来处理中断。
到此,从中断的初始化,中断的注册,中断的处理过程都基本讲了一遍,有许多地方都是我的理解,所以正确率就一般了,呵呵!