第4节 中断处理
推荐给好友
打印
加入收藏
更新于2009-06-14 07:54:49

3.4.1中断和异常处理的硬件处理

首先,我们从硬件的角度来看CPU如何处理中断和异常。这里假定内核已被初始化,CPU已从实模式转到保护模式。

当CPU执行了当前指令之后,CS和EIP这对寄存器中所包含的内容就是下一条将要执行指令的逻辑地址。在对下一条指令执行前,CPU先要判断在执行当前指令的过程中是否发生了中断或异常。如果发生了一个中断或异常,那么CPU将做以下事情:

· 确定所发生中断或异常的向量i(在0~255之间)。
· 通过IDTR寄存器找到IDT表,读取IDT表第i项(或叫第i个门)。
· 分两步进行有效性检查:首先是“段”级检查,将CPU的当前特权级CPL(存放在CS寄存器的最低两位)与IDT中第i项段选择符中的DPL相比较,如果DPL(3)大于CPL(0),就产生一个“通用保护”异常(中断向量13),因为中断处理程序的特权级不能低于引起中断的程序的特权级。这种情况发生的可能性不大,因为中断处理程序一般运行在内核态,其特权级为0。然后是“门”级检查,把CPL与IDT中第i个门的DPL相比较,如果CPL大于DPL,也就是当前特权级(3)小于这个门的特权级(0),CPU就不能“穿过”这个门,于是产生一个“通用保护”异常,这是为了避免用户应用程序访问特殊的陷阱门或中断门。但是请注意,这种“门”级检查是针对一般的用户程序,而不包括外部I/O产生的中断或因CPU内部异常而产生的异常,也就是说,如果产生了中断或异常,就免去了“门”级检查。

检查是否发生了特权级的变化。当中断发生在用户态(特权级为3),而中断处理程序运行在内核态(特权级为0),特权级发生了变化,所以会引起堆栈的更换。也就是说,从用户堆栈切换到内核堆栈。而当中断发生在内核态时,即CPU在内核中运行时,则不会更换堆栈,如图3.5所示。

图3.5中断处理程序堆栈示意图

从图可以看出,当从用户态堆栈切换到内核态堆栈时,先把用户态堆栈的值压入中断程序的内核态堆栈中,同时把 EFLAGS寄存器自动压栈,然后把被中断进程的返回地址压入堆栈。如果异常产生了一个硬件错误码,则将它也保存在堆栈中。如果特权级没有发生变化,则压入栈中的内容如图3.4中‚。你可能要问,现在SS:ESP和CS:EIP 这两对寄存器的值分别是什么?SS:ESP的值从当前进程的TSS中获得,也就是获得当前进程的内核栈指针,因为此时中断处理程序成为当前进程的一部分,代表当前进程在运行。CS:EIP的值就是IDT表中第i项门描述符的段选择符和偏移量的值,此时,CPU就跳转到了中断或异常处理程序。

3.4.2 Linux对异常和中断的处理

上面给出了硬件对异常和中断进行处理的一般步骤,下面将概要描述Linux对异常和中断的处理,具体的实现过程将在后面介绍。

1.异常处理

Linux利用异常来达到两个截然不同的目的:

· 给进程发送一个信号以通报一个反常情况
· 管理硬件资源

对于第一种情况,例如,如果进程执行了一个被0除的操作,CPU则会产生一个“除法错误”异常,并由相应的异常处理程序向当前进程发送一个SIGFPE信号。当前进程接收到这个信号后,就要采取若干必要的步骤,或者从错误中恢复,或者终止执行(如果这个信号没有相应的信号处理程序)。

对于第二种情况,内核使用两种异常来有效地管理硬件资源,相应的处理程序也就更复杂。在这种情况下,异常并不表示一种错误情况:

· 用“设备不可用”异常来推迟装载浮点寄存器。
· 用“缺页”异常推迟把新页框分配给进程。

内核对异常处理程序的调用有一个标准的结构,它由以下三部分组成:
· 在内核栈中保存大多数寄存器的内容(由汇编语言实现)
· 调用C编写的异常处理函数
· 通过ret_from_exception()函数从异常退出。

关于内核对异常的具体处理在此不进行详细介绍,在第六章内存管理中我们将涉及到“缺页”异常处理程序,本节的重点放在中断处理。

2.中断处理

当一个中断发生时,并不是所有的操作都具有相同的急迫性。事实上,把所有的操作都放进中断处理程序本身并不合适。需要时间长的、非重要的操作应该推后,因为当一个中断处理程序正在运行时,相应的IRQ中断线上再发出的信号就会被忽略。更重要的是,中断处理程序是代表进程执行的,它所代表的进程必需总处于TASK_RUNNING状态,否则,就可能出现系统僵死情形。因此,中断处理程序不能执行任何阻塞过程,如I/O设备操作。因此,Linux把一个中断要执行的操作分为下面的三类:

(1)紧急的(Critical)

这样的操作诸如:中断到来时中断控制器做出应答,对中断控制器或设备控制器重新编程,或者对设备和处理器同时访问的数据结构进行修改。这些操作都是紧急的,应该被很快地执行,也就是说,紧急操作应该在一个中断处理程序内立即执行,而且是在禁用中断的状态下。

(2)非紧急的(Noncritical)

这样的操作如修改那些只有处理器才会访问的数据结构(例如,按下一个键后,读扫描码)。这些操作也要很快地完成,因此,它们由中断处理程序立即执行,但在启用中断的状态下。

(3)非紧急可延迟的(Noncritical deferrable)

这样的操作如,把一个缓冲区的内容拷贝到一些进程的地址空间(例如,把键盘行缓冲区的内容发送到终端处理程序的进程)。这些操作可能被延迟较长的时间间隔而不影响内核操作:有兴趣的进程会等待需要的数据。非紧急可延迟的操作由一些被称为“下半部分”(bottom halves)的函数来执行。我们将在后面讨论“下半部分”。

所有的中断处理程序都执行四个基本的操作:

· 在内核栈中保存IRQ的值和寄存器的内容。
· 给与IRQ中断线相连的中断控制器发送一个应答,这将允许在这条中断线上进一步发出中断请求。
· 执行共享这个IRQ的所有设备的中断服务例程(ISR)。
· 跳到ret_from_intr( )的地址后终止。

3.4.3 与堆栈有关的常量、数据结构及宏

在中断、异常及系统调用的处理中,涉及一些相关的常量、数据结构及宏,在此先给予介绍(大部分代码在arch/i386/kernel/entry.S中)。

1. 常量定义

下面这些常量定义了进入中断处理程序时,相关寄存器与堆栈指针(ESP)的相对位置,图3.6给出了在相应位置上所保存的寄存器内容:

EBX = 0x00
ECX= 0x04
EDX= 0x08
ESI= 0x0C
EDI= 0x10
EB = 0x14
EAX= 0x18
DS= 0x1C
ES = 0x20
ORIG_EAX = 0x24
EIP = 0x28
CS = 0x2C
EFLAGS = 0x30
OLDESP= 0x34
OLDSS = 0x38

图3.6 进入中断理程序时内核堆栈示意图

其中,ORIG_EAX是Original eax之意,其具体含义将在后面介绍。

2.存放在栈中的寄存器结构pt_regs

在内核中,很多函数的参数是pt_regs数据结构,定义在include/i386/ptrace.h中:

struct pt_regs {
long ebx;
long ecx;
long edx;
long esi;
long edi;
long ebp;
long eax;
int xds;
int xes;
long orig_eax;
long eip;
int xcs;
long eflags;
long esp;
int xss;
};

把这个结构与内核栈的内容相比较,会发现堆栈的内容是这个数据结构的一个映象。

3.保存现场的宏SAVE_ALL

在中断发生前夕,要把所有相关寄存器的内容都保存在堆栈中,这是通过SAVE_ALL宏完成的:

#define SAVE_ALL \
cld; \
pushl %es; \
pushl %ds; \
pushl %eax; \
pushl %ebp; \
pushl %edi; \
pushl %esi; \
pushl %edx; \
pushl %ecx; \
pushl %ebx; \
movl $(__KERNEL_DS),%edx; \
movl %edx,%ds; \
movl %edx,%es;

该宏执行以后,堆栈内容如图3.6所示。把这个宏与图3.5 结合起来就很容易理解图3.6,在此对该宏再给予解释:

· CPU在进入中断处理程序时自动将用户栈指针(如果更换堆栈)、EFLAGS寄存器及返回地址一同压入堆栈。
· 段寄存器DS和ES原来的内容入栈,然后装入内核数据段描述符__KERNEL_DS(定义为0x18),内核段的DPL为0。

4.恢复现场的宏RESTORE_ALL

当从中断返回时,恢复相关寄存器的内容,这是通过RESTORE_ALL宏完成的:

#define RESTORE_ALL \
popl %ebx; \
popl %ecx; \
popl %edx; \
popl %esi; \
popl %edi; \
popl %ebp; \
popl %eax; \
1: popl %ds; \
2: popl %es; \
addl $4,%esp; \
3: iret;

可以看出,RESTORE_ALL与SAVE_ALL遥相呼应。当执行到iret指令时,内核栈又恢复到刚进入中断门时的状态,并使CPU从中断返回。

5.将当前进程的task_struct 结构的地址放在寄存器中

#define GET_CURRENT(reg) \
movl $-8192, reg; \
andl %esp, reg

从下一章“task_struct 结构在内存存放”一节我们将知道,当前进程的task_struct存放在内核栈的底部,因此,以上两条指令就可以把task_struct结构的地址放在reg寄存器中。

3.4.4 中断处理程序的执行

从前面的介绍,我们已经知道了 i386的中断机制及有关的初始化工作。现在,我们可以从中断请求的发生到CPU的响应,再到中断处理程序的调用和返回,沿着这一思路走一遍,以体会Linux内核对中断的响应及处理。

假定外设的驱动程序都已完成了初始化工作,并且已把相应的中断服务例程挂入到特定的中断请求队列。 又假定当前进程正在用户空间运行(随时可以接受中断),且外设已产生了一次中断请求。当这个中断请求通过中断控制器8259A到达CPU的中断请求引线INTR时(参看图3.1),CPU就在执行完当前指令后来响应该中断。

CPU从中断控制器的一个端口取得中断向量I,然后根据I从中断描述符表IDT中找到相应的表项,也就是找到相应的中断门。因为这是外部中断,不需要进行“门级”检查,CPU就可以从这个中断门获得中断处理程序的入口地址,假定为IRQ0x05_interrupt。因为这里假定中断发生时CPU运行在用户空间(CPL=3),而中断处理程序属于内核(DPL=0),因此,要进行堆栈的切换。也就是说,CPU从TSS中取出内核栈指针,并切换到内核栈(此时栈还为空)。当CPU进入IRQ0x05_interrupt时,内核栈如图3.5的,栈中除用户栈指针、EFLAGS的内容以及返回地址外再无其他内容。另外,由于CPU进入的是中断门(而不是陷阱门),因此,这条中断线已被禁用,直到重新启用。

我们用IRQn_interrupt来表示从IRQ0x01_interrupt 到IRQ0x0f_interrupt任意一个中断处理程序。这个中断处理程序实际上要调用do_IRQ(),而do_IRQ()要调用handle_IRQ_event()函数;最后这个函数才真正地执行中断服务例程(ISR)。图3.7给出它们的调用关系:

图3.7中断处理函数的调用关系

1.中断处理程序IRQn_interrupt

我们首先看一下从IRQ0x01_interrupt 到IRQ0x0f_interrupt的这16个函数是如何定义的,在i8259.c中定义了如下宏:

#define BI(x,y) \
BUILD_IRQ(x##y)
#define BUILD_16_IRQS(x) \
BI(x,0) BI(x,1) BI(x,2) BI(x,3) \
BI(x,4) BI(x,5) BI(x,6) BI(x,7) \
BI(x,8) BI(x,9) BI(x,a) BI(x,b) \
BI(x,c) BI(x,d) BI(x,e) BI(x,f)
BUILD_16_IRQS(0x0)

经过gcc的预处理,宏定义BUILD_16_IRQS(0x0) 会被展开成BUILD_IRQ(0x00)至BUILD_IRQ(0x0f)。BUILD_IRQ宏是一段嵌入式汇编代码(在/include/i386/hw_irq.h中),为了有助于理解,我们把它展开成下面的汇编语言片段:

IRQn_interrupt:
pushl $n-256
jmp common_interrupt

把中断号减256的结果保存在栈中,这就是进入中断处理程序后第一个压入堆栈的值,也就是堆栈中ORIG_EAX的值,如图3.6。这是一个负数,正数留给系统调用使用。对于每个中断处理程序,唯一不同的就是压入栈中的这个数。然后,所有的中断处理程序都跳到一段相同的代码common_interrupt。这段代码可以在BUILD_COMMON_IRQ 宏中找到,同样,我们略去其嵌入式汇编源代码,而把这个宏展开成下列的汇编语言片段:

common_interrupt:
SAVE_ALL
call do_IRQ
jmp ret_from_intr

SAVE_ALL宏已经在前面介绍过,它把中断处理程序会使用的所有CPU寄存器都保存在栈中。然后,BUILD_COMMON_IRQ 宏调用do_IRQ( )函数,因为通过CALL调用这个函数,因此,该函数的返回地址被压入栈。当执行完do_IRQ( ),就跳转到ret_from_intr( )地址(参见后面的“从中断和异常返回)。

2. do_IRQ( )函数

do_IRQ()这个函数处理所有外设的中断请求。当这个函数执行时,内核栈从栈顶到栈底包括:

· do_IRQ( )的返回地址
· 由SAVE_ALL 推进栈中的一组寄存器的值
· ORIG_EAX(即n-256)
· CPU自动保存的寄存器

该函数的实现用到中断线的状态,下面给予具体说明:

#define IRQ_INPROGRESS 1 /* 正在执行这个IRQ的一个处理程序*/
#define IRQ_DISABLED 2 /* 由设备驱动程序已经禁用了这条IRQ中断线 */
#define IRQ_PENDING 4 /* 一个IRQ已经出现在中断线上,且被应答,但还没有为它提供服务 */
#define IRQ_REPLAY 8 /* 当Linux重新发送一个已被删除的IRQ时 */
#define IRQ_AUTODETECT 16 /* 当进行硬件设备探测时,内核使用这条IRQ中断线 */
#define IRQ_WAITING 32 /*当对硬件设备进行探测时,设置这个状态以标记正在被测试的irq */
#define IRQ_LEVEL 64 /* IRQ level triggered */
#define IRQ_MASKED 128 /* IRQ masked - shouldn't be seen again */
#define IRQ_PER_CPU 256 /* IRQ is per CPU */

这8个状态的前5个状态比较常用,因此我们给出了具体解释。另外,我们还看到每个状态的常量是2的幂次方。最大值为256(28), 因此可以用一个字节来表示这8个状态,其中每一位对应一个状态。

该函数在arch//i386/kernel/irq.c中定义如下:

asmlinkage unsigned int do_IRQ(struct pt_regs regs)
{
/* 函数返回0则意味着这个irq正在由另一个CPU进行处理,
或这条中断线被禁用*/
int irq = regs.orig_eax & 0xff; /* 还原中断号 */
int cpu = smp_processor_id(); /*获得CPU号*/
irq_desc_t *desc = irq_desc + irq; /*在irq_desc[]数组中获得irq 的描述符*/
struct irqaction * action;
unsigned int status;
kstat.irqs[cpu][irq]++;
spin_lock(&desc->lock); /*针对多处理机加锁*/
desc->handler->ack(irq); /*CPU对中断请求给予确认*/
status = desc->status & ~(IRQ_REPLAY | IRQ_WAITING);
status |= IRQ_PENDING; /* we _want_ to handle it */
action = NULL;
if (!(status & (IRQ_DISABLED | IRQ_INPROGRESS))) {
action = desc->action;
status &= ~IRQ_PENDING; /* we commit to handling */
status |= IRQ_INPROGRESS; /* we are handling it */
}
desc->status = status;
if (!action)
goto out;
for (;;) {
spin_unlock(&desc->lock); /*进入临界区*
handle_IRQ_event(irq, &regs, action);
spin_lock(&desc->lock); /*出临界区*/
if (!(desc->status & IRQ_PENDING))
break;
desc->status &= ~IRQ_PENDING;
}
desc->status &= ~IRQ_INPROGRESS;
out:
/*
* The ->end() handler has to deal with interrupts which got
* disabled while the handler was running.
*/
desc->handler->end(irq);
spin_unlock(&desc->lock);
if (softirq_pending(cpu))
do_softirq(); /*处理软中断*/
return 1;
}

下面对这个函数进行进一步的讨论:

· 当执行到for (;;)这个无限循环时,就准备对中断请求队列进行处理,这是由handle_IRQ_event ()函数完成的。因为中断请求队列为一临界资源,因此在进入这个函数前要加锁。
· handle_IRQ_event ()函数的主要代码片段为:

if (!(action->flags & SA_INTERRUPT))
__sti(); /*关中断*/
do {
status |= action->flags;
action->handler(irq, action->dev_id, regs);
action = action->next;
} while (action);
__cli(); /*开中断*/

这个循环依次调用请求队列中的每个中断服务例程。中断服务例程及其参数已经在前面进行过简单描 述,至于更具体的解释将在驱动程序一章进行描述。

· 这里要说明的是,中断服务例程都在关中断的条件下进行(不包括非屏蔽中断),这也是为什么CPU在穿过中断门时自动关闭中断的原因。但是,关中断时间绝不能太长,否则就可能丢失其它重要的中断。也就是说,中断服务例程应该处理最紧急的事情,而把剩下的事情交给另外一部分来处理。即后半部分(bottom half)来处理,这一部分内容将在下一节进行讨论。
· 经验表明,应该避免在同一条中断线上的中断嵌套,内核通过IRQ_PENDING标志位的应用保证了这一点。当do_IRQ()执行到for (;;)循环时,desc->status 中的IRQ_PENDING的标志位肯定为0(想想为什么?)。当CPU执行完handle_IRQ_event ()函数返回时,如果这个标志位仍然为0,那么循环就此结束。如果这个标志位变为1,那就说明这条中断线上又有中断产生(对单CPU而言),所以循环又执行一次。通过这种循环方式,就把可能发生在同一中断线上的嵌套循环化解为“串行”。
· 不同的CPU不允许并发地进入同一中断服务例程,否则,那就要求所有的中断服务例程必须是“可重入”的纯代码。可重入代码的设计和实现就复杂多了,因此,Linux在设计内核时巧妙地“避难就易”,以解决问题为主要目标。
· 在循环结束后调用desc->handler->end()函数,具体来说,如果没有设置IRQ_DISABLED标志位,就调用低级函数enable_8259A_irq()来启用这条中断线。
· 如果这个中断有后半部分,就调用do_softirq()执行后半部分。

3.4.5 从中断返回

从前面的讨论我们知道,do_IRQ()这个函数处理所有外设的中断请求。这个函数执行的时候,内核栈栈顶包含的就是do_IRQ()的返回地址,这个地址指向ret_from_intr。实际上,ret_from_intr是一段汇编语言的入口点,为了描述简单起见,我们以函数的形式提及它。虽然我们这里讨论的是中断的返回,但实际上中断、异常及系统调用的返回是放在一起实现的,因此,我们常常以函数的形式提到下面这三个入口点:

ret_from_intr()

终止中断处理程序

ret_from_sys_call( )

终止系统调用,即由0x80引起的异常。

ret_from_exception( )

终止除了0x80的所有异常

在相关的计算机课程中,我们已经知道从中断返回时CPU要做的事情,下面我们来看一下Linux内核的具体实现代码(在entry.S中):

ENTRY(ret_from_intr)
GET_CURRENT(%ebx)
ret_from_exception:
movl EFLAGS(%esp),%eax # mix EFLAGS and CS
movb CS(%esp),%al
testl $(VM_MASK | 3),%eax # return to VM86 mode or non-supervisor?
jne ret_from_sys_call
jmp restore_all

这里的GET_CURRENT(%ebx)将当前进程task_struct结构的指针放入寄存器EBX中,此时,内核栈的内容还如图3.6所示。然后两条“mov”指令是为了把中断发生前夕EFALGS寄存器的高16位与代码段CS寄存器的内容拼揍成32位的长整数,其目的是要检验:

· 中断前夕CPU是否够运行于VM86模式
· 中断前夕CPU是运行在用户空间还是内核空间。

VM86模式是为在i386保护模式下模拟运行DOS软件而设置的,EFALGS寄存器高16位中有个标志位表示CPU是否运行在VM86模式,我们在此不予详细讨论。CS的最低两位表示中断发生时CPU的运行级别CPL,若这两位为3,说明中断发生于用户空间。

如果中断发生在内核空间,则控制权直接转移到标号restore_all。如果中断发生于用户空间(或VM86模式),则转移到ret_from_sys_call:

ENTRY(ret_from_sys_call)
cli # need_resched and signals atomic test
cmpl $0,need_resched(%ebx)
jne reschedule
cmpl $0,sigpending(%ebx)
jne signal_return
restore_all:
RESTORE_ALL
reschedule:
call SYMBOL_NAME(schedule) # test
jmp ret_from_sys_call

进入ret_from_sys_call后,首先关中断,也就是说,执行这段代码时CPU不接受任何中断请求。然后,看调度标志是否为非0,其中常量need_resched 定义为20,need_resched(%ebx)表示当前进程task_struct结构中偏移量need_resched处的内容,如果调度标志为非0,说明需要进行调度,则去调用schedule()函数进行进程调度,这将在第五章进行详细讨论。

同样,如果当前进程的task_struct结构中的sigpending标志为非0,就表示该进程有信号等待处理,要先处理完这些信号后才从中断返回,关于信号的处理将在“进程间通信”一章进行讨论。处理完信号,控制权还是返回到restore_all。RESTORE_ALL宏操作在前面已经介绍过,也就是恢复中断现场,彻底从中断返回。



上一节   下一节

相关链接


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