中断处理基本过程:首先设备产生中断,通过中断线将电信号传递到中断控制器,如果未被屏蔽则会送往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号,看四