第四节中断处理

http://www.eefocus.com/article/09-06/74822s.html

http://www.openhw.org/html/09-06/74822.shtml

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宏操作在前面已经介绍过,也就是恢复中断现场,彻底从中断返回。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值