第2节 中断描述符表的初始化
推荐给好友
打印
加入收藏
更新于2009-06-14 07:52:54

3.2. 1 外部中断向量的设置

前面我们已经提到,Linux把向量0~31分配给异常和非屏蔽中断,而把32~47之间的向量分配给可屏蔽中断,可屏蔽中断的向量是通过对中断控制器的编程来设置的。前面介绍了8259A中断控制器,下面我们通过对其初始化的介绍,来了解如何设置中断向量。

8259A通过两个端口来进行数据传送,对于单块的8259A或者是级连中的8259A_1来说,这两个端口是0x20和0x21。对于8259A_2来说,这两个端口是0xA0和0xA1。8259A有两种编程方式,一是初始化方式,二是工作方式。在操作系统启动时,需要对8959A做一些初始化工作,这就是初始化方式编程。

先简单介绍一下8259A内部的四个中断命令字(ICW)寄存器的功能,它们都是用来启动初始化编程的:

ICW1:初始化命令字。

ICW2:中断向量寄存器,初始化时写入高五位作为中断向量的高五位,然后在中断响应

时由8259根据中断源(哪个管脚)自动填入形成完整的8位中断向量(或叫中断类

型号)。

ICW3: 8259的级连命令字,用来区分主片和从片。

ICW4:指定中断嵌套方式、数据缓冲选择、中断结束方式和CPU类型。

8259A初始化的目的是写入有关命令字,8259A内部有相应的寄存器来锁存这些命令字,以控制8259A工作。有关的硬件知识笔者就不详细描述了,请读者查阅有关可编程中断控制器的资料,我们只具体把Linux对8259A的初始化讲解一下,代码在/arch/i386/kernel/i8259.c的函数init_8259A()中:

outb(0xff, 0x21); /* 送数据到工作寄存器OCW1(又称中断屏蔽字),
屏蔽所有外部中断, 因为此时系统尚未初始化完毕,
outb(0xff, 0xA1); 不能接收任何外部中断请求 */
outb_p(0x11, 0x20); /*送0x11到ICW1(通过端口0x20),启动初始化编程。
0x11表示外部中断请求信号为上升沿有效,系统中有
多片8295A级连,还表示要向ICW4送数据*/
outb_p(0x20 + 0, 0x21); /* 送0x20到ICW2,写入高五位作为中断向量的高五
位,低3位根据中断源(管脚)填入中断号0~7,
因此把IRQ0-7映射到向量0x20-0x27 */
outb_p(0x04, 0x21); /* 送0x04到ICW3,ICW3是8259的级连命令字,
0x04表示8259A-1是主片 */
outb_p(0x11, 0xA0); /* 用ICW1初始化 8259A-2 */
outb_p(0x20 + 8, 0xA1); /* 用ICW2把8259A-2的IRQ0-7映射到 0x28-0x2f */
outb_p(0x02, 0xA1); /* 送0x04到ICW3。表示8259A-2 是从片,
并连接在8259A_1的2号管脚上*/
outb_p(0x01, 0xA1); /* 把0x01送到ICW4 */

最后一句有四方面含义:① 中断嵌套方式为一般嵌套方式。当某个中断正在服务时,本级中断及更低级的中断都被屏蔽,只有更高级的中断才能响应。注意,这对于多片8259A级连的中断系统来说,当某从片中一个中断正在服务时,主片即将这个从片的所有中断屏蔽,所以此时即使本片有比正在服务的中断级别更高的中断源发出请求,也不能得到响应,即不能中断嵌套。② 8259A数据线和系统总线之间不加三态缓冲器。一般来说,只有级连片数很多时才用到三态缓冲器;③ 中断结束方式为正常方式(非自动结束方式)。即在中断服务结束时(中断服务程序末尾),要向8259A芯片发送结束命令字EOI(送到工作寄存器OCW2中),于是中断服务寄存器ISR中的当前服务位被清0,EOI命令字的格式有多种,在此不详述;④ CPU类型为x86系列。

outb_p()函数就是把第一个操作数拷贝到由第二个操作数指定的I/O端口,并通过一个空操作而产生一个暂停。

这里介绍了8259A初始化的主要工作。最后要说明的是:IBM PC机的BIOS中固化有对中断控制器的初始化程序段,在计算机加电时,这段程序自动执行,读者感兴趣可以查阅资料看看它的源代码。典型的PC机将外部中断的中断向量分配为:08H~0FH,70H~77H。但是Linux对8259A作了重新初始化,修改了外部中断的中断向量的分配(20H~2FH),使中断向量的分配更加合理。

3.2.2中断描述符表IDT的预初始化

当计算机运行在实模式时,IDT被初始化并由BIOS使用。然而,一旦真正进入了Linux内核,IDT就被移到内存的另一个区域,并进行进入实模式的初步初始化。

1.中断描述表寄存器IDTR的初始化

用汇编指令LIDT对中断向量表寄存器IDTR进行初始化,其代码在arch/i386/boot/setup.S 中:

lidt idt_48 # load idt with 0,0

idt_48:
.word 0 # idt limit = 0
.word 0, 0 # idt base = 0L

2.把IDT表的起始地址装入IDTR

用汇编指令LIDT装入IDT的大小和它的地址(在arch/i386/kernel/head.S中):

#define IDT_ENTRIES 256
.globl SYMBOL_NAME(idt)
lidt idt_descr

idt_descr:
.word IDT_ENTRIES*8-1 # idt contains 256 entries
SYMBOL_NAME(idt):
.long SYMBOL_NAME(idt_table)

其中idt为一个全局变量,内核对这个变量的引用就可以获得IDT表的地址。表的长度为256´8=2048字节。

3.用setup_idt()函数填充idt_table表中的256个表项。

我们首先要看一下idt_table的定义(在arch/i386/kernel/traps.c中): 

struct desc_struct idt_table[256] __attribute__((__section__(".data.idt"))) = { {0, 0}, };
desc_struct结构定义为:
struct desc_struct {
unsigned long a,b }

对idt_table变量还定义了其属性(__attribute__),__section__是汇编中的“节”,指定了idt_table的起始地址存放在数据节的idt变量中,如上面第2条所述。

在对idt_table表进行填充时,使用了一个空的中断处理程序ignore_int()。因为现在处于初始化阶段,还没有任何中断处理程序,因此用这个空的中断处理程序填充每个表项。ignore_int()是一段汇编程序(在head.S中):

ignore_int:
cld #方向标志清0,表示串指令自动增长它们的索引寄存器(esi和edi)
pushl %eax
pushl %ecx
pushl %edx
pushl %es
pushl %ds
movl $(__KERNEL_DS),%eax
movl %eax,%ds
movl %eax,%es
pushl $int_msg
call SYMBOL_NAME(printk)
popl %eax
popl %ds
popl %es
popl %edx
popl %ecx
popl %eax
iret
int_msg:
.asciz "Unknown interrupt\n"
ALIGN

该中断处理程序模仿一般的中断处理程序,执行如下操作:

· 在栈中保存一些寄存器的值
· 调用printk()函数打印“Unknown interrupt”系统信息
· 从栈中恢复寄存器的内容
· 执行iret指令以恢复被中断的程序。

实际上,ignore_int()处理程序应该从不执行。如果在控制台或日志文件中出现了 “Unknown interrupt”消息,说明要么是出现了一个硬件问题(一个I/O设备正在产生没有预料到的中断),要么就是出现了一个内核问题(一个中断或异常未被恰当地处理)。

最后,我们来看setup_idt()函数如何对IDT表进行填充:

/*
* 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.
*/
setup_idt:
lea ignore_int,%edx /*计算ignore_int地址的偏移量,并将其装入%edx*/
movl $(__KERNEL_CS << 16),%eax /* selector = 0x0010 = cs */
movw %dx,%ax
movw $0x8E00,%dx /* interrupt gate - dpl=0, present */
lea SYMBOL_NAME(idt_table),%edi
mov $256,%ecx
rp_sidt:
movl %eax,(%edi)
movl %edx,4(%edi)
addl $8,%edi
dec %ecx
jne rp_sidt
ret

这段程序的理解要对照门描述符的格式。8个字节的门描述符放在两个32位寄存器eax和edx,如图3.5所示,从rp_sidt开始的那段程序是循环填充256个表项。

图3.4 门描述符存放在两个32位的寄存器

3.2.3 中断向量表的最终初始化

在对中断描述符表进行预初始化后, 内核将在启用分页功能后对IDT进行第二遍初始化,也就是说,用实际的陷阱和中断处理程序替换这个空的处理程序。一旦这个过程完成,对于每个异常,IDT都由一个专门的陷阱门或系统门,而对每个外部中断,IDT都包含专门的中断门。

1. IDT表项的设置

IDT表项的设置是通过_set_gaet()函数实现的,这与IDT表的预初始化比较相似,但这里使用的是嵌入式汇编,因此,理解起来比较困难。在此,我们给出函数源码(在traps.c中)及其解释:

#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)

这是一个带参数的宏定义,其中,gate_addr 是门的地址,type为门类型,dpl为请求特权级,addr为中断处理程序的地址。对这段嵌入式汇编代码的说明如下:

· 输出部分有4个变量,分别与%1、%2、%3及%4相结合,其中,%0与gate_addr结合,1%与(gate_aggr+1)结合,这两个变量都存放在内存;2%与局部变量__d0结合,存放在eax寄存器中;3%与__d1结合,存放在edx寄存器中。

· 输入部分有3个变量。由于输出部分已定义了0%~3%,因此,输入部分的第一个变量为4%,其值为“0x8000+(dpl<<13)+(type<<8”,而后面两个变量分别等价于输出部分的%3(edx)和2%(eax),其值分别为“addr”和“__KERNEL_CS << 16”

· 有了参数的这种对照关系,再参考前面的set_idt()函数,就不难理解那4条mov语句了。

下面我们来看如何调用_set_get()函数来给IDT插入门:

void set_intr_gate(unsigned int n, void *addr)
{
_set_gate(idt_table+n,14,0,addr);
}

在第 n个表项中插入一个中断门。这个门的段选择符设置成代码段的选择符(__KERNEL_CS),DPL域设置成0,14表示D标志位为1而类型码为110,所以set_intr_gate()设置的是中断门,偏移域设置成中断处理程序的地址addr。

static void __init set_trap_gate(unsigned int n, void *addr)
{
_set_gate(idt_table+n,15,0,addr);
}

在第 n个表项中插入一个陷阱门。这个门的段选择符设置成代码段的选择符,DPL域设置成0,15表示D标志位为1而类型码为111,所以set_trap_gate()设置的是陷阱门,偏移域设置成异常处理程序的地址addr。

static void __init set_system_gate(unsigned int n, void *addr)
{
_set_gate(idt_table+n,15,3,addr);
}

在第 n个表项中插入一个系统门。这个门的段选择符设置成代码段的选择符,DPL域设置成3,15表示D标志位为1而类型码为111,所以set_system_gate()设置的也是陷阱门,但因为DPL为3,因此,系统调用在用户空间可以通过“INT0X80”顺利穿过系统门,从而进入内核空间。

2.对陷阱门和系统门的初始化

trap_init()函数就是设置中断描述符表开头的19个陷阱门,如前所说,这些中断向量都是CPU保留用于异常处理的:

set_trap_gate(0,&divide_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_intr_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);

在对陷阱门及系统门设置以后,我们来看一下中断门的设置。

3.中断门的设置

下面介绍的相关代码均在arch/I386/kernel/i8259.c文件中,其中中断门的设置是由init_IRQ( )函数中的一段代码完成的:

for (i = 0; i< NR_IRQS; i++) {
int vector = FIRST_EXTERNAL_VECTOR + i;
if (vector != SYSCALL_VECTOR)
set_intr_gate(vector, interrupt[i]);

其含义比较明显:从FIRST_EXTERNAL_VECTOR开始,设置NR_IRQS个IDT表项。常数FIRST_EXTERNAL_VECTOR定义为0x20,而NR_IRQS则为224,即中断门的个数。注意,必须跳过用于系统调用的向量0x80,因为这在前面已经设置好了。

这里,中断处理程序的入口地址是一个数组interrupt[],数组中的每个元素是指向中断处理函数的指针。我们来看一下编码的作者如何巧妙地避开了繁琐的文字录入,而采用统一的方式处理多个函数名。

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)

其中,“##”的作用是把字符串连接在一起。经过gcc预处理,IRQLIST_16(0x0)被替换为IRQ0x00_interrupt,IRQ0x01_interrupt,IRQ0x02_interrupt…IRQ0x0f_interrupt。

到此为止,我们已经介绍了15个陷阱门、4个系统门和16个中断门的设置。内核代码中还有对其它中断门的设置,在此就不一一介绍。




上一节   下一节

相关链接


 
关于我们 | 诚邀加盟 | 客户服务 | 相关法律 | 网站地图 | 友情链接 | 服务信箱:service@eefocus.com
© 2006 与非门科技信息咨询(北京)有限公司 All Rights Reserved.