2.5 异常处理类中断服务程序挂接
不论是用户进程还是系统内核都要经常使用中断或遇到很多异常情况需要处理,如CPU在参与运算过程中,可能会遇到除零错误、溢出错误、边界检查错误、缺页错误……免不了需要“异常处理”。中断技术也是广泛使用的,系统调用就是利用中断技术实现的。这些中断、异常都需要具体的服务程序来执行。trap_init()函数将中断、异常处理的服务程序与IDT进行挂接,逐步重建中断服务体系,支持内核、进程在主机中的运算。挂接的具体过程及异常处理类中断服务程序在IDT中所占用的位置如图2-6所示。
执行代码如下:
//代码路径:init/main.c:
void main(void)
{
…
trap_init();
…
}
//代码路径:kernel/traps.c:
void trap_init(void)
{
int i;
set_trap_gate(0,÷_error);//除零错误
set_trap_gate(1,&debug); //单步调试
set_trap_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); //无效TSS
set_trap_gate(11,&segment_not_present); //段不存在
set_trap_gate(12,&stack_segment); //栈异常
set_trap_gate(13,&general_protection); //一般性保护异常
set_trap_gate(14,&page_fault); //缺页
set_trap_gate(15,&reserved); //保留
set_trap_gate(16,&coprocessor_error); //协处理器错误
for (i=17;i<48;i++) //都先挂接好,中断服务程序函数名初
//始化为保留
set_trap_gate(i,&reserved);
set_trap_gate(45,&irq13); //协处理器
outb_p(inb_p(0x21)&0xfb,0x21); //允许IRQ2中断请求
outb(inb_p(0xA1)&0xdf,0xA1); // 允许IRQ2中断请求
set_trap_gate(39,¶llel_interrupt); //并口
}
//代码路径:include\asm\system.h:
…
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__("movw %%dx,%%ax\n\t" \ //将edx的低字赋值给eax的低字
"movw %0,%%dx\n\t" \ //%0对应第二个冒号后的第1行的"i"
"movl %%eax,%1\n\t" \ //%1对应第二个冒号后的第2行的"o"
"movl %%edx,%2" \ //%2对应第二个冒号后的第3行的"o"
: \ //这个冒号后面是输出,下面冒号后面
//是输入
: "i" ((short) (0x8000 + (dpl<<13) + (type<<8))), \ //立即数
"o" (*((char *) (gate_addr))), \ //中断描述符前4个字节的地址
"o" (*(4 + (char *) (gate_addr))), \ //中断描述符后4个字节的地址
"d" ((char *) (addr)),"a" (0x00080000)) //"d"对应edx,"a"对应eax
…
#define set_trap_gate(n,addr) \
_set_gate(&idt[n],15,0,addr)
这些代码的目的就是要拼出第1章1.3.5节讲述过的中断描述符。为了便于阅读,复制在下面,如图2-7所示。
上述代码的执行效果如图2-8所示。
对比:
set_trap_gate(0,÷_error)
set_trap_gate(n,addr)
_set_gate(&idt[n],15,0,addr)
_set_gate(gate_addr,type,dpl,addr)
可以看出,n是0;gate_addr是&idt[0],也就是idt的第一项中断描述符的地址;type是15;dpl(描述符特权级)是0;addr是中断服务程序divide_error(void)的入口地址,如图2-9所示。
“movw %%dx,%%axnt”是把edx的低字赋值给eax的低字;edx是(char ) (addr),也就是÷_error;eax的值是0x00080000,这个数据在head.s中就提到过,8应该看成1000,每一位都有意义,这样eax的值就是0x00080000 + ((char )(addr)的低字),其中的0x0008是段选择符,含义与第1章中讲解过的“jmpi 0,8”中的8一致。
"movw %0,%%dxnt”是把(short) (0x8000 + (dpl<<13) + (type<<8))赋值给dx。别忘了,edx是(char *) (addr),也就是÷_error。
因为这部分数据是按位拼接的,必须计算精确,我们耐心详细计算一下:
0x8000就是二进制的1000 0000 0000 0000;
dpl是00,dpl<<13就是000 0000 0000 0000;
type是15,type<<8就是1111 0000 0000;
加起来就是1000 1111 0000 0000,这就是dx的值。edx的计算结果就是(char *) (addr) 的高字即÷_error的高字 + 1000 1111 0000 0000。
"movl %%eax,%1nt”是把eax的值赋给((char ) (gate_addr)),就是赋给idt[0]的前4字节。同理,"movl %%edx,%2”是把edx的值赋给(4 + (char ) (gate_addr)),就是赋给idt[0]的前后4字节。8字节合起来就是完整的idt[0]。拼接的效果如图2-10所示。
IDT中的第一项除零错误中断描述符初始化完毕,其余异常处理服务程序的中断描述符初始化过程大同小异。后续介绍的所有中断服务程序与IDT的初始化基本上都是以这种方式进行的。
set_system_gate(n,addr)与set_trap_gate(n,addr)用的_set_gate(gate_addr,type,dpl,addr)是一样的;差别是set_trap_gate的dpl是0,而set_system_gate的dpl是3。dpl为0的意思是只能由内核处理,dpl为3的意思是系统调用可以由3特权级(也就是用户特权级)
调用。
有关特权级更深入的内容,请参看《Intel IA-32 Architectures Software Developer’s Manual Volume 3.pdf》(可以到Intel官方网站下载)。
接下来将IDT的int 0x11~int 0x2F都初始化,将IDT中对应的指向中断服务程序的指针设置为reserved(保留)。
设置协处理器的IDT项。
允许主8259A中断控制器的IRQ2、IRQ3的中断请求。
设置并口(可以接打印机)的IDT项。
32位中断服务体系是为适应“被动响应”中断信号机制而建立的。其特点、技术路线是这样的:一方面,硬件产生信号传达给8259A,8259A对信号进行初步处理并视CPU执行情况传递中断信号给CPU;另一方面,CPU如果没有接收到信号,就不断地处理正在执行的程序,如果接收到信号,就打断正在执行的程序并通过IDT找到具体的中断服务程序,让其执行,执行完后,返回刚才打断的程序点继续执行。如果又接收到中断信号,就再次处理中断……
最原始的设计不是这样,那时候CPU每隔一段时间就要对所有硬件进行轮询,以检测它的工作是否完成,如果没有完成就继续轮询,这样就消耗了CPU处理用户程序的时间,降低了系统的综合效率。可见,CPU以“主动轮询”的方式来处理信号是非常不划算的。以“被动响应”模式替代“主动轮询”模式来处理主机与外设的I/O问题,是计算机历史上的一大进步。