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: |
如图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); |


