Linux 中,当外设触发中断后,大体处理流程如下:
a -- 具体CPU architecture相关的模块会进行现场保护,然后调用machine driver对应的中断处理handler;
b -- machine driver对应的中断处理handler中会根据硬件的信息获取HW interrupt ID,并且通过irq domain模块翻译成IRQ number;
c -- 调用该IRQ number 对应的high level irq event handler,在这个high level的handler中,会通过和interupt controller交互,进行中断处理的flow control(处理中断的嵌套、抢占等),当然最终会遍历该中断描述符的IRQ action list,调用外设的specific handler来处理该中断;
d -- 具体CPU architecture相关的模块会进行现场恢复;
总结下来,整个过程可以分为三部分:1、硬件处理部分;2、汇编处理部分;3、C 处理部分;
下面我们来追踪一下代码,了解当中断发生时,Linux 是如何处理的,前面的一些中断初始化部分就不再这里详述了,下面开始具体分析:
一、硬件处理部分
当一切准备好之后,一旦打开处理器的全局中断就可以处理来自外设的各种中断事件了。
当外设(SOC内部或者外部都可以)检测到了中断事件,就会通过interrupt requestion line上的电平或者边沿(上升沿或者下降沿或者both)通知到该外设连接到的那个中断控制器,而中断控制器就会在多个处理器中选择一个,并把该中断通过IRQ(或者FIQ,本文不讨论FIQ的情况)分发给该processor。
ARM处理器感知到了中断事件后,会进行下面一系列的动作(硬件处理部分):
1、切换处理器模式
修改 CPSR 寄存器中的 M[4:0],切换处理器模式位 IRQ Mode(这里M[4:0] 所添值为 10010);
2、保护现场
保存发生中断时,CPSR值与PC值(为恢复现场做准备);这里要注意,此时中断可能发生在 usr mode (用户空间),也可能发生在 SVC mode(内核空间);
3、mask IRQ exception
关闭IRQ中断,也就是设定CPSR.I = 1;
4、设定PC值为IRQ exception vector
实现向异常向量表的跳转,ARM处理器会跳转到IRQ的exception vector地址,到这硬件所做的工作就结束了,下面就是软件行为了。
软件处理部分流程如下:
可以看到 Vetor_irq 是汇编部分入口点,而Asm_do_irq 是C 部分入口点,下面分析Vetor_irq 向 Asm_do_irq 跳转过程
二、汇编部分
前面硬件部分结束后,跳转到相应的异常中断处理程序处执行,对于ARMv7向量表普遍是0xFFFF0018 ,而对于低向量PC=0x00000018
假设在用户空间时,产生了外部硬件中断,这个时候的会跳转到异常向量表,向量表(vector table)的代码如下
【arch/arm/kernel/entry-armv.S】
__vectors_start:---------------〉在中断向量表被拷贝后,该地址就是0xffff0000.
ARM( swi SYS_ERROR0 )
THUMB( svc #0 )
THUMB( nop )
W(b) vector_und + stubs_offset
W(ldr) pc, .LCvswi + stubs_offset
W(b) vector_pabt + stubs_offset
W(b) vector_dabt + stubs_offset
W(b) vector_addrexcptn + stubs_offset
W(b)vector_irq + stubs_offset----------〉当外部中断产生时,pc直接指向这个地址。
W(b) vector_fiq + stubs_offset
.globl __vectors_end
1、IRQ mode中的处理 (vector table --- > vector_irq )
IRQ mode的处理都在vector_irq中,vector_stub是一个宏,定义如下:
/*
* Vector stubs.
*
* This code is copied to 0xffff0200 so we can use branches in the
* vectors, rather than ldr's. Note that this code must not
* exceed 0x300 bytes.
*
* Common stub entry macro:
* Enter in IRQ mode, spsr = SVC/USR CPSR, lr = SVC/USR PC
*
* SP points to a minimal amount of processor-private memory, the address
* of which is copied into r0 for the mode specific abort handler.
*/
.macro vector_stub, name, mode, correction=0
.align 5
vector_name:
.if correction
sub lr, lr, #correction //因为硬件处理器是将当前指令的下两条指令的地址存储在lr寄存器中,所以这里需要减4,让他指向被中断指令的下一条,这样当中断被恢复时,可以继续被中断的指令继续执行。
.endif //需要注意的是,这个时候的lr寄存器,已经是irq模式下的私有寄存器了,在中断产生时,硬件处理器已经自动为他赋了值。
@
@ Save r0, lr_ (parent PC) and spsr_
@ (parent CPSR)
@
stmia sp, {r0, lr} @ save r0, lr//保存r0和lr寄存器,即被中断的下一条指令
mrs lr, spsr
str lr, [sp, #8] @ save spsr
@
@ Prepare for SVC32 mode. IRQs remain disabled.//准备从中断模式切换到管理模式,不同的模式,对应各自不同的堆栈。
@
mrs r0, cpsr
eor r0, r0, #(mode ^ SVC_MODE | PSR_ISETSTATE)
msr spsr_cxsf, r0
@
@ the branch table must immediately follow this code
@
and lr, lr, #0x0f //获取被中断前,处理器所处的模式
THUMB( adr r0, 1f )
THUMB( ldr lr, [r0, lr, lsl #2] )
mov r0, sp //让r0寄存器指向中断模式下堆栈的基地址
ARM( ldr lr, [pc, lr, lsl #2] )
movs pc, lr @ branch to handler in SVC mode,同时将中断模式下的spsr_irq(irq私有的)赋值给cpsr(该寄存器所有模式共享)
ENDPROC(vector_name)
从这可以看出 vector_stub 的使用方法:
vector_stub, name, mode, correction=0
上面这段究竟做了些什么呢?
(1)我们期望在栈上保存发生中断时候的硬件现场(HW context),这里就包括ARM的core register。上一章我们已经了解到,当发生IRQ中断的时候,lr中保存了发生中断的PC+4,如果减去4的话,得到的就是发生中断那一点的PC值。
(2)当前是IRQ mode,SP_irq在初始化的时候已经设定(12个字节)。在irq mode的stack上,依次保存了发生中断那一点的r0值、PC值以及CPSR值(具体操作是通过spsr进行的,其实硬件已经帮我们保存了CPSR到SPSR中了)。为何要保存r0值?因为随后的代码要使用r0寄存器,因此我们要把r0放到栈上,只有这样才能完完全全恢复硬件现场。
(3)可怜的IRQ mode稍纵即逝,这段代码就是准备将ARM推送到SVC mode。如何准备?其实就是修改SPSR的值,SPSR不是CPSR,不会引起processor mode的切换(毕竟这一步只是准备而已)。
(4)很多异常处理的代码返回的时候都是使用了stack相关的操作,这里没有。“movs pc, lr ”指令除了字面上意思(把lr的值付给pc),还有一个
隐含的操作(movs中‘s’的含义):把SPSR copy到CPSR,从而实现了模式的切换。
这里有个问题:中断为什么必须进入svc模式?
一个最重要原因是:如果一个中断模式(例如从usr进入irq模式,在irq模式中)中重新允许了中断,并且在这个中断例程中使用了BL指令调用子程序,BL指令会自动将子程序返回地址保存到当前模式的sp(即r14_irq)中,这个地址随后会被在当前模式下产生的中断所破坏,因为产生中断时CPU会将当前模式的PC保存到r14_irq,这样就把刚刚保存的子程序返回地址冲掉。为了避免这种情况,中断例程应该切换到SVC或者系统模式,这样的话,BL指令可以使用r14_svc来保存子程序的返回地址。
2、vector table --- > vector_irq ---> vector _stub
对于IRQ Mode 则 vector_stub, irq, IRQ_MODE, 4
__stubs_start:
/*
* Interrupt dispatcher
*/
vector_stub irq, IRQ_MODE, 4 //减去4,确保返回发生中断之后的那条指令
.long __irq_usr@ 0 (USR_26 / USR_32) //从用户态进入中断的处理函数 base address + 0
.long __irq_invalid@ 1 (FIQ_26 / FIQ_32)
.long __irq_invalid@ 2 (IRQ_26 / IRQ_32)
.long __irq_svc@ 3 (SVC_26 / SVC_32) //从SVC进入中断的处理函数 base address + 12
.long __irq_invalid@ 4
.long __irq_invalid@ 5
.long __irq_invalid@ 6
.long __irq_invalid@ 7
.long __irq_invalid@ 8
.long __irq_invalid@ 9
.long __irq_invalid@ a
.long __irq_invalid@ b
.long __irq_invalid@ c
.long __irq_invalid@ d
.long __irq_invalid@ e
.long __irq_invalid@ f
这里根据被中断时,处理器模式的不同,分别跳转到__irq_usr和__irq_svc两个分支。
3、vector table --- > vector_irq ---> vector _stub ---> __irq_usr
在这里我们以__irq_usr为例来说明:
__irq_usr:
usr_entry //进行中断前的硬件上下文的保存
kuser_cmpxchg_check
irq_handler
get_thread_info tsk//获取被中断的用户进程或内核线程所对应的内核栈所对应的thread info结构。
mov why, #0
b ret_to_user_from_irq//恢复被中断时的上下文,然后继续被中断的进程或线程的执行
UNWIND(.fnend )
ENDPROC(__irq_usr)
4、vector table --- > vector_irq ---> vector _stub ---> __irq_usr ---> usr_entry
usr_entry展开如下:
.macro usr_entry
UNWIND(.fnstart )
UNWIND(.cantunwind ) @ don't unwind the user space
sub sp, sp, #S_FRAME_SIZE // #S_FRAME_SIZE的值为72
ARM( stmib sp, {r1 - r12} ) //尽管当前是处于管理模式,但由于svc和usr的r0-r12是公共的,所以相当于保存用户模式的r1-r12寄存器
THUMB( stmia sp, {r0 - r12} )
ldmia r0, {r3 - r5} //将之前保存在中断模式堆栈中的r0_usr,lr,spsr分别存储到r3-r5中
add r0, sp, #S_PC @ here for interlock avoidance #S_PC=60
mov r6, #-1 @ "" "" "" ""
str r3, [sp] @ save the "real" r0 copied
@ from the exception stack
@
@ We are now ready to fill in the remaining blanks on the stack:
@
@ r4 - lr_, already fixed up for correct return/restart
@ r5 - spsr_
@ r6 - orig_r0 (see pt_regs definition in ptrace.h)
@
@ Also, separately save sp_usr and lr_usr
@
stmia r0, {r4 - r6}
ARM( stmdb r0, {sp, lr}^ )//保存用户模式下的sp_usr,lr_usr
THUMB( store_user_sp_lr r0, r1, S_SP - S_PC )
@
@ Enable the alignment trap while in kernel mode
@
alignment_trap r0
@
@ Clear FP to mark the first stack frame
@
zero_fp
ifdef CONFIG_IRQSOFF_TRACER
bl trace_hardirqs_off
endif
.endm
代码执行到这里的时候,ARM处理已经切换到了【SVC mode】。一旦进入SVC mode,ARM处理器看到的寄存器已经发生变化,这里的sp已经变成了sp_svc了。因此,后续的压栈操作都是压入了发生中断那一刻的进程的(或者内核线程)内核栈(svc mode栈)。
此时的管理模式的内核栈分布如下:
需要说明的是:上图中的lr_irq即为用户模式下被中断指令的下一条指令,spsr_irq即为用户模式下被中断时的cpsr寄存器。
5、vector table --- > vector_irq ---> vector _stub ---> __irq_usr ---> (usr_entry ---> irq_handler )
usr_entry 结束后,会执行 irq_handler
irq_handler的实现过程 archarmkernelentry-armv.S
.macro irq_handler
get_irqnr_preamble r5, lr
@在include/asm/arch-s3c2410/entry-macro.s中定义了宏get_irqnr_preamble为空操作,什么都不做
1: get_irqnr_and_base r0, r6, r5, lr @判断中断号,通过R0返回,3.5节有实现过程
movne r1, sp
@
@ routine called with r0 = irq number, r1 = struct pt_regs *
@
adrne lr, 1b
bne asm_do_IRQ @进入中断处理。
……
.endm
可以看到 bne asm_do_IRQ @进入中断处理 泪奔~~o(>_
可以看到整个跳转过程:
vector table --- > vector_irq --->vector_stub, irq, IRQ_MODE, 4 ---> __irq_usr ---> usr_entry ---> irq_handler --->asm_do_IRQ
三、C语言处理部分
1、 asm_do_IRQ
asm_do_IRQ实现过程,arch/arm/kernel/irq.c
* asm_do_IRQ is the interface to be used from assembly code.
*/
asmlinkage void __exception_irq_entry
asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
{
handle_IRQ(irq, regs);
}
其中关键一步handle_IRQ(irq, regs)
2、handle_IRQ(irq, regs)
/*
* handle_IRQ handles all hardware IRQ's. Decoded IRQs should
* not come via this function. Instead, they should provide their
* own 'handler'. Used by platform code implementing C-based 1st
* level decoding.
*/
void handle_IRQ(unsigned int irq, struct pt_regs *regs)
{
struct pt_regs *old_regs = set_irq_regs(regs);
irq_enter();
/*
* Some hardware gives randomly wrong interrupts. Rather
* than crashing, do something sensible.
*/
if (unlikely(irq >= nr_irqs)) {
if (printk_ratelimit())
printk(KERN_WARNING "Bad IRQ%un", irq);
ack_bad_irq(irq);
} else {
generic_handle_irq(irq);
}
irq_exit();
set_irq_regs(old_regs);
}
主要调用generic_handle_irq(irq)
3、generic_handle_irq(irq)
include/linux/irq.h
static inline void generic_handle_irq_desc(unsigned int irq, struct irq_desc *desc)
{
#ifdef CONFIG_GENERIC_HARDIRQS_NO__DO_IRQ
desc->handle_irq(irq, desc);
#else
if (likely(desc->handle_irq))
desc->handle_irq(irq, desc);
else
__do_IRQ(irq);
#endif
}
static inline void generic_handle_irq(unsigned int irq)
{
generic_handle_irq_desc(irq, irq_to_desc(irq));
}
generic_handle_irq调用前面定义的generic_handle_irq_desc
4、generic_handle_irq_des
static inline void generic_handle_irq_desc(unsigned int irq, struct irq_desc *desc)
{
desc->handle_irq(irq, desc);
}
而 generic_handle_irq_desc也没做什么,调用desc——>handle_irq,这个函数就是irq_desc中的成员
5、irq_desc结构体
Linux内核将所有中断统一编号,使用irq_desc结构来描述中断:每个数组项对应一个中断(也可能是一组中断,它们使用共同的中断号),里面记录了中断的名称,中断状态,中断标记,并提供硬件访问函数(清除,屏蔽,使能中断),提供了这个中断的处理函数的入口,通过它可以调用用户注册的中断处理函数
include/linux/irq.h
{
.........
irq_flow_handler_t handle_irq; //当前的中断处理函数入口
struct irq_chip *chip; //底层的硬件访问
..........
struct irqaction *action; //用户提供的中断处理函数链表
unsigned int status; //IRQ状态
...........
const char *name; //中断名称
} ____cacheline_internodealigned_in_smp;
Handle_irq是这个或者这组中断的处理函数入口
这里调用desc->handle_irq分为俩种情况,一是单独的中断号的,一是共享中断号的,俩者的区别在于后者需要先判断是共享中断的中的哪一个然后再真正的去调用handle_irq,所以我这里分析一下单独中断号的处理流程,共享中断也是一样可以分析。
我们分析一个具体的,以外部中断为例
for (irqno = IRQ_EINT0; irqno <= IRQ_EINT3; irqno++) {
irqdbf("registering irq %d (ext int)n", irqno);
set_irq_chip(irqno, &s3c_irq_eint0t4);
set_irq_handler(irqno, handle_edge_irq);
set_irq_flags(irqno, IRQF_VALID);
}
上面代码我们看到,set_irq_handler的值是handler_edge_irq ,这里是处理边沿触发的中断函数,当然还有电平触发方式的中断(handler_level_irq),继续看代码