第5章 中断和异常Word下载.docx
《第5章 中断和异常Word下载.docx》由会员分享,可在线阅读,更多相关《第5章 中断和异常Word下载.docx(47页珍藏版)》请在冰点文库上搜索。
第一级(称主片)的第二个中断请求输入端,与第二级8259A(称从片)的中断输出端INT相连,如图5.1所示。
我们把与中断控制器相连的每条线叫做中断线,要使用中断线,就要进行中断线的申请,也就是IRQ(InterruptReQuirement),因此我们也常把申请一条中断线称为申请一个IRQ或者是申请一个中断号。
IRQ线是从0开始顺序编号的;
因此,第一条IRQ线通常表示成IRQ0。
IRQn的缺省向量是n+32;
如前所述,IRQ和向量之间的映射可以通过中断控制器端口来修改。
图5.1级连的8259A的中断机构
并不是每个设备都可以向中断线上发中断信号的,只有对某一条确定的中断线拥有了控制权,才可以向这条中断线上发送信号。
由于计算机的外部设备越来越多,所以15条中断线已经不够用了,中断线是非常宝贵的资源,所以只有当设备需要中断的时候才申请占用一个IRQ,或者是在申请IRQ时采用共享中断的方式,这样可以让更多的设备使用中断。
对于外部I/O请求的屏蔽可分为两种情况,一种是从CPU的角度,也就是清除eflag的中断标志位(IF),当IF=0时,禁止任何外部I/O的中断请求,即关中断;
一种是从中断控制器的角度,因为中断控制器中有一个8位的中断屏蔽寄存器,每位对应8259A中的一条中断线,如果要禁用某条中断线,则把中断屏蔽寄存器相应的位置1,要启用,则置0。
5.1.3异常及非屏蔽中断
异常就是CPU内部出现的中断,也就是说,在CPU执行特定指令时出现的非法情况。
非屏蔽中断就是计算机内部硬件出错时引起的异常情况。
从上图可以看出,二者与外部I/O接口没有任何关系。
Intel把非屏蔽中断作为异常的一种来处理,因此,后面所提到的异常也包括了非屏蔽中断。
在CPU执行一个异常处理程序时,就不再为其他异常或可屏蔽中断请求服务,也就是说,当某个异常被响应后,CPU清除eflag的中IF位,禁止任何可屏蔽中断。
但如果又有异常产生,则由CPU锁存(CPU具有缓冲异常的能力),待这个异常处理完后,才响应被锁存的异常。
我们这里讨论的异常中断向量在0~31之间,不包括系统调用(中断向量为0x80)。
Intelx86处理器发布了大约20种异常(具体数字与处理器模式有关)。
Linux内核必须为每种异常提供一个专门的异常处理程序。
5.1.4中断描述符表
在实地址模式中,CPU把内存中从0开始的1K字节作为一个中断向量表。
表中的每个表项占四个字节,由两个字节的段地址和两个字节的偏移量组成,这样构成的地址便是相应中断处理程序的入口地址。
但是,在保护模式下,由四字节的表项构成的中断向量表显然满足不了要求。
这是因为,除了两个字节的段描述符,偏移量必用四字节来表示;
要有反映模式切换的信息。
因此,在保护模式下,中断向量表中的表项由8个字节组成,如图5.2所示,中断向量表也改叫做中断描述符表IDT(InterruptDescriptorTable)。
其中的每个表项叫做一个门描述符(gatedescriptor),“门”的含义是当中断发生时必须先通过这些门,然后才能进入相应的处理程序。
图5.2门描述符的一般格式
其中类型占3位,表示门描述符的类型,主要门描述符为:
(1)中断门(Interruptgate)
其类型码为110,中断门包含了一个中断或异常处理程序所在段的选择符和段内偏移量。
当控制权通过中断门进入中断处理程序时,处理器清IF标志,即关中断,以避免嵌套中断的发生。
中断门中的请求特权级(DPL)为0,因此,用户态的进程不能访问Intel的中断门。
所有的中断处理程序都由中断门激活,并全部限制在内核态。
(2)陷阱门(Trapgate)
其类型码为111,与中断门类似,其唯一的区别是,控制权通过陷阱门进入处理程序时维持IF标志位不变,也就是说,不关中断。
(3)系统门(Systemgate)
这是Linux内核特别设置的,用来让用户态的进程访问Intel的陷阱门,因此,门描述符的DPL为3。
系统调用就是通过系统门进入内核的。
最后,在保护模式下,中断描述符表在内存的位置不再限于从地址0开始的地方,而是可以放在内存的任何地方。
为此,CPU中增设了一个中断描述符表寄存器IDTR,用来存放中断描述符表在内存的起始地址。
中断描述符表寄存器IDTR是一个48位的寄存器,其低16位保存中断描述符表的大小,高32位保存中断描述符表的基址,如图5.3所示。
32位基址界限
4716150图5.3中断描述符表寄存器IDTR
5.1.5相关汇编指令
为了有助于读者对中断实现过程的理解,下面介绍几条相关的汇编指令:
(1)调用过程指令CALL
指令格式:
CALL过程名
说明:
在取出CALL指令之后及执行CALL指令之前,使指令指针寄存器EIP指向紧接CALL指令的下一条指令。
CALL指令先将EIP值压入栈内,再进行控制转移。
当遇到RET指令时,栈内信息可使控制权直接回到CALL的下一条指令
(2)调用中断过程的指令INT
INT中断向量
EFLAG、CS及EIP寄存器被压入栈内。
控制权被转移到由中断向量指定的中断处理程序。
在中断处理程序结束时,IRET指令又把控制权送回到刚才执行被中断的地方。
(3)中断返回指令IRET
IRET
IRET与中断调用过程相反:
它将EIP、CS及EFLAGS寄存器内容从栈中弹出,并将控制权返回到发生中断的地方。
IRET用在中断处理程序的结束处。
(4)加载中断描述符表的指令LIDT
格式:
LIDT48位的伪描述符
LIDT将指令中给定的48位伪描述符装入中断描述符寄存器IDTR。
5.2中断描述符表的初始化
通过上面的介绍,我们知道了Intel微处理器对中断和异常所做的工作。
下面,我们从操作系统的角度来对中断描述符表的初始化给予描述。
Linux内核在系统的初始化阶段要进行大量的初始化工作,其与中断相关的工作有:
初始化可编程控制器8259A;
将中断描述符表的起始地址装入IDTR寄存器,并初始化表中的每一项。
这些操作的完成将在本节进行具体描述。
用户进程可以通过INT指令发出一个中断请求,其中断请求向量在0~255之间。
为了防止用户使用INT指令模拟非法的中断和异常,必须对中断描述符表进行谨慎的初始化。
其措施之一就是将中断门或陷阱门中的请求特权级DPL域置为0。
如果用户进程确实发出了这样一个中断请求,CPU会检查出其当前特权级CPL(3)与所请求的特权级DPL(0)有冲突,因此产生一个“通用保护”异常。
但是,有时候必须让用户进程能够使用内核所提供的功能(比如系统调用),也就是说从用户态进入内核态,这可以通过把中断门或陷阱门的DPL域置为3来达到。
当计算机运行在实模式时,中断描述符表被初始化,并由BIOS使用。
然而,一旦真正进入了Linux内核,中断描述符表就被移到内存的另一个区域,并为进入保护模式进行预初始化:
用汇编指令LIDT对中断向量表寄存器IDTR进行初始化,即把IDTR置为0。
把中断描述符表IDT的起始地址装入IDTR
用setup_idt()函数填充中断描述表中的256个表项。
在对这个表进行填充时,使用了一个空的中断处理程序。
因为现在处于初始化阶段,还没有任何中断处理程序,因此,用这个空的中断处理程序填充每个表项。
在对中断描述符表进行预初始化后,内核将在启用分页功能后对IDT进行第二遍初始化,也就是说,用实际的陷阱和中断处理程序替换这个空的处理程序。
一旦这个过程完成,对于每个异常,IDT都由一个专门的陷阱门或系统门,而对每个外部中断,IDT都包含专门的中断门。
5.2.1IDT表项的设置
IDT表项的设置是通过_set_gaet()函数实现的,在此,我们给出如何调用该函数在IDT表中插入一个门:
(1)插入一个中断门
voidset_intr_gate(unsignedintn,void*addr)
{
_set_gate(idt_table+n,14,0,addr);
}
其中,idt_table是中断描述符表IDT在程序中的符号表示,n表示在第n个表项中插入一个中断门。
这个门的段选择符设置成代码段的选择符,DPL域设置成0,14表示D标志位为1(表示32位)而类型码为110,所以set_intr_gate()设置的是中断门,偏移域设置成中断处理程序的地址addr。
(2)插入一个陷阱门
staticvoid__initset_trap_gate(unsignedintn,void*addr)
_set_gate(idt_table+n,15,0,addr);
在第n个表项中插入一个陷阱门。
这个门的段选择符设置成代码段的选择符,DPL域设置成0,15表示D标志位为1而类型码为111,所以set_trap_gate()设置的是陷阱门,偏移域设置成异常处理程序的地址addr。
(3)插入一个系统门
staticvoid__initset_system_gate(unsignedintn,void*addr)
_set_gate(idt_table+n,15,3,addr);
在第n个表项中插入一个系统门。
这个门的段选择符设置成代码段的选择符,DPL域设置成3,15表示D标志位为1而类型码为111,所以set_system_gate()设置的也是陷阱门,但因为DPL为3,因此,系统调用在用户态可以通过“INT0x80”顺利穿过系统门,从而进入内核态。
5.2.2对陷阱门和系统门的初始化
trap_init()函数就是设置中断描述符表开头的19个陷阱门和系统门,这些中断向量都是CPU保留用于异常处理的:
set_trap_gate(0,&
divide_error);
set_trap_gate(1,&
debug);
……
set_trap_gate(19,&
simd_coprocessor_error);
set_system_gate(SYSCALL_VECTOR,&
system_call);
其中,“&
”之后的名字就是每个异常处理程序的名字。
最后一个是对系统调用的设置。
5.2.3中断门的设置
中断门的设置是由init_IRQ()函数中的一段代码完成的:
for(i=0;
i<
(NR_VECTORS-FIRST_EXTERNAL_VECTOR);
i++){
intvector=FIRST_EXTERNAL_VECTOR+i;
if(i>
=NR_IRQS)
break;
if(vector!
=SYSCALL_VECTOR)
set_intr_gate(vector,interrupt[i]);
}
从FIRST_EXTERNAL_VECTOR开始,设置NR_IRQS(NR_VECTORS-FIRST_EXTERNAL_VECTOR)个IDT表项。
常数FIRST_EXTERNAL_VECTOR定义为0x20,而NR_IRQS则为224,即中断门的个数。
注意,必须跳过用于系统调用的向量0x80,因为这在前面已经设置好了。
这里,中断处理程序的入口地址是一个数组interrupt[],数组中的每个元素是指向中断处理函数的指针。
5.2.4中断处理程序的形成
由前一节知道,interrupt[]为中断处理程序的入口地址,这只是一个笼统的说法。
实际上不同的中断处理程序,不仅名字不同,其内容也不同,但是,这些函数又有很多相同之处,因此应当以统一的方式形成其函数名和函数体,于是,内核对该数组的定义如下:
staticvoid(*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)
};
这里定义的数组interrupt[],从IRQLIST_16(0x2)到IRQLIST_16(0xf)一共有14个数组元素,其中IRQLIST_16()宏的定义如下:
#defineIRQLIST_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)
该宏中定义了16个IRQ(x,y),这样就有224(14*16)个函数指针。
不妨再接着展开IRQ(x,y)宏:
#defineIRQ(x,y)\
IRQ##x##y##_interrupt
##表示将字符串连接起来,比如IRQ(0x2,0)就是IRQ0x20_interrupt。
综上可知,以这样的方式就定义出224个函数,从IRQ0x20_interrupt一直到IRQ0xff_interupt。
那么这些函数名又是如何形成的?
我们看如下宏定义:
#defineIRQ_NAME2(nr)nr##_interrupt(void)
#defineIRQ_NAME(nr)IRQ_NAME2(IRQ##nr)
从这两个宏的定义可以推知,IRQ_NAME(n)就是IRQn_interrupt(void)函数形式,其中随n具体数字不同,则形成不同的IRQn_interrupt()函数名。
接下来,又如何以统一的方式让这些函数拥有内容,也就是说,这些函数的代码是如何形成的?
内核定义了BUILD_IRQ宏。
BUILD_IRQ宏是一段嵌入式汇编代码,为了有助于理解,我们把它展开成下面的汇编语言片段:
IRQn_interrupt:
pushl
$n-256
jmp
common_interrupt
把中断号减256的结果保存在栈中,这是进入中断处理程序后第一个压入堆栈的值,是一个负数,正数留给系统调用使用。
对于每个中断处理程序,唯一不同的就是压入栈中的这个数。
然后,所有的中断处理程序都跳到一段相同的代码common_interrupt。
关于这段代码,请参看5.3.3一节中断处理程序IRQn_interrupt。
5.3中断处理
通过上面的介绍,我们知道了中断描述符表已被初始化,并具有了相应的内容;
对于外部中断,还要建立中断请求队列,以及执行中断处理程序,这正是我们本节要关心的主要内容。
5.3.1中断和异常的硬件处理。
首先,我们从硬件的角度来看CPU如何处理中断和异常。
这里假定内核已被初始化,CPU已从实模式转到保护模式。
当CPU执行了当前指令之后,CS和EIP这对寄存器中所包含的内容就是下一条将要执行指令的虚地址。
在对下一条指令执行前,CPU先要判断在执行当前指令的过程中是否发生了中断或异常。
如果发生了一个中断或异常,那么CPU将做以下事情:
(1)确定所发生中断或异常的向量i(在0~255之间)。
(2)通过IDTR寄存器找到IDT表,读取IDT表第i项(或叫第i个门)。
(3)分两步进行有效性检查:
首先是“段”级检查,将CPU的当前特权级CPL(存放在CS寄存器的最低两位)与IDT中第i项段选择符中的DPL相比较,如果DPL(3)大于CPL(0),就产生一个“通用保护”异常,因为中断处理程序的特权级不能低于引起中断的进程的特权级。
这种情况发生的可能性不大,因为中断处理程序一般运行在内核态,其特权级为0。
然后是“门”级检查,把CPL与IDT中第i个门的DPL相比较,如果CPL大于DPL,也就是当前特权级(3)小于这个门的特权级(0),CPU就不能“穿过”这个门,于是产生一个“通用保护”异常,这是为了避免用户应用程序访问特殊的陷阱门或中断门。
但是请注意,这种“门”级检查是针对一般的用户程序,而不包括外部I/O产生的中断或因CPU内部异常而产生的异常,也就是说,如果产生了中断或异常,就免去了“门”级检查。
(4)检查是否发生了特权级的变化。
当中断发生在用户态(特权级为3),而中断处理程序运行在内核态(特权级为0),特权级发生了变化,所以会引起堆栈的更换。
也就是说,从用户堆栈切换到内核堆栈。
而当中断发生在内核态时,即CPU在内核中运行时,则不会更换堆栈,如图5.4所示。
错误码
中断处理程序的堆栈
特权级发生了变化特权级没有变化
图5.4中断处理程序堆栈示意图
从图可以看出,当从用户态堆栈切换到内核态堆栈时,先把用户态堆栈的值压入中断程序的内核态堆栈中,同时把EFLAGS寄存器自动压栈,然后把被中断进程的返回地址压入堆栈。
如果异常产生了一个硬错误码,则将它也保存在堆栈中。
如果特权级没有发生变化,则压入栈中的内容如图5.4中。
此时,CS:
EIP的值就是IDT表中第i项门描述符的段选择符和偏移量的值,于是,CPU就跳转到了中断或异常处理程序。
5.3.2中断请求队列的建立
由于硬件的限制,很多外部设备不得不共享中断线,例如,一些PC配置可以把同一条中断线分配给网卡和图形卡。
由此看来,让每个中断源都必须占用一条中断线是不现实的。
所以,仅仅中断描述符表并不能提供中断产生的所有信息,内核必须对中断线给出进一步的描述。
在Linux设计中,专门为每个中断请求IRQ设置了一个队列,这就是所谓的中断请求队列。
1中断服务程序与中断处理程序
我们这里提到的中断服务程序ISR(InterruptServiceRoutine)与以前所提到的中断处理程序(Interrupthandler)是两个不同的概念。
在Linux中,15条中断线对应15个中断处理程序,其名依次为IRQ0x00_interrupt(),IRQ0x01_interrupt()……IRQ0x0f_interrupt()。
具体来说,中断处理程序相当于某个中断向量的总处理程序,例如IRQ0x05_interrupt()是中断号5(向量为37)的总处理程序,如果这个5号中断由网卡和图形卡共享,则网卡和图形卡分别有其相应的中断服务程序。
2中断线共享的数据结构
为了让多个设备能共享一条中断线而内核设置了一个叫irqaction的数据结构:
typedefirqreturn_t(*irq_handler_t)(int,void*);
structirqaction{
irq_handler_thandler;
unsignedlongflags;
cpumask_tmask;
constchar*name;
void*dev_id;
structirqaction*next;
intirq;
…
};
对每个域描述如下:
handler
指向一个具体I/O设备的中断服务程序,该函数有两个参数,第一个参数为中断号IRQ,第二个参数为void指针,该指针一般传入dev_id(唯一地标示某个设备的设备号)的值。
flags
用一组标志描述中断线与I/O设备之间的关系。
IRQF_DISABLED
中断处理程序执行时必须禁止中断
IRQF_SHARED
允许其它设备共享这条中断线。
IRQF_SAMPLE_RANDOM
可以把这个设备看作是随机事件发生源;
因此,内核可以用它做随机数产生器。
name
I/O设备名
dev_id
指定I/O设备的主设备号和次设备号(参见第9章)。
next
指向irqaction描述符链表的下一个元素,前提是flags为IRQF_SHARED标志。
共享同一中断线的每个硬件设备都有其对应的中断服务程序,链表中的每个元素就是对相应设备及中断服务程序的描述。
3注册中断服务程序
在IDT表初始化完成之初,每个中断服务队列还为空。
此时,即使打开中断且某个外设中断真的发生了,也得不到实际的服务。
因为CPU虽然通过中断门进入了某个中断向量的总处理程序,例如IRQ0x05_interrupt(),但是,具体的中断服务程序(如图形卡的)还没有挂入中断请求队列。
因此,在设备驱动程序的初始化阶段,必须通过request_irq()函数将相应的中断服务程序挂入中断请求队列,也就是对其进行注册。
request_irq()函数原型为:
intrequest_irq(unsignedintirq,
irq_handler_thandler,
unsignedlongirqfla