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数组全部被初始化。

以下以图说明:


IDT表的初始化 - lagignition - lagignition 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则表示段当前不再内存中。此处对每一个数据项的初始化为:


IDT表的初始化 - lagignition - lagignition 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]里的中断处理程序来处理中断。

到此,从中断的初始化,中断的注册,中断的处理过程都基本讲了一遍,有许多地方都是我的理解,所以正确率就一般了,呵呵!


Logo

更多推荐