中断和异常-中断处理

本文详细介绍了中断和异常的硬件处理流程,包括CPU如何判断和响应中断,以及中断处理程序的执行过程。内容涵盖中断请求队列的建立、中断服务程序的注册与注销,以及中断处理程序的执行细节。通过对中断处理的深入理解,有助于掌握操作系统内核对中断的响应和管理机制。

通过上面介绍,中断描述符表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文件中查找到,这里要申请的中断号必须是可共享的。

观察网卡中断,当网络连接断开时出现什么现象,当有网络请求时又出现什么现象。

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值