通过上面介绍,中断描述符表IDT已被初始化,并具有相应的内容;对于外部中断,还要建立中断请求队列和执行中断处理程序。
一、中断和异常的硬件处理
从硬件角度来看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(0)小于DPL(3),那么CPU不能穿过这个门,则产生一个“通用保护”异常,这是为了避免用户应用程序访问特殊的中断门或陷阱门。
备注:门级检查针对一般的用户程序,不包括外部I/O产生的中断或CPU内部产生的异常,如果产生中断或异常,就免去门级检查。
(4)检查特权级是否发生改变。当中断发生在用户态(特权级为3),而中断处理程序运行在内核态(特权级为0),特权级发生变化,引起堆栈切换,从用户态堆栈切换到内核态堆栈。当中断发生在内核态时,即CPU运行在内核中时,则不会切换堆栈,如图5.4所示。

总结如下:
从图5.4看出,当从用户态堆栈切换到内核态堆栈时,先把用户态堆栈的值压入中断处理程序的内核态堆栈中,同时把EFLAGS寄存器自动压入堆栈,然后把中断进程的返回地址压入堆栈。如果异常产生一个硬错误码,则将这个硬错误码也保存在堆栈中。如果特权级没有发生变化,压入堆栈的内容如图5.4(b)所示。此时,CS:EIP的值是IDT表中第i项门描述符的段选择符和偏移量的值,CPU跳转到中断或异常处理程序。
二、中断请求队列的建立
由于硬件限制,很多外部设备必须共享中断线,例如,一些PC(个人电脑)配置可以把同一条中断线分配给网卡和图形卡。由此看来,让每个中断源都必须占用一条中断线不现实。所以,仅仅用中段描述符表IDT并不能提供中断产生的所有信息,内核必须对中断线给出进一步的描述。在Linux中,为每个中断请求IRQ设置一个队列,即中断请求队列。
1、中断服务程序与中断处理程序
这里提到的中断服务程序与中断处理程序概念不同。在Linux中15条中断线对应15个中断处理程序,其名字依次为IRQ0x00_interrupt(),IRQ0x01_interrupt(),......,IRQ0x0f_interrupt()。具体来说,中断处理程序相当于某个中断向量的总处理程序,例如IRQ0x05_interrupt()是中断号为5的总处理程序,如果5号中断由网卡和图形卡共享,则网卡和图形卡分别有其对应的中断服务程序。
2、中断共享的数据结构
为让多个设备能共享一条中断线,内核定义一个irqaction的数据结构:
typedef irqreturn_t (*irq_handler_t)(int, void *); // 定义函数指针类型
struct irqaction {
irq_handler_t handler; //用户注册的中断服务程序,中断发生时会执行这个中断服务程序
unsigned long flags; //中断标志,注册中断时设置,比如上升沿中断,下降沿中断等
cpumask_t mask; //中断掩码
const char *name; //中断名称,产生中断的硬件的名字
void *dev_id; //设备id
struct irqaction *next; //后继指针,指向下一个元素
int irq; //中断号
struct proc_dir_entry *dir; //指向IRQn相关的/proc/irq/
};
对每个成员描述如下:
(1)handler
指向一个具体的I/O设备的中断服务程序,该函数有两个参数,第一个参数为中断号IRQ,第二个参数为void类型的指针,该指针一般传入dev_id(唯一地标识某个设备的设备号)的值。
(2)flags
用一组标志描述中断线与I/O设备之间的关系,具体如下。
IRQF_DISABLED
中断服务程序执行时必须禁止中断。
IRQF_SHARED
允许其它设备共享这条中断线。
IRQF_SAMPLE_RANDOM
可以把这个设备看作是随机事件发生源;因此,内核可以用它做随机数发生器。
(3)name
I/O设备名
(4)dev_id
指定I/O设备的主设备号和次设备号
(5)next
指向irqaction描述符链表的下一个元素,前提是flags为IRQF_SHARED标志。共享同一条中断线的每个硬件设备都有其对应的中断服务程序,链表中的每个元素就是对应设备及其中断服务程序的描述。
(6)irq
中断号
3、注册中断服务程序
在中断描述符表IDT初始化完成之处,每个中断服务队列为空。这时,即使打开中断并且某个外部设备真的发生中断,也得不到实际的服务。因为CPU虽通过中断门进入某个中断向量的总处理程序,例如IRQ0x05_interrupt(),但具体的中断服务程序还没有挂入中断请求队列。所以,在外部设备驱动程序初始化阶段,必须通过request_irq()函数将相应的中断服务程序挂入中断请求队列,也就是对其进行注册。
request_irq()函数原型为:
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long irq_flags,const char *dev_name, void *dev_id);
irq:表示要分配的中断号。对某些设备,例如传统PC设备上的系统时钟或键盘,这个值通常是预先设定的。而对于大多数其它设备来说,这个值可以通过探测获取,也可以通过编程动态确定。
handler:是一个函数指针,指向处理这个中断的实际中断服务程序。操作系统接收到中断,就会调用中断服务程序函数。注意,handler函数的原型是特定的,它接收两个参数,并返回类型为irqreturn_t的返回值。
irq_flags:中断的标志,可以为0,也可能是IRQF_SAMPLE_RANDOM,IRQF_SHARED或IRQF_DISABLED这几个标志的位掩码。
dev_name:与中断相关的设备的名字,例如,PC上键盘中断对应的这个值为keyboard。这些名字在/proc/irq和/proc/interrupt文件中使用,以便与用户通信。
dev_id:主要用于共享中断线。当一个中断服务程序需要释放时,dev_id将提供唯一的标志信息,以便从共享中断线的诸多服务程序中删除指定的那一个。如果没有这个参数,那么内核不可能知道在给定的中断线上要删除哪一个处理程序。如果无须中断线,那么该参数的值设置为NULL,但如果中断线是被共享的,那么就必须传递唯一的信息。
备注:在驱动程序初始化或者在设备第一次打开时,首先要调用注册中断request_irq()函数,以申请使用参数中指明的中断请求号irq,另一个参数handler是指要挂入到中断请求队列中的中断服务程序。假定一个程序要对/dev/fd0/(第一个软盘对应的设备)设备进行访问,通常将IRQ6分配给软盘控制器,给定中断号6,软盘驱动程序可以发出下列请求,以将其中断服务程序挂入中断请求队列:
request_irq(6, floppy_interrupt, IRQF_DISABLED|IRQF_SAMPLE_RANDOM, "floppy", NULL);
可以看到,floppy_interrupt()中断服务程序运行时必须禁止中断(设置了IRQF_DISABLED标志),并且不允许共享这个IRQ(清IRQF_SHARED标志),但允许根据这个中断发生的时间产生随机数(设置了IRQF_SAMPLE_RANDOM标志,用于建立熵池,以供系统产生随机数使用)。
在Linux内核中,熵池是环境噪声数据流的集合,被作为种子,用于生成随机数。
总结:request_irq()函数可能会睡眠,用在进程上下文中,因此,不能在中断上下文或其它不允许阻塞的代码中调用该函数。
4、注销中断服务程序
卸载驱动程序时,需要注销相应的中断处理服务程序,并释放中断线。调用free_irq(unsigned int irq, void *dev_id)释放中断线。
如果指定的中断线不是共享的,那么,该函数删除处理程序的同时将禁用这条中断线。如果中断线是共享的,则仅删除dev_id所对应的服务程序,而这条中断线本身只有在删除了最后一个服务程序时才会被禁用。对于共享的中断线,需要一个唯一的信息来区分其上面的多个服务程序,并让free_irq()仅仅删除指定的服务程序。不管共享或不共享,如果dev_id非空,它都必须与需要删除的服务程序相匹配。
总结:必须从进程上下文中调用free_irq()。
三、中断处理程序的执行
已了解中断机制及有关的初始化工作。从中断请求的发生到CPU的响应,再到中断处理程序的调用和返回,根据这个思路,体会Linux内核对中断的响应及处理。
假定外部设备的驱动程序初始化工作都已完成,并且已把相应的中断服务程序挂入到特定的中断请求队列。又假定当前进程正在用户空间运行(随时可接受中断),且外设已产生了一次中断请求。当这个中断请求通过中断控制器8259A到达CPU的中断请求引线INTR时,CPU就会在执行完当前指令后来响应该中断。
CPU从中断控制器的一个端口取得中断向量I(8位无符号整数),然后根据I从中断描述符表IDT中找到相应的表项,也就是找到相应的中断门。因为这是外部中断,不需要进行门级检查,CPU就可以从这个中断门获得中断处理程序的入口地址,假定中断处理程序IRQ0x05_interrupt。因为这里假定中断发生时CPU运行在用户空间(CPL=3),而中断处理程序属于内核(DPL=0),因此,要进行堆栈的切换。当CPU进入IRQ0x05_interrupt时,内核栈如图5.4(a)所示,栈中除用户栈指针、EFLAGS的内容以及返回地址外再没有其它内容。另外,由于CPU进入的是中断门,因此,这条中断线已被禁用,直到重新启用。
用IRQn_interrupt表示从IRQ0x00_interrupt到IRQ0x0f_interrupt的任意一个中断处理程序。这个中断处理程序调用do_IRQ()函数。do_IRQ()对所接收的中断进行应答,并禁止这条中断线,然后要确保这条中断线上有一个有效的中断服务程序,而且这个例程已经启动但目前还没有执行。这时,do_IRQ()调用handle_IRQ_event()来运行挂在这条中断线上的所有中断服务程序。其调用关系如图所示:

1、中断处理程序IRQn_interrupt
一个中断处理程序主要包含如下两条语句。
IRQn_interrupt:
pushl $n-256
jmp common_interrupt
其中第一条语句把中断号减256的结果保存在栈中,这是每个中断处理程序唯一的不同之处。然后,所有的中断处理程序都跳转到一段相同的代码common_interrupt。这段代码的汇编语言片段如下。
common_interrupt:
SAVE_ALL
call do_IRQ
jmp ret_from_intr
备注:SAVE_ALL宏把中断处理程序会使用的所有CPU寄存器都保存在栈中。然后,调用do_IRQ()函数,因为通过CALL调用这个函数,所以,该函数的返回地址被压入栈。当执行完do_IRQ(),跳转到ret_from_intr()地址。
2、do_IRQ()函数
do_IRQ()这个函数处理所有外设的中断请求。do_IRQ()对中断请求队列的处理主要是通过调用handle_IRQ_event()函数完成的,handle_IRQ_event()函数如下:
asmlinkage int handle_IRQ_event(unsigned int irq, struct pt_regs *regs,struct irqaction *action)
{
int status = 1;
int retval = 0;
if (!(action->flags & SA_INTERRUPT))
local_irq_enable();
do {
status |= action->flags;
retval |= action->handler(irq, action->dev_id, regs);
action = action->next;
} while (action);
if (status & SA_SAMPLE_RANDOM)
add_interrupt_randomness(irq);
local_irq_disable();
return retval;
}
这个循环依次调用请求队列中的每个中断服务程序。
备注:中断服务程序都在关中断的条件下进行(可屏蔽中断),这也是为什么CPU在穿过中断门时自动关闭中断的原因。但是,关中断时间不能太长,否则可能丢失其它重要的中断。也就是说,中断服务程序应该处理最紧急的事情,而把剩下的事情交给另外一部分来处理,即下半部来处理。
四、从中断返回
do_IRQ()函数处理所有外设的中断请求。当这个函数执行时,内核栈从栈顶到栈底包括以下内容。
(1)do_IRQ()的返回地址。
(2)由SAVE_ALL推进栈中的一组寄存器的值。
(3)n-256。
(4)CPU自动保存的寄存器
可以看出,内核栈顶包含的是do_IRQ()的返回地址,这个地址指向ret_from_intr。实际上,ret_from_intr是一段汇编语言的入口点,为描述简单起见,以函数的形式提及它。虽然这里讨论的是中断的返回,但实际上中断、异常及系统调用的返回是放在一起实现的,常常以函数的形式提到下面这三个入口点。
(1)ret_from_intr():终止中断处理程序。
(2)ret_from_sys_call():终止系统调用,即由0x80引起的异常。
(3)ret_from_exception():终止除了0x80的所有异常。
备注:调用恢复中断现场的宏RESTORE_ALL,彻底从中断返回。
五、中断的简单应用
通过实例说明如何编写中断服务程序。编写内核模块,计算两次中断的时间间隔。
说明:在内核中,时间用无符号长整型jiffies表示,这是一个全局变量,表示自系统启动以来的时钟节拍数。另外,通过给内核模块传递参数的形式,把设备名和对应的中断号irq传给内核模块。
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/interrupt.h>
static int irq; /* 模块参数: 中断号 */
static char *interface; /* 模块参数: 设备名 */
static int count = 0; /* 统计插入模块期间发生的中断次数 */
module_param(interface, charp, 0644);
module_param(irq, int, 0644);
/* 中断服务程序 */
static irqreturn_t intr_handler(int irq, void *dev_id)
{
static long interval = 0; // 局部静态变量仅被初始化一次
if (count == 0)
interval = jiffies;
interval = jiffies - interval;// 计算两次中断之间的间隔,时间单位为节拍
printk("The interval between two interrupts is %ld\n", interval);
interval = jiffies;
count++;
return IRQ_NONE;
}
static __init int intr_init(void)
{
if(request_irq(irq,intr_handler, IRQF_SHARED, interface, &irq)){ // 注册中断
printk(KERN_ERR "Fails to register IRQ %d\n", irq);
return -EIO;
}
printk("%s Request on IRQ %d succeed\n", interface, irq);
return 0;
}
static __exit void intr_exit(void)
{
printk("The %d interrupts happened on irq %d\n", count, irq);
free_irq(irq, &irq);
printk("Freeing IRQ %d\n", irq);
}
module_init(intr_init);
module_exit(intr_exit);
MODULE_LICENSE("GPL");
# Makefile
obj-m := test.o # 产生test模块的目标文件
CURRENT_PATH := $(shell pwd) # 内核模块源代码所在的当前路径
LINUX_KERNEL := $(shell uname -r) # Linux内核源代码的当前版本
LINUX_KERNEL_PATH := /usr/src/linux-headers-$(LINUX_KERNEL) # Linux内核源代码的绝对路径
all:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules # 编译模块
clean:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean # 清理
备注:内核模块编译完成后,安装内核模块时需要带参数interface和irq,interface是设备名,irq是所要申请的中断号,可以从/proc/interrupts文件中查找到,这里要申请的中断号必须是可共享的。
观察网卡中断,当网络连接断开时出现什么现象,当有网络请求时又出现什么现象。
本文详细介绍了中断和异常的硬件处理流程,包括CPU如何判断和响应中断,以及中断处理程序的执行过程。内容涵盖中断请求队列的建立、中断服务程序的注册与注销,以及中断处理程序的执行细节。通过对中断处理的深入理解,有助于掌握操作系统内核对中断的响应和管理机制。

被折叠的 条评论
为什么被折叠?



