中断是指在CPU正常运行期间,由于内外部事件或由程序预先安排的事件引起的CPU暂时停止正在运行的程序,转而为该内部或外部事件或预先安排的事件服务的程序中去,服务完毕后再返回去继续运行被暂时中断的程序。Linux中通常分为外部中断(又叫硬件中断)和内部中断(又叫异常)。
在实地址模式中,CPU把内存中从0开始的1KB空间作为一个中断向量表。表中的每一项占4个字节。但是在保护模式中,有这4个字节的表项构成的中断向量表不满足实际需求,于是根据反映模式切换的信息和偏移量的足够使得中断向量表的表项由8个字节组成,而中断向量表也叫做了中断描述符表(IDT)。
下面先解释一下CPU对中断的处理机制,以ARM为例,一般学过处理器架构的人都知道,我们再来复习一下。
ARM有七种运行模式USR,SYS,SVC,IRQ,FIQ,UND,ABT
非特权模式 | 特权模式 | |||||
非异常模式 | 异常模式 | |||||
用户模式 USR | 系统模式 SYS | 中断模式 IRQ | 快中断模式 FIQ | 管理模式 SVC | 终止模式 ABT | 未定义模式 UND |
五种异常模式:SVC,IRQ,FIQ,UND,ABT
中断模式是ARM异常模式之一(IRQ模式,FIQ模式),是一种异步事件,如外部按键产生中断,内部定时器产生中断,通信数据口数据收发产生中断等。
1.当一个异常产生时,以FIQ为例,CPU切入FIQ模式时,
①将原来执行程序的下一条指令地址保存到LR中,就是将R14保存到R14_fiq里面。
②拷贝CPSR到SPSR_fiq。
③改变CPSR模式位的值,改到FIQ模式。
④改变PC值,将其指向相应的异常处理向量表。
2.离开异常处理的时候,
①将LR(R14_fiq)赋给PC。
②将SPSR(SPSR_fiq)拷贝到CPSR。
③清除中断禁止标志(如果开始时置位了)。
ARM一般在某个固定地址中有一个异常向量表,比如0x0 ,当一个外部IRQ中断产生时
①处理器切换到IRQ模式
②PC跳到0x18处运行,因为这是IRQ的中断入口。
③通过0x18:LDR PC, IRQ_ADDR,跳转到相应的中断服务程序。这个中断服务程序就要确定中断源,每个中断源会有自己独立的中断服务程序。
④得到中断源,然后执行相应中断服务程序
⑤清除中断标志,返回
然后来看看linux中队中断的处理和使用。
1.建立异常向量表:
之前我们分析了系统的启动,那到底这个中断系统到底是在什么时候开始和初始化的呢,中断第一个需要关注的函数式start_kernel()->setup_arch ()->early_trap_init() ,early_trap_init()函数在各个linux版本源码中所调用的位置有所不同,如linux3.6在start_kernel()->setup_arch ()->paging_init->devicemaps_init()->early_trap_init()。
void __init early_trap_init(void *vectors_base)
{
unsigned long vectors = (unsigned long)vectors_base;
extern char __stubs_start[], __stubs_end[];
extern char __vectors_start[], __vectors_end[];
extern char __kuser_helper_start[], __kuser_helper_end[];
int kuser_sz = __kuser_helper_end - __kuser_helper_start;
vectors_page = vectors_base;
/*
* Copy the vectors, stubs and kuser helpers (in entry-armv.S)
* into the vector page, mapped at 0xffff0000, and ensure these
* are visible to the instruction stream.
*/
memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start);
memcpy((void *)vectors + 0x200, __stubs_start, __stubs_end - __stubs_start);
memcpy((void *)vectors + 0x1000 - kuser_sz, __kuser_helper_start, kuser_sz);
/*
* Do processor specific fixups for the kuser helpers
*/
kuser_get_tls_init(vectors);
/*
* Copy signal return handlers into the vector page, and
* set sigreturn to be a pointer to these.
*/
memcpy((void *)(vectors + KERN_SIGRETURN_CODE - CONFIG_VECTORS_BASE),
sigreturn_codes, sizeof(sigreturn_codes));
flush_icache_range(vectors, vectors + PAGE_SIZE);
modify_domain(DOMAIN_USER, DOMAIN_CLIENT);
}
CONFIG_VECTORS_BASE在autoconf.h定义,在ARM V4及V4T以后的大部分处理器中,中断向量表的位置可以有两个位置:一个是0,另一个是0xffff0000。可以通过CP15协处理器c1寄存器中V位(bit[13])控制。V和中断向量表的对应关系如下:
V=0 ~ 0x00000000~0x0000001C
V=1 ~ 0xffff0000~0xffff001C
__vectors_end 至 __vectors_start之间为异常向量表,位于arch/arm/kernel/entry-armv.S
.globl __vectors_start
__vectors_start:
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
W(b) vector_fiq + stubs_offset
.globl __vectors_end
__vectors_end:
当中断到来时,CPU会执行“W(b) vector_irq+ stubs_offset“,经过一系列的跳转和处理会执行到下面一段代码
_irq_usr:
usr_entry
kuser_cmpxchg_check
irq_handler
get_thread_info tsk
mov why, #0
b ret_to_user_from_irq
UNWIND(.fnend )
ENDPROC(__irq_usr)
svc_entry是一个宏,主要实现了将usr模式下的寄存器、中断返回地址保存到堆栈中
__irq_svc:
svc_entry
irq_handler
#ifdef CONFIG_PREEMPT
get_thread_info tsk
ldr r8, [tsk, #TI_PREEMPT] @ get preempt count
ldr r0, [tsk, #TI_FLAGS] @ get flags
teq r8, #0 @ if preempt count != 0
movne r0, #0 @ force flags to 0
tst r0, #_TIF_NEED_RESCHED
blne svc_preempt
#endif
#ifdef CONFIG_TRACE_IRQFLAGS
@ The parent context IRQs must have been enabled to get here in
@ the first place, so there's no point checking the PSR I bit.
bl trace_hardirqs_on
#endif
svc_exit r5 @ return from exception
UNWIND(.fnend )
ENDPROC(__irq_svc)
svc_entry是一个宏,主要实现了将SVC模式下的寄存器、中断返回地址保存到堆栈中。然后进入最核心的中断响应函数irq_handler,irq_handler实现过程arch\arm\kernel\entry-armv.S
/*
* Interrupt handling.
*/
.macro irq_handler
#ifdef CONFIG_MULTI_IRQ_HANDLER
ldr r1, =handle_arch_irq
mov r0, sp
adr lr, BSYM(9997f)
ldr pc, [r1]
#else
arch_irq_handler_default
#endif
.endm
然后在arch/arm/include/asm/entry_macro_multi.S中:
/*
* Interrupt handling. Preserves r7, r8, r9
*/
.macro arch_irq_handler_default
get_irqnr_preamble r6, lr
get_irqnr_and_base r0, r2, r6, lr
movne r1, sp
@
@ routine called with r0 = irq number, r1 = struct pt_regs *
@
adrne lr, BSYM(1b)
bne asm_do_IRQ
get_irqnr_preamble和get_irqnr_and_base两个宏由machine级的代码定义,目的就是从中断控制器中获得IRQ编号,紧接着就调用asm_do_IRQ,从这个函数开始,中断程序进入C代码中,传入的参数是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);
}
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%u\n", irq);
ack_bad_irq(irq);
} else {
generic_handle_irq(irq);
}
irq_exit();
set_irq_regs(old_regs);
}
int generic_handle_irq(unsigned int irq)
{
struct irq_desc *desc = irq_to_desc(irq);
if (!desc)
return -EINVAL;
generic_handle_irq_desc(irq, desc);
return 0;
}
static inline void generic_handle_irq_desc(unsigned int irq, struct irq_desc *desc)
{
desc->handle_irq(irq, desc);//中断处理
}
中断处理过程代码就跟到这了,那么最后一个问题desc->handle_irq(irq, desc);系统根据中断号找到相应的irq_desc结构,调用里面的中断处理函数执行最后的中断处理。所以要想知道linux中断系统是怎么样工作的,还需要研究一下irq_desc这个结构体。