第3节 异常处理
推荐给好友
打印
加入收藏
更新于2009-06-15 07:45:35

3.3异常处理

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

· 给进程发送一个信号以通报一个反常情况
· 处理请求分页

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

内核对异常处理程序的调用有一个标准的结构,它由以下三部分组成:

· 在内核栈中保存大多数寄存器的内容(由汇编语言实现)
· 调用C编写的异常处理函数
· 通过ret_from_exception()函数从异常退出。

3.3.1 在内核栈中保存寄存器的值

所有异常处理程序被调用的方式比较相似,因此,我们用handler_name来表示一个通用的异常处理程序的名字(实际名字都出现在表3.1中)。进入异常处理程序的汇编指令在arch/I386/kernel/entry.S中:

handler_name:
pushl $0 /* only for some exceptions */
pushl $do_handler_name
jmp error_code

例如: overflow:
pushl $0
pushl $ do_overflow
jmp error_code

当异常发生时,如果控制单元没有自动地把一个硬件错误代码插入到栈中,相应的汇编语言片段会包含一条pushl $0指令,在栈中垫上一个空值,如果错误码已经被压入堆栈,则没有这条指令。然后,把异常处理函数的地址压进栈中;函数的名字由异常处理程序名与do_前缀组成。

标号为error_code的汇编语言片段对所有的异常处理程序都是相同的,除了“设备不可用”这一个异常。这段代码为实际上是为异常处理程序的调用和返回进行相关的操作,代码如下:

error_code:
pushl %ds
pushl %eax
xorl %eax,%eax
pushl %ebp
pushl %edi 把C函数可能用到的寄存器都保存在栈中
pushl %esi
pushl %edx
decl %eax #eax = -1
pushl %ecx
pushl %ebx
cld # 清eflags的方向标志,以确保edi和esi寄存器的值自动增加
movl %es,%ecx
movl ORIG_EAX(%esp), %esi # get the error code, ORIG_EAX= 0x24
movl ES(%esp), %edi # get the function address, ES = 0x20
movl %eax, ORIG_EAX(%esp) #把栈中的这个位置置为-1
movl %ecx, ES(%esp)
movl %esp,%edx
pushl %esi # push the error code
pushl %edx # push the pt_regs pointer movl $(__KERNEL_DS),%edx
movl %edx,%ds #把内核数据段选择符装入ds寄存器
movl %edx,%es
GET_CURRENT(%ebx) #ebx中存放当前进程task_struct结构的地址
call *%edi #调用这个异常处理程序
addl $8,%esp
jmp ret_from_exception

如图3.6给出从用户进程进入异常处理程序时内核堆栈的示意图:

(a) 进入异常处理程序时内核堆栈示意图

(b) 异常处理程序被调用后堆栈的示意图

图3.6 进入异常后内核堆栈的变化

3.3.2 中断请求队列的初始化

由于硬件的限制,很多外部设备不得不共享中断线,例如,一些PC配置可以把同一条中断线分配给网卡和图形卡。由此看来,让每个中断源都必须占用一条中断线是不现实的。所以,仅仅中断描述符表并不能提供中断产生的所有信息,内核必须对中断线给出进一步的描述。在Linux设计中,专门为每个中断请求IRQ设置了一个队列,这就是我们所说的中断请求队列。

注意:中断线、中断请求(IRQ)号及中断向量之间的关系为:中断线是中断请求的一种物理描述,逻辑上对应一个中断请求号(或简称中断号),第n个中断号(IRQn)的缺省中断向量是n+32。

3.3.3中断请求队列的数据结构

如前所述,在256个中断向量中,除了32个分配给异常外,还有224个作为中断向量。对于每个IRQ,Linux都用一个irq_desc_t数据结构来描述,我们把它叫做IRQ描述符,224个IRQ形成一个数组irq_desc[],其定义在/include/linux/irq.h中:

/*
* This is the "IRQ descriptor", which contains various information
* about the irq, including what kind of hardware handling it has,
* whether it is disabled etc etc.
*
* Pad this out to 32 bytes for cache and indexing reasons.
*/
typedef struct {
unsigned int status; /* IRQ status */
hw_irq_controller *handler;
struct irqaction *action; /* IRQ action list */
unsigned int depth; /* nested irq disables */
spinlock_t lock;
} ____cacheline_aligned irq_desc_t;
extern irq_desc_t irq_desc [NR_IRQS];

编码作者对这个数据结构给出了一定的解释,“____cacheline_aligned”表示这个数据结构的存放按32字节(高速缓存行的大小)进行对齐,以便于将来存放在高速缓存并容易存取。下面对这个数据结构的各个域给予描述:

status

描述IRQ中断线状态的一组标志(在irq.h中定义),其具体含义及应用将在do_IRQ()函数中介绍:

handler

指向hw_interrupt_type描述符,这个描述符是对中断控制器的描述,下面会给出具体解释。

action

指向一个单向链表的指针,这个链表就是对中断服务例程进行描述的irqaction结构,后面将给予具体描述。

depth

如果启用这条IRQ中断线,depth则为0,如果禁用这条IRQ中断线不止一次,则为一个正数。每当调用一次disable_irq( ),该函数就对这个域的值加1;如果depth等于0,该函数就禁用这条IRQ中断线。相反,每当调用enable_irq( )函数时,该函数就对这个域的值减1;如果depth变为0,该函数就启用这条IRQ中断线。

1.IRQ描述符的初始化

在系统初始化期间,init_ISA_irqs()函数对IRQ数据结构(或叫描述符)的域进行初始化(参见i8258.c):

for (i = 0; i < NR_IRQS; i++) {
irq_desc[i].status = IRQ_DISABLED;
irq_desc[i].action = 0;
irq_desc[i].depth = 1;
if (i < 16) {
*
* 16 old-style INTA-cycle interrupts:
*/
irq_desc[i].handler = &i8259A_irq_type;
} else {
/*
* 'high' PCI IRQs filled in on demand
*/
irq_desc[i].handler = &no_irq_type;
}
}

从这段程序可以看出,初始化时,让所有的中断线都处于禁用状态;每条中断线上还没有任何中断服务例程(action为0);因为中断线被禁用,因此depth为1;对中断控制器的描述分为两种情况,一种就是通常所说的8259A,另一种是其它控制器。

然后,更新中断描述符表IDT,如3.2.3节所述,用最终的中断门来代替临时使用的中断门。

2.中断控制器描述符hw_interrupt_type

这个描述符包含一组指针,指向与特定中断控制器电路(PIC)打交道的低级I/O例程,定义如下:

/*
* Interrupt controller descriptor. This is all we need
* to describe about the low-level hardware.
*/
struct hw_interrupt_type {
const char * typename;
unsigned int (*startup)(unsigned int irq);
void (*shutdown)(unsigned int irq);
void (*enable)(unsigned int irq);
void (*disable)(unsigned int irq);
void (*ack)(unsigned int irq);
void (*end)(unsigned int irq);
void (*set_affinity)(unsigned int irq, unsigned long mask);
};
typedef struct hw_interrupt_type hw_irq_controller;

Linux除了支持本章前面已提到的8259A芯片外,也支持其他的PIC电路,如SMP IO-APIC、PIIX4的内部 8259 PIC及 SGI的Visual Workstation Cobalt (IO-)APIC。但是,为了简单起见,我们在本章假定,我们的计算机是有两片8259A PIC的单处理机,它提供16个标准的IRQ。在这种情况下,有16个irq_desc_t描述符,其中每个描述符的handler域指向8259A_irq _type变量。对其进行如下的初始化:

struct hw_interrupt_type i8259A_irq_type = {
"XT-PIC",
startup_8259A_irq,
shutdown_8259A_irq,
do_8259A_IRQ,
enable_8259A_irq,
disable_8259A_irq
};

在这个结构中的第一个域“XT-PIC”是一个名字。接下来,8259A_irq_type包含的指针指向五个不同的函数,这些函数就是对PIC编程的函数。前两个函数分别启动和关闭这个芯片的中断线。但是,在使用8259A芯片的情况下,这两个函数的作用与后两个函数是一样的,后两个函数是启用和禁用中断线。后面在对do_IRQ描述时具体描述do_8259A_IRQ( )函数。

3.中断服务例程描述符irqaction

在IRQ描述符中我们看到指针action的结构为irqaction,它是为多个设备能共享一条中断线而设置的一个数据结构。在include/linux/interrupt.h中定义如下:

struct irqaction {
void (*handler)(int, void *, struct pt_regs *);
unsigned long flags;
unsigned long mask;
const char *name;
void *dev_id;
struct irqaction *next;
};

这个描述符包含下列域。

handler

指向一个具体I/O设备的中断服务例程。这是允许多个设备共享同一中断线的关键域。

flags

用一组标志描述中断线与I/O设备之间的关系。

SA_INTERRUPT

中断处理程序必须以禁用中断来执行

SA_SHIRQ

该设备允许其中断线与其他设备共享。

SA_SAMPLE_RANDOM

可以把这个设备看作是随机事件发生源;因此,内核可以用它做随机数产生器。(用户可以从/dev/random 和/dev/urandom设备文件中取得随机数而访问这种特征)

SA_PROBE

内核在执行硬件设备探测时正在使用这条中断线。

name

I/O设备名(通过读取/proc/interrupts文件,可以看到,在列出中断号时也显示设备名。)

dev_id

指定I/O设备的主设备号和次设备号。

next

指向irqaction描述符链表的下一个元素。共享同一中断线的每个硬件设备都有其对应的中断服务例程,链表中的每个元素就是对相应设备及中断服务例程的描述。

4.中断服务例程

我们这里提到的中断服务例程(Interrupt Service Routine)与以前所提到的中断处理程序(Interrupt handler)是不同的概念。具体来说,中断处理程序相当于某个中断向量的总处理程序,例如IRQ0x05_interrupt(),是中断号5(向量为37)的总处理程序,如果这个5号中断由网卡和图形卡共享,则网卡和图形卡分别有其相应的中断服务例程。每个中断服务例程都有相同的参数:

IRQ:中断号;

dev_id: 设备标识符,其类型为void*;

regs: 指向内核堆栈区的指针,堆栈中存放的是中断发生后所保存的寄存器,有关pt_regs结构的具体内容将在后面介绍。

在实际中,大多数中断服务例程并不使用这些参数。

3.3.2中断请求队列的初始化

在IDT表初始化完成之初,每个中断服务队列还为空。此时,即使打开中断且某个外设中断真的发生了,也得不到实际的服务。因为CPU虽然通过中断门进入了某个中断向量的总处理程序,例如IRQ0x05_interrupt(),但是,具体的中断服务例程(如图形卡的)还没有挂入中断请求队列。因此,在设备驱动程序的初始化阶段,必须通过request_irq()函数将对相应的中断服务例程挂入中断请求队列。

request_irq()函数的代码在/arch/i386/kernel/irq.c中:

/*
* request_irq - allocate an interrupt line
* @irq: Interrupt line to allocate
* @handler: Function to be called when the IRQ occurs
* @irqflags: Interrupt type flags
* @devname: An ascii name for the claiming device
* @dev_id: A cookie passed back to the handler function
*

* This call allocates interrupt resources and enables the
* interrupt line and IRQ handling. From the point this
* call is made your handler function may be invoked. Since
* your handler function must clear any interrupt the board
* raises, you must take care both to initialise your hardware
* and to set up the interrupt handler in the right order.
*
* Dev_id must be globally unique. Normally the address of the
* device data structure is used as the cookie. Since the handler
* receives this value it makes sense to use it.
*
* If your interrupt is shared you must pass a non NULL dev_id
* as this is required when freeing the interrupt.
*
* Flags:
*
* SA_SHIRQ Interrupt is shared
*
* SA_INTERRUPT Disable local interrupts while processing
*
* SA_SAMPLE_RANDOM The interrupt can be used for entropy
*
*/
int request_irq(unsigned int irq,
void (*handler)(int, void *, struct pt_regs *),
unsigned long irqflags,
const char * devname,
void *dev_id)
{
int retval;
struct irqaction * action;
#if 1
*
* Sanity-check: shared interrupts should REALLY pass in
* a real dev-ID, otherwise we'll have trouble later trying
* to figure out which interrupt is which (messes up the
* interrupt freeing logic etc).
*/
if (irqflags & SA_SHIRQ) {
if (!dev_id)
printk("Bad boy: %s (at 0x%x) called us without a dev_id!\n", devname, (&irq)[-1]);
}
#endif
if (irq >= NR_IRQS)
return -EINVAL;
if (!handler)
return -EINVAL;
action = (struct irqaction *)
kmalloc(sizeof(struct irqaction), GFP_KERNEL);
if (!action)
return -ENOMEM;
action->handler = handler;
action->flags = irqflags;
action->mask = 0;
action->name = devname; 对action进行初始化
action->next = NULL;
action->dev_id = dev_id;
retval = setup_irq(irq, action);
if (retval)
kfree(action);
return retval;
}

编码作者对此函数给出了比较详细的描述。其中主要语句就是对setup_irq()函数的调用,该函数才是真正对中断请求队列进行初始化的函数(有所简化):

int setup_irq(unsigned int irq, struct irqaction * new)
{
int shared = 0;
unsigned long flags;
struct irqaction *old, **p;
irq_desc_t *desc = irq_desc + irq; /*获得irq的描述符*/
/* 对中断请求队列的操作必须在临界区中进行 */
spin_lock_irqsave(&desc->lock,flags); /*进入临界区*/
p = &desc->action; /*让p 指向irq描述符的action域,即irqaction链表的首部*/
if ((old = *p) != NULL) { /*如果这个链表不为空*/
/* Can't share interrupts unless both agree to */
if (!(old->flags & new->flags & SA_SHIRQ)) {
spin_unlock_irqrestore(&desc->lock,flags);
return -EBUSY;
}
* 把新的中断服务例程加入到irq中断请求队列*/
do {
p = &old->next;
old = *p;
} while (old);
shared = 1;
}
*p = new;
if (!shared) { /*如果irq不被共享 */
desc->depth = 0; /*启用这条irq线*/
desc->status &= ~(IRQ_DISABLED | IRQ_AUTODETECT | IRQ_WAITING);
desc->handler->startup(irq); /*即调用startup_8259A_irq()函数*/
}
spin_unlock_irqrestore(&desc->lock,flags); /*退出临界区*/
register_irq_proc(irq); /*在proc文件系统中显示irq的信息*/
return 0;
}

下面我们举例说明对这两个函数的使用:

1.对register_irq()函数的使用:

在驱动程序初始化或者在设备第一次打开时,首先要调用该函数,以申请使用该irq。其中参数handler指的是要挂入到中断请求队列中的中断服务例程。假定一个程序要对/dev/fd0/(第一个软盘对应的设备)设备进行访问,有两种方式,一是直接访问/dev/fd0/,另一种是在系统上安装一个文件系统,我们这里假定采用第一种。通常将IRQ6分配给软盘控制器,给定这个中断号6,软盘驱动程序就可以发出下列请求,以将其中断服务例程挂入中断请求队列:

request_irq(6, floppy_interrupt,

SA_INTERRUPT|SA_SAMPLE_RANDOM, "floppy", NULL);

我们可以看到,floppy_interrupt()中断服务例程运行时必须禁用中断(设置了SA_INTERRUPT标志),并且不允许共享这个IRQ(清SA_SHIRQ标志)。

在关闭设备时,必须通过调用free_irq()函数释放所申请的中断请求号。例如,当软盘操作终止时(或者终止对/dev/fd0/的I/O操作,或者卸载这个文件系统),驱动程序就放弃这个中断号:

free_irq(6, NULL);

2.对setup_ irq()函数的使用

在系统初始化阶段,内核为了初始化时钟中断设备irq0描述符,在time_init( )函数中使用了下面的语句:

struct irqaction irq0 =

{timer_interrupt, SA_INTERRUPT, 0, "timer", NULL,};

setup_irq(0, &irq0);

首先,初始化类型为irqaction的irq0变量,把handler域设置成timer_interrupt( )函数的地址,flags域设置成SA_INTERRUPT,name域设置成"timer",最后一个域设置成NULL以表示没有用dev_id值。接下来,内核调用setup_x86_irq( ),把irq0插入到IRQ0的中断请求队列:

类似地,内核初始化与IRQ2和IRQ13相关的irqaction描述符,并把它们插入到相应的请求队列中,在 init_IRQ( )函数中有下面的语句:

struct irqaction irq2 =
{no_action, 0, 0, "cascade", NULL,};
struct irqaction irq13 =
{ math_error_irq, 0, 0, "fpu", NULL,};
setup_x86_irq(2, &irq2);
setup_x86_irq(13, &irq13);


上一节   下一节

相关链接


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