Linux中断系列之中断或异常处理(四)

2019-07-12 19:35发布

中断处理基本过程:首先设备产生中断,通过中断线将电信号传递到中断控制器,如果未被屏蔽则会送往CPU的INTR引脚。CPU停止当前任务,根据得到的中断向量,从IDT找到相应的门描述符,可从中获取中断服务程序的地址并执行。
异常处理过程:不需要产生电信号,当异常发生时,CPU通过特定的中断向量,从IDT找到相应的门描述符,可从中获取异常服务程序的地址并执行。
一、中断控制器的工作 (以8259A为例)
    1、中断控制器是外部设备和CPU之间中断信号的桥梁,中断控制器主要有三个寄存器:IRR ISR IMR
  (1) IRR:中断请求队列,共8bit,对应八个引脚。中断到来时,若未被屏蔽,则IRR相应位置置1,表示收到中断请求,但未交给CPU。
  (2) ISR:中断服务寄存器,共8bit,对应八个引脚。当IRR中某个中断请求送往CPU后,该位置1,表示已递交给CPU,但CPU未处理完。
  (3) IMR:中断屏蔽寄存器,共8bit,对应八个引脚,当某位置1,表示对应中断引脚被屏蔽。
2、引脚的优先级问题:有两种优先级管理方式,固定优先级和循环优先级,固定优先级不可改变,而循环优先级在系统工作过程中可以动态改变。
3、8259A支持两种嵌套模式:一般嵌套模式和特殊嵌套模式
  一般嵌套模式,指在一个中断被处理的时候,一直相比低优先级或同等优先级会自动被屏蔽,而高优先级则可以响应。
  特殊嵌套模式,与一般不同的是,同优先级的中断也能响应。
4、8259A在中断处理过程中所要完成的工作:
  (1) 中断请求:设备发起中断,与之相连的IR引脚产生电信号,若对应中断未被屏蔽,IMR对应位为0,则将IRR对应位置1,通过INT引脚向CPU的INTR引脚发出请求信号。若被屏蔽,则丢弃。
  (2) 中断响应:CPU响应中断三个条件:至少有一个中断请求;CPU允许中断;当前指令执行完毕。
      8259A在提交中断请求信号,CPU可能在执行一条指令,并不会立即响应。此时还可能有其他IRQ线也产生了中断请求。
  当CPU执行完一条指令后,回去检查INTR管脚是否有信号,若有则检查EFLAGS寄存器中断允许标志IF是否为1,为1,则通过INTA引脚应答8259A。
  (3) 优先级判定:8259A收到CPU应答后,在IRR挑取优先级最高的中断,将其ISR对应的位置1。表示中断正在接收CPU的处理。
  (4) 提交中断向量:CPU通过INTA第二次发送脉冲,8259A根据被设置的起始信号量,计算最高的优先级的中断向量(比如起始向量号为16,当前中断请求为IRQ3,则得到的中断向量号为19),通过数据线递交给CPU。
  (5) 中断结束:递交中断向量后,8259A会检测是否AEOI模式,若是自动清除中断请求信号,将ISR相应位置0。若不是则等待CPU发送EOI指令(由中断服务程序发起),收到EOI,ISR相应位会清零。
  注:也就是说,在递交中断向量之前,高优先级的中断可以打断低优先级,而递交之后则不能。因为内核接收到中断向量后直接进入中断门,同时CPU会清零EFLAGS的IF位。    
      当CPU在正在处理IRQ1时,IRQ2线来了信号,8259A会自动将其与ISR中的值比较,ISR上有IRQ1置1,则仅仅IRR上置1。若IRQ0来了信号,相同的比较,发现IRQ0优先级大,此时8259A将IRQ0的ISR位置1,并向CPU发出中断请求。因为此时正在处理IRQ1,所以ISR上ISR0和ISR1均为1。
  8259A是AEOI模式,ISR中的位总是被清零(在提交中断向量号后),意味着如果有新的中断线来了信号,8259A会立刻向CPU提交中断请求,即使在处理IRQ0的中断,CPU只会简单应答8259A。因此这种情况下低优先级的中断会打断高优先级的中断服务程序。
二、CPU的工作
1、确定中断或异常的中断向量i(0~255之间)。可屏蔽中断的中断向量从中断控制器获得,不可屏蔽中断或异常的中断向量是固定的。
2、通过IDTR寄存器找到IDT,读取第i项(即第i个门描述符)。
3、进行特权级检查,将当前CPU的特权级CPL与门描述符的DPL比较,若小于或等于DPL,则进入门,之后,将获取门描述符中所指向代码段的DPL与CPL比较,如果小于或等于CPL,则执行中断服务程序。如果两次比较其中一次不符,则进入通用保护异常(中断向量13)。
4、若上述过程(1-3)检测到特权级发生变化,则需要进行堆栈转换。中断或异常程序运行在内核态(特权级0),而中断或异常在用户态(特权级0)时发生,此时需要从用户态(特权级3)变成内核态(特权级0),所以要将堆栈从用户态堆栈切换到内核态堆栈。
5、如果是异常,则CPU将异常码压入当前的堆栈。
6、如果是中断,进入中断门后,CPU会清零EFLAGS的IF位,即关闭所有的可屏蔽中断;如果是异常则不清零此位。(见handle_IRQ_event代码)
7、进入中断或异常服务程序执行
三、内核对中断的处理
过程如下:
      common_interrupt
              ↓
         do_IRQ()                     handle_IRQ_event               →                             运行该IRQ中断请求队列中的所有中断服务程序
              ↓                                          ↑(否)                                                                    ↓
      该IRQ公共服务程序 → 该IRQ中断请求队列是否为空?  (是)→              ret_from_intr      
    
1、所有中断(不可屏蔽中断除外)的服务程序在init_IRQ函数均被初始化为interrupt[i],数组每一项指向一个代码片段,此代码片段将中断向量压入堆栈外,之后调用了一个公共处理程序common_interrupt
  interrupt数组的代码片段                        例如:中断向量32 即IRQ0
     pushl $~(vector)//vector表示中断向量号            pushl $~(0)                 
 CFI_ADJUST_CFA_OFFSET 4                           CFI_ADJUST_CFA_OFFSET 4
 jmp common_interrupt                              jmp common_interrupt
  关于common_interrupt公共处理程序,每个中断发生后都会调用此函数。此程序功能如下:   (1) 保存现场,将中断发生前所有的寄存器的值保存在堆栈中;
  (2) 调用do_IRQ函数,函数功能: (注:在IRQ层引入之前,所有的IRQ线都共用一个公共的服务程序__do_IRQ对中断进行处理,它们对应的结构体都不同引入IRQ层以后的;引入后,后面讲解)       ① 从堆栈中取出中断向量号,由中断向量号找到
      ② 调用irq_enter函数进入中断上下文,preempt_count加一,表示禁止其他进程抢占,调用irq_exit函数,preempt_count减一;
      ③ 调用函数__do_IRQ,看源码
      其功能:<1> 屏蔽当前的IRQ,禁止该IRQ线的中断传递。
                      <2> 判断中断请求队列是否为空,即是否有一个或多个中断服务程序,
     如果有则调用handle_IRQ_event函数进行遍历,执行相应程序处理中断 /** * do_IRQ执行与一个中断相关的所有中断服务例程.这是在引用IRQ层之前的处理方式,也属于插入讲解引入IRQ层后面讲解 */ fastcall unsigned int do_IRQ(struct pt_regs *regs) { /* high bits used in ret_from_ code */ int irq = regs->orig_eax & 0xff; #ifdef CONFIG_4KSTACKS union irq_ctx *curctx, *irqctx; u32 *isp; #endif /** * irq_enter增加中断嵌套计数 */ irq_enter(); #ifdef CONFIG_DEBUG_STACKOVERFLOW /* Debugging check for stack overflow: is there less than 1KB free? */ { long esp; __asm__ __volatile__("andl %%esp,%0" : "=r" (esp) : "0" (THREAD_SIZE - 1)); if (unlikely(esp < (sizeof(struct thread_info) + STACK_WARN))) { printk("do_IRQ: stack overflow: %ld ", esp - sizeof(struct thread_info)); dump_stack(); } } #endif #ifdef CONFIG_4KSTACKS /** * 如果中断栈使用不同的的栈,就需要切换栈. */ curctx = (union irq_ctx *) current_thread_info(); irqctx = hardirq_ctx[smp_processor_id()]; /* * this is where we switch to the IRQ stack. However, if we are * already using the IRQ stack (because we interrupted a hardirq * handler) we can't do that and just have to keep using the * current stack (which is the irq stack already after all) */ /** * 当前在使用内核栈,而不是硬中断请求栈.就需要切换栈 */ if (curctx != irqctx) { int arg1, arg2, ebx; /* build the stack frame on the IRQ stack */ isp = (u32*) ((char*)irqctx + sizeof(*irqctx)); /** * 保存当前进程描述符指针 */ irqctx->tinfo.task = curctx->tinfo.task; /** * 把esp栈指针寄存器的当前值存入irqctx的thread_info(内核oops时使用) */ irqctx->tinfo.previous_esp = current_stack_pointer; /** * 将中断请求栈的栈顶装入esp,isp即为中断栈顶 * 调用完__do_IRQ后,从ebx中恢复esp */ asm volatile( " xchgl %%ebx,%%esp " " call __do_IRQ " " movl %%ebx,%%esp " : "=a" (arg1), "=d" (arg2), "=b" (ebx) : "0" (irq), "1" (regs), "2" (isp) : "memory", "cc", "ecx" ); } else/* 否则,发生了中断嵌套,不用切换 */ #endif __do_IRQ(irq, regs); /** * 递减中断计数器并检查是否有可延迟函数 */ irq_exit(); /** * 结束后,会返回ret_from_intr函数. */ return 1; } // fastcall unsigned int __do_IRQ(unsigned int irq, struct pt_regs *regs) { //得到对应irq号的irq_desc结构体 irq_desc_t *desc = irq_desc + irq; struct irqaction * action; unsigned int status; /** * 中断发生次数计数. */ kstat_this_cpu.irqs[irq]++; if (desc->status & IRQ_PER_CPU) { irqreturn_t action_ret; /* * No locking required for CPU-local interrupts: */ desc->handler->ack(irq); action_ret = handle_IRQ_event(irq, regs, desc->action); if (!noirqdebug) note_interrupt(irq, desc, action_ret); desc->handler->end(irq); return 1; } /** * 虽然中断是关闭的,但是还是需要使用自旋锁保护desc,在多处理器中,防止多CPU对主IRQ描述符进行操作,但CPU无影响 */ spin_lock(&desc->lock); /** * 如果是旧的8259A PIC,ack就是mask_and_ack_8259A,它应答PIC上的中断并禁用这条IRQ线.屏蔽IRQ线是为了确保在这个中断处理程序结束前, * CPU不进一步接受这种中断的出现. * do_IRQ是以禁止本地中断运行,事实上,CPU控制单元自动清eflags寄存器的IF标志.因为中断处理程序是通过IDT中断门调用的. * 不过,内核在执行这个中断的中断服务例程之前可能会重新激活本地中断. * 在使用APIC时,应答中断信赖于中断类型,可能是ack,也可能延迟到中断处理程序结束(也就是应答由end方法去做). * 无论如何,中断处理程序结束前,本地APIC不进一步接收这种中断,尽管这种中断可能会被其他CPU接受. */ desc->handler->ack(irq); /* * REPLAY is when Linux resends an IRQ that was dropped earlier * WAITING is used by probe to mark irqs that are being tested */ /** * 初始化主IRQ描述符的几个标志.设置IRQ_PENDING标志.也清除IRQ_WAITING和IRQ_REPLAY * 这几个标志可以很好的解决中断重入的问题. * IRQ_REPLAY标志是"挽救丢失的中断"所用.在此不详述. */ status = desc->status & ~(IRQ_REPLAY | IRQ_WAITING); status |= IRQ_PENDING; /* we _want_ to handle it */ /* * If the IRQ is disabled for whatever reason, we cannot * use the action we have. */ action = NULL; /** * IRQ_DISABLED和IRQ_INPROGRESS被设置时,什么都不做(action==NULL) * 即使IRQ线被禁止,CPU也可能执行do_IRQ函数.首先,可能是因为挽救丢失的中断,其次,也可能是有问题的主板产生伪中断. * 所以,是否真的执行中断代码,需要根据IRQ_DISABLED标志来判断,而不仅仅是禁用IRQ线. * IRQ_INPROGRESS标志的作用是:如果一个CPU正在处理一个中断,那么它会设置它的IRQ_INPROGRESS.这样,其他CPU上发生同样的中断 * 就可以检查是否在其他CPU上正在处理同种类型的中断,如果是,就什么都不做,这样做有以下好处: * 一是使内核结构简单,驱动程序的中断服务例程式不必是可重入的.二是可以避免弄脏当前CPU的硬件高速缓存. */ if (likely(!(status & (IRQ_DISABLED | IRQ_INPROGRESS)))) { action = desc->action;//得到此irq的中断服务程序的地址 status &= ~IRQ_PENDING; /* we commit to handling */ status |= IRQ_INPROGRESS; /* we are handling it */ } desc->status = status; /* * If there is no IRQ handler or it was disabled, exit early. * Since we set PENDING, if another processor is handling * a different instance of this same irq, the other processor * will take care of it. */ /** * 当前面两种情况出现时,不需要(或者是不需要马上)处理中断.就退出 * 或者没有相关的中断服务例程时,也退出.当内核正在检测硬件设备时就会发生这种情况. */ if (unlikely(!action)) goto out; /* * Edge triggered interrupts need to remember * pending events. * This applies to any hw interrupts that allow a second * instance of the same irq to arrive while we are in do_IRQ * or in the handler. But the code here only handles the _second_ * instance of the irq, not the third or fourth. So it is mostly * useful for irq hardware that does not mask cleanly in an * SMP environment. */ /** * 这里是需要循环处理的,并不是说调用一次handle_IRQ_event就行了. */ for (;;) { irqreturn_t action_ret; /** * 现在打开自旋锁了,那么,其他CPU可能也接收到同类中断,并设置IRQ_PENDING标志. * xie.baoyou注:请注意开关锁的使用方法.有点巧妙,不可言传. */ spin_unlock(&desc->lock); /** * 调用中断服务例程.处理挂在此IRQ线上的所有中断例程 */ action_ret = handle_IRQ_event(irq, regs, action); spin_lock(&desc->lock); if (!noirqdebug) note_interrupt(irq, desc, action_ret); /** * 如果其他CPU没有接收到同类中断,就退出 * 否则,继续处理同类中断. */ if (likely(!(desc->status & IRQ_PENDING))) break; /** * 清除了IRQ_PENDING,如果再出现IRQ_PENDING,就说明是其他CPU上接收到了同类中断. * 注意,IRQ_PENDING仅仅是一个标志,如果在调用中断处理函数的过程中,来了多次的同类中断,则意味着只有一次被处理,其余的都丢失了. */ desc->status &= ~IRQ_PENDING; } desc->status &= ~IRQ_INPROGRESS; out: /* * The ->end() handler has to deal with interrupts which got * disabled while the handler was running. */ /** * 现在准备退出了,end方法可能是应答中断(APIC),也可能是通过end_8259A_irq方法重新激活IRQ(只要不是伪中断). */ desc->handler->end(irq); /** * 好,工作已经全部完成了,释放自旋锁吧.注意两个锁的配对使用方法. */ spin_unlock(&desc->lock); return 1; } //插入handle_IRQ_event代码 irqreturn_t handle_IRQ_event(unsigned int irq, struct irqaction *action) { irqreturn_t ret, retval = IRQ_NONE; unsigned int status = 0; handle_dynamic_tick(action); /*因为CPU再通过中断门后会自动关闭所有中断,即CPU会清零EFLAGS的IF位,如果这里希望能打开, 需要在使用request_irq函数注册中断程序时,不要设置IRQF_DISABLED标志(见CPU工作,第六步)*/ if (!(action->flags & IRQF_DISABLED))/* 第一个中断ISR不要求关中断运行 */ local_irq_enable_in_hardirq();/* 开中断 */ /* 遍历ISR链表 */ do { /* 回调ISR处理函数 */ ret = action->handler(irq, action->dev_id); if (ret == IRQ_HANDLED)/* 该ISR响应了中断 */ status |= action->flags; retval |= ret; /* 下一个ISR */ action = action->next; } while (action); if (status & IRQF_SAMPLE_RANDOM)/* 处理随机数种子 */ add_interrupt_randomness(irq); local_irq_disable();/* 回到上层函数,关闭中断 */ return retval; }

(承接上面)关于common_interrupt公共处理程序,每个中断发生后都会调用此函数。此程序功能如下:
  (1) 保存现场,将中断发生前所有的寄存器的值保存在堆栈中;
  (2) 调用do_IRQ函数,函数功能:
(注:在IRQ层引入之后,对不同类型中断的处理流程做出了区分,各类型中断有单独的公共服务程序,比如:边沿触发中断handle_edge_irq,电平触发中断对应handle_level_irq函数,8259A就是对应电平触发。          中断系列六讲解:通用IRQ层)
      ① 从堆栈中取出中断向量号,由中断向量号找到
      ② 调用irq_enter函数进入中断上下文,preempt_count加一,表示禁止其他进程抢占,调用irq_exit函数,preempt_count减一;
      ③ 调用IRQ自身的公共服务程序,详细看源码。
      ④ 调用每个IRQ自身的公共服务程序,对不同类型中断的处理流程做出了区分,并分别设置了一些对应的服务程序;
      其功能:<1> 屏蔽当前的IRQ,禁止该IRQ线的中断传递。
                      <2> 判断中断请求队列是否为空,即是否有一个或多个中断服务程序,
     如果有则调用handle_IRQ_event函数进行遍历,执行相应程序处理中断
fastcall unsigned int do_IRQ(struct pt_regs *regs) { struct pt_regs *old_regs; /* high bit used in ret_from_ code */ int irq = ~regs->orig_eax;//去除IRQ号 struct irq_desc *desc = irq_desc + irq;//得到对应的irq_desc结构体 #ifdef CONFIG_4KSTACKS union irq_ctx *curctx, *irqctx; u32 *isp; #endif if (unlikely((unsigned)irq >= NR_IRQS)) { printk(KERN_EMERG "%s: cannot handle IRQ %d ", __FUNCTION__, irq); BUG(); } old_regs = set_irq_regs(regs); irq_enter();//进入中断上下文 #ifdef CONFIG_DEBUG_STACKOVERFLOW /* Debugging check for stack overflow: is there less than 1KB free? */ { long esp; __asm__ __volatile__("andl %%esp,%0" : "=r" (esp) : "0" (THREAD_SIZE - 1)); if (unlikely(esp < (sizeof(struct thread_info) + STACK_WARN))) { printk("do_IRQ: stack overflow: %ld ", esp - sizeof(struct thread_info)); dump_stack(); } } #endif #ifdef CONFIG_4KSTACKS curctx = (union irq_ctx *) current_thread_info(); irqctx = hardirq_ctx[smp_processor_id()]; /* * this is where we switch to the IRQ stack. However, if we are * already using the IRQ stack (because we interrupted a hardirq * handler) we can't do that and just have to keep using the * current stack (which is the irq stack already after all) */ if (curctx != irqctx) { int arg1, arg2, ebx; /* build the stack frame on the IRQ stack */ isp = (u32*) ((char*)irqctx + sizeof(*irqctx)); irqctx->tinfo.task = curctx->tinfo.task; irqctx->tinfo.previous_esp = current_stack_pointer; /* * Copy the softirq bits in preempt_count so that the * softirq checks work in the hardirq context. */ irqctx->tinfo.preempt_count = (irqctx->tinfo.preempt_count & ~SOFTIRQ_MASK) | (curctx->tinfo.preempt_count & SOFTIRQ_MASK); asm volatile( " xchgl %%ebx,%%esp " " call *%%edi " " movl %%ebx,%%esp " : "=a" (arg1), "=d" (arg2), "=b" (ebx) : "0" (irq), "1" (desc), "2" (isp), "D" (desc->handle_irq) : "memory", "cc" ); } else #endif //比如8259A,handle_irq就指向用户设定与其对应的公共服务函数,8259A上的所有中断线的函数指针handle_irq都会指向这个公共服务函数。在初始化时 ,set_irq_chip_and_handler_name函数,将handle_level_irq作为IRQ0~IRQ15的公共服务函数,因为他们触发的是这一类型的中断 desc->handle_irq(irq, desc);//根据irq号和其对应的irq_desc结构体,执行对应的中断服务程序 irq_exit(); set_irq_regs(old_regs); return 1; }

2、中断结束后,返回common_interrupt,然后调用ret_form_intr函数从中断返回,如果返回到用户空间,并且被中断进程设置了标志TIF_NEED_RESCHD,则ret_form_intr会调用schedule函数进行重新调度。
  如果返回内核空间,则会检查被中断进程的preempt_coount是否为0,只有为0时才能调用schedule函数。
四、内核对异常的处理
    与中断不同,各种异常都具有固定的中断向量和固定的异常服务程序(在trap_init中指定)。当异常发生时,将直接跳入相应的服务程序中。每个异常都有自己的代码段
    如同中断跳入common_interrupt函数中一样,异常则跳入汇编函数error_code中,也是公共处理程序,这个程序对每个异常都一样
    例如堆栈异常:
    ENTRY(stack_segment)    //堆栈异常处理程序入口
RING0_EC_FRAME
pushl $do_satck_segment  //异常服务程序地址压入堆栈
CFI_ADJUST_CFA_OFFSET 4 
jmp error_code           //调用对应的服务函数,这个异常就调用do_satck_segment程序
CFI_ENDPROC
END(stack_sehment)

五、总结:中断如何进行的
1、8259A接收到中断后,通过INTR向CPU发起中断请求;
2、CPU响应(INTA)后,8259A进行优先级判定,将寄存器ISR此时最高优先级位置1,将IRR对应位置0;
3、8259A等待CPU二次信号(INTA),提交中断向量i,可能为(0~255)之间的值,其中异常除外;
4、基本上8259A都是可屏蔽中断(32~255),还有不可屏蔽中断和异常中断向量是确定的(0~31);
5、根据IDTR找到的IDT,CPU用中断向量从IDT中读取第i项(即第i个门描述符);
6、CPU进行特权级检查,来决定是否能通过门;
7、CPU判定上述过程特权级是否发生变化;
8、进行异常与中断的不同处理;(5~8)此过程是由硬件完成;
9、如果i(0~31)是此时CPU通过门描述直接进入服务程序;
  如果i(32~255)此时CPU通过门描述符跳到其对应的代码段(在初始化的时候代码段的地址也存储在interrupt[i]中,然后存入中断门中)。
10、如果是0~31就直接执行中断服务程序了,看13步;如果是32~255先执行代码片段,将中断向量号压入堆栈中,然后跳到公共处理程序comm_interrupt。
11、comm_interrupt执行的任务是先保存现场,中断前寄存器的值保存在堆栈中,然后调用函数do_IRQ;
12、do_IRQ函数任务(参看三),其实对于32~255号,中断的处理是’分-总-分‘的结构,分:先是硬件通过IRQ号找到门描述符,然后找到各自的代码片段,转向内核处理,总:每个代码片段又会调用同一个汇编函数common_interrupt,
   分:接下来又通过irq号找到对应的irq_desc结构体,然后根据这个结构体执行自身的中断服务程序。
13、第9步,对于0~31号,看四