中断处理

1.中断和异常的硬件处理

     首先,从硬件的角度来看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)(CPU当前执行权限和中断处理程序权限比较),就产生一个“通用保护”异常(中断向量13),因为中断处理程序的特权级不能低于引起中断的程序的特权级。这种情况发生的可能性不大,因为中断处理程序一般运行在内核态,其特权级为0。然后是“门”级检查,把CPL与IDT中第i个门的DPL相比较,如果CPL大于DPL,也就是当前特权级(3)小于这个门的特权级(0)(CPU当前执行权限与门的执行权限比较),CPU就不能“穿过”这个门,于是产生一个“通用保护”异常,这是为了避免用户应用程序访问特殊的陷阱门或中断门。但是请注意,这种“门”级检查是针对一般的用户程序,而不包括外部I/O产生的中断或因CPU内部异常而产生的异常,也就是说,如果产生了中断或异常,就免去了“门”级检查。

(4)检查是否发生了特权级的变化。当中断发生在用户态(特权级为3),而中断处理程序运行在内核态(特权级为0),特权级发生了变化,所以会引起堆栈的更换。也就是说,从用户堆栈切换到内核堆栈。而当中断发生在内核态时,即CPU在内核中运行时,则不会更换堆栈。

 

2.中断请求队列的建立

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

(1)中断服务程序与中断处理程序

    中断服务程序与中断处理程序并不同,在Linux中,15条中断线对应15个终端处理程序,其名依次是IRQ0x00_interrupt()~IRQ0x0f_interruption(),中断处理程序相当于某个中断向量的总处理程序,比如IRQ0x05_interrupt()对应 中断号为5(向量为32+5)的总处理程序,如过这个5号中断由网卡和图形卡共享,则他们分别有自己的中断服务程序。

(2)中断线共享的数据结构

为了让多个设备可以共享一条中断线,内核设置了一个叫irqaction的数据结构:

    typedef  irqreturn_t  (*irq_handler_t)(int , void *);

    struct irqaction {

       irq_handler_t  handler;       //指向一个中断服务程序,该函数有两个参数,第一个参数为中段号IRQ,第二个参数一般为dev_id(唯一的标识某个设备的设备号)

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

       cpumask_t mask;               

       const char *name;               //I/O设备名

       void *dev_id;                         //指定I/O设备的主设备号和次设备号

       struct irqaction *next;          //指向irqaction描述符链表的下一个元素,前提是flags为IRQF_SHARED标志(允许其他设备共享中段线)。

       int irq;                                    

...

    };

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

一条中断线对应一条由该结构体组成的链表,若共享则链表存储了每个设备的中断服务程序,若不共享则只有一个结构体。

(3)注册中断服务程序

    在IDT表初始化完成之初,每个中断服务队列还为空。此时,即使打开中断且某个中断发生了,也得不到实际的服务。因为此时中断服务程序还没有挂入中断请求队列。所以,在设备驱动程序的初始化阶段,必须通过request_irq()函数将相对应中断服务程序挂入中断请求队列,也就是对其进行注册

 int request_irq(unsigned int irq,     void (*handler)(int, void *, struct pt_regs *),       unsigned long irqflags,       const char * devname,      void *dev_id)

第一个参数irq表示要分配的中断号。

第二个参数handler是一个指针,指向处理这个中断的实际中断服务程序。

第三个参数irqflags对应irqaction中的flags。

第四个参数devname是与中断相关的设备名字。这些名字会被/proc/irq和/proc/interrupt文件使用。

第五个参数dev_id主要用于共享中断线。如两个设备共享中断线则需要dev_id作为唯一标识信息,以便从众多的中断服务程序中找到指定的那一个。

这里要说明的是,在驱动程序初始化或者在设备第一次打开的时候,事先要调用request_irq()函数,以申请使用参数中指定的中断请求号irq,另一参数handler指的是要挂入到中断请求队列中的中断服务程序。

注意,request_irq()函数可能会睡眠,因此,不能在中断上下文或其他不允许阻塞的代码中调用该函数。在睡眠不安全的上下文中调用request_irq()函数是一种常见的错误。

(4)注销中断函数

卸载驱动程序时,需要注销相应的中断处理服务程序,并释放中断线。可以调用void free_irq(unsigned int irq,  void * dev_id)来释放中断线。

如果指定的中断线不是共享的,那么,该函数删除处理程序的同时将禁用这条中断线。如果中断线是共享的,则仅删除dev_id所对应的服务程序,而这条中断线本身只有在删除了最后一个服务程序时才会被禁用。由此可以看出为什么唯一的dev_id如此重要。对于共享的中断线,需要一个唯一的信息来区分其上面的多个服务程序,并让free_irq()仅仅删除指定的服务程序。

 

3.中断处理程序的制定

假定外设的驱动程序都已完成了初始化工作,并且已把相应的中断服务例程挂入到特定的中断请求队列。 又假定当前进程正在用户空间运行(随时可以接受中断),且外设已产生了一次中断请求。当这个中断请求通过中断控制器8259A到达CPU的中断请求引线INTR时,CPU就在执行完当前指令后来响应该中断。

CPU从中断控制器的一个端口取得中断向量I,然后根据I从中断描述符表IDT中找到相应的表项,也就是找到相应的中断门。因为这是外部中断,不需要进行“门级”检查,CPU就可以从这个中断门获得中断处理程序的入口地址,假定为IRQ0x05_interrupt。因为这里假定中断发生时CPU运行在用户空间(CPL=3),而中断处理程序属于内核(DPL=0),因此,要进行堆栈的切换。也就是说,CPU从TSS中取出内核栈指针,并切换到内核栈(此时栈还为空)。当CPU进入IRQ0x05_interrupt时,内核栈中除用户栈指针、EFLAGS的内容以及返回地址外再无其他内容。另外,由于CPU进入的是中断门(而不是陷阱门),因此,这条中断线已被禁用,直到重新启用。

这个中断处理程序实际上要调用do_IRQ(),而do_IRQ()要调用handle_IRQ_event()函数(运行挂在这条中断线上的所有中断服务程序);最后这个函数才真正地执行中断服务例程(ISR)。

(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


SVAE_AL L宏把中断处理程序会使用的所有CPU寄存器都保存在栈中。然后,BUILD_COMMON_IRQ 宏调用do_IRQ(  )函数,因为通过CALL调用这个函数,因此,该函数的返回地址被压入栈。当执行完do_IRQ(  ),就跳转到ret_from_intr(  )地址。

(2)do_IRQ()函数

do_IRQ()这个函数处理所有的外设的中断请求。该函数对中断请求队列的处理主要是通过handle_IRQ_event()函数完成的,handle_IRQ_event()函数的主要代码片段为:

retval = 0;
do {
                status |= action->flags;
                retval |= action->handler(irq, action->dev_id, regs);
                action = action->next;
        } while (action);

这个循环依次调用请求队列中的每个中断服务程序。中断服务程序都是在关中断的条件下进行(不包括非屏蔽中断),这也是为什么CPU再穿过中断门时自动关闭中断的原因了。但是,关中断时间绝对不能太长,否则就可能丢失其他重要的中断。也就是说,中断服务程序应该处理最紧急的事情,而把剩下的事情交给另外一部分来处理,即下半部来处理。

 

4.从中断返回

do_IRQ()这个函数处理所有外设的中断请求。当这个函数执行时,内核栈从栈顶到栈底包括:

·      do_IRQ(  )的返回地址

·      由SAVE_ALL 推进栈中的一组寄存器的值

·      ORIG_EAX(即n-256)

·      CPU自动保存的寄存器

内核栈顶包含的就是do_IRQ()的返回地址,这个地址指向ret_from_intr。实际上,ret_from_intr是一段汇编语言的入口点。而实际上中断,异常及系统调用的返回是放在一起实现的,因此,我们常常以函数的形式提到下面三个入口点。

(1)ret_from_intr():终止中断处理程序。

(2)ret_from_sys_call():终止系统调用,即0x80引起的异常。

(3)ret_from_exception():终止除了0x80的所有异常。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值