分类: LINUX
2012-05-27 18:58:58
好了,现在,我们知道了80x86微处理器在硬件级对中断和异常做了些什么,接下来,我们继续关注的是如何初始化中断描述符表。
内核启用中断以前,必须把IDT表的初始地址装到idtr寄存器,并初始化表中的每一项。这项工作是在初始化系统时完成的。
int指令允许用户态进程发出一个中断信号,其值可以是0-255的任意一个向量。因此,为了防止用户通过int指令模拟非法的中断和异常,IDT的初始
化必须非常小心。这可以通过把中断或陷阱门描述符的DPL字段设置成0来实现。如果进程试图发出其中的一个中断信号,控制单元将会发现CPL的值与DPL
字段有冲突,并且产生一个"General protection”异常。
然而,在少数情况下,用户态进程必须能发出一个编程异常。为此,只要把中断或陷阱门描述符的DPL字段设置成3,即特权级尽可能一样高就足够了。
现在,让我们来看一下Linux是如何实现这种策略的。
我们先回忆一下前一篇博文的“中断描述符表”,Intel提供了三种类型的中断描述符:任务门、中断门及陷阱门描述符。Linux使用与Intel稍有不同的细目分类和术语,把它们如下进行分五类:
中断门(interrupt gate):用户态的进程不能访问Intel中断门(门的DPL字段为0)。所有的Linux中断处理程序都通过中断门激活,并全部限制在内核态。
系统门(system gate):用户态的进程可以访问Intel陷阱门(门的DPL字段为3)。通过系统门来激活三个Linux异常处理程序,它们的向量是4,5及128,因此,在用户态下,可以发布into、bound及int $0x80三条汇编语言指令。
系统中断门(system interrupt gate):能够被用户态进程访问的Intel中断门(门的DPL字段为3)。与向量3相关的异常处理程序是由系统中断门激活的,因此,在用户态可以使用汇编语言指令int3。
陷阱门(trap gate):用户态的进程不能访问的一个Intel陷阱门(门的DPL字段为0)。大部分Linux异常处理程序都通过陷阱门来激活。
任务门(task gate):不能被用户态进程访问的Intel任务门(门的DPL字段为0)。Linux对“Double fault”异常的处理程序是由任务门激活的。
下列体系结构相关的函数用来在IDT中插入门:
set_intr_gate(n,addr)
在IDT的第n个表项插入一个中断门。门中的段选择符设置成内核代码的段选择符,偏移量设置为中断处理程序的地址addr,DPL字段设置为0。
set_system_gate(n,addr)
在IDT的第n个表项插入一个陷阱门。门中的段选择符设置成内核代码的段选择符,偏移量设置为异常处理程序的地址addr,DPL字段设置为3。
set_system_intr_gate(n,addr)
在IDT的第n个表项插入一个中断门。门中的段选择符设置成内核代码的段选择符,偏移量设置为异常处理程序的地址addr,DPL字段设置为3。
set_trap_gate(n,addr)
与前一个函数类似,只不过DPL的字段设置成0。
set_task_gate(n,gdt)
在IDT的第n个表项中插入一个中断门。门中的段选择符中存放一个TSS的全局描述符表的指针,该TSS中包含要被激活的函数。偏移量设置为0,而DPL字段设置为3。
当计算机还运行在实模式时,IDT就被初始化并由BIOS例程使用。然而,一旦Linux接管,IDT就被移到RAM主存的另一个区域,并进行第二次初始化,因为Linux没有利用任何BIOS的例程。
在源代码中,IDT存放在idt_table表中,有256个表项。6字节的idt_descr变量指定了IDT的大小和它的地址,只有当内核用lidt汇编指令初始化idtr寄存器时才用到这个变量(回忆一下,idtr寄存器存放的是IDT的基址)。
在内核初始化过程中,setup_idt()汇编语言函数用同一个中断门(即指向ignore_int()中断处理程序)来填充所有这256个idt_table表项:
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
ret
用汇编语言写成的ignore_int()中断处理程序,可以看作一个空的处理程序,它执行下列动作:
1. 在栈中保存一些寄存器的内容。
2. 调用printk()函数打印“Unknown interrupt”系统消息。
3. 从栈恢复寄存器的内容。
4. 执行iret指令以恢复被中断的程序。
ignore_int()处理程序应该从不被执行。如果在控制台或日志文件中出现的“Unknown
interrupt”消息,则标志着要么是出现了一个硬件的问题(一个I/O设备正在产生没有预料到的中断),要么就是出现了一个内核的问题(一个中断或
异常未被适当地处理)。
紧接着这个预初始化,内核将在IDT中进行第二遍初始化,用有意义的陷阱和中断处理程序替换这个空处理程序。一旦这个过程完成,对控制单元产生的每个不同
的异常,IDT都有一个专门的陷阱或系统门,而对于可编程中断控制器确认的每一个IRQ,IDT都将包含一个专门的中断门。
好了,现在有了对中断硬件环境的了解,以及在得到一个空的IDT的表以后,接下来的博文中,将分别针对异常和中断来详细地说明这个工作是如何完成的。随后
将分别为中断和异常举一个实例,一个是进程调度的“心脏”——时钟中断,一个是虚拟存储的核心内容——缺页异常。敬请期待!