一、arm中断的相关硬件知识:
正常的程序执行流程发生暂时的停止时,称之为异常。例如处理一个外部的中断请求。在处理异常之前,当前处理器的状态必须保留,这样当异常处理完成之后,当前程序可以继续执行。处理器允许多个异常同时发生,它们将会按固定的优先级进行处理。ARM体系结构中的异常,与8位/16位体系结构的中断有很大的相似之处,但异常与中断的概念并不完全等同。
ARM的异常有七种
复位异常、SWI异常、未定义指令异常、数据中止和指令中止异常。外部中断分为FIQ和IRQ两种,分别为快速中断和通用中断。都可以通过CPSR中的相应位来屏蔽。
CPU知道一个source触发了中断,怎么调用执行一些函数(汇编,或者c语言),就是靠异常向量表(事实上,exception vector table 也是由汇编组成的)
Arm的7种异常对应的模式
地 址 |
异 常 |
进入模式 5个异常模式 |
优先级
6最低 |
0x0000,0000 |
复位 |
管理模式 |
1 |
0x0000,0004 |
未定义指令 |
未定义模式 |
6 |
0x0000,0008 |
软件中断 |
管理模式 |
6 |
0x0000,000C |
中止(预取指令) |
中止模式 |
5 |
0x0000,0010 |
中止(数据) |
中止模式 |
2 |
0x0000,0014 |
保留 |
保留 |
未使用 |
0x0000,0018 |
IRQ |
IRQ |
4 |
0x0000,001C |
FIQ |
FIQ |
3 |
arm异常表,对应模式及向量表偏移 (摘自arm体系结构与编程一书)
当一个异常/中断出现后, 4020系统中ARM处理器对其的响应过程如下:
(1) 保存处理器当前状态、中断屏蔽位以及各条件标志位。将当前程序状态寄存器CPSR的内容保存到将要执行的异常中断对应的SPSR寄存器中。
(2) 设置当前程序状态寄存器CPSR中相应的位。包括设置CPSR中的位,使处理器进入相应的执行模式;设置CPSR中的位,禁止IRQ中断,当进入FIQ模式时,禁止FIQ中断。
(3) 将寄存器lr_mode设置成返回地址。
(4) 将程序计数器值(PC),设置成该异常中断的中断向量地址,从而跳转到相应的异常中断处理程序执行。即处理器跳转到异常向量表中相应的入口(对于IRQ , 显然pc=0x18) 。
所以当触发IRQ后,CPU会最后跳入0x18 这个入口,定制kernel时只需在这个入口填入自己的指令(当然是汇编语句) ,即可调用中断处理函数,可能这样:
而对于4020 linux系统中断向量的含义是:
触发IRQ—CPU jump 到0x18,同时要把irqno传入相应的寄存器调用一个中断通用处理函数如:asm_do_IRQ(unsigned int irqno) asm_do_IRQ() 这个函数根据irqno 就可以找到对应的中断描述符,然后调用中断描述符里面的handler()了。
二、linux中中断向量表的初始化
ARM linux内核启动时,通过start_kernel()->trap_init()的调用关系,初始化内核的中断异常向量表。
linux/init/main.c Start_kernel中的中断向量表初始化 asmlinkage void __init start_kernel(void) { ..... trap_init(); init_IRQ(); .... }
|
中断的初始化主要和这两个函数相关
(1)trap_init函数
这个trap_init函数主要起到搬运中断向量到高地址的作用
void __init trap_init(void) { 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;
/* * 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 *)0xffff0000, __vectors_start, __vectors_end - __vectors_start); memcpy((void *)0xffff0200, __stubs_start, __stubs_end - __stubs_start); memcpy((void *)0xffff1000 - kuser_sz, __kuser_helper_start, kuser_sz);
/* * Copy signal return handlers into the vector page, and * set sigreturn to be a pointer to these. */ memcpy((void *)KERN_SIGRETURN_CODE, sigreturn_codes, sizeof(sigreturn_codes));
flush_icache_range(0xffff0000, 0xffff0000 + PAGE_SIZE); modify_domain(DOMAIN_USER, DOMAIN_CLIENT); }
|
(2)init_IRQ函数
void __init init_IRQ(void) { struct irqdesc *desc; int irq;
#ifdef CONFIG_SMP bad_irq_desc.affinity = CPU_MASK_ALL; bad_irq_desc.cpu = smp_processor_id(); #endif // irq_desc数组是用来描述IRQ的请求队列,每一个中断号分配一个 //irq_desc结构,组成了一个数组。 for (irq = 0, desc = irq_desc; irq < NR_IRQS; irq++, desc++) { *desc = bad_irq_desc; /* 把每个中断先初始化为bad_irq*/ INIT_LIST_HEAD(&desc->pend); }
init_arch_irq(); /* 调用4020自己sep4020_init_irq函数来初始化各个中断句柄为do_level_IRQ*/ }
|
其中关键函数为:init_arch_irq();
在/arch/arm/setup.c函数中有定义:init_arch_irq = mdesc->init_irq;
而mdesc->init_irq就是在arch/arm/mach-sep4020/irq.c中定义的
void __init sep4020_init_irq(void) { unsigned int i; unsigned long flags; local_save_flags(flags); *(RP)(INTC_IER_V) = 0XFFFFFFFF;使能所有的4020中断 *(RP)(INTC_IMR_V) = 0XFFFFFFFF;同时屏蔽所有的中断 *(RP)(INTC_IPLR_V) = 0X0; local_irq_restore(flags); for(i = 0; i < NR_IRQS; i++) { set_irq_handler(i, do_level_IRQ); /*设置4020各个中断的初始处理句柄*/ set_irq_chip(i, &sep4020_chip);/*设置中断屏蔽和应答的处理句柄*/ set_irq_flags(i, IRQF_VALID | IRQF_PROBE);/*设置初始化标志*/ } } static struct irqchip sep4020_chip= { .ack = sep4020int_ack, .mask = sep4020int_mask, .unmask = sep4020int_unmask, };
|
其实系统中是有个全局变量来保持中断结构指针数组,它是定义在arch/arm/kernel/irq.c
struct irqdesc irq_desc[NR_IRQS]; /*正真的中断处理函数指针数组*/
而表示中断类型的结构体是:
include/linux/irq.h
/** * struct irq_desc - interrupt descriptor * * @handle_irq: highlevel irq-events handler [if NULL, __do_IRQ()] * @chip: low level interrupt hardware access * @msi_desc: MSI descriptor * @handler_data: per-IRQ data for the irq_chip methods * @chip_data: platform-specific per-chip private data for the chip * methods, to allow shared chip implementations * @action: the irq action chain * @status: status information * @depth: disable-depth, for nested irq_disable() calls * @wake_depth: enable depth, for multiple set_irq_wake() callers * @irq_count: stats field to detect stalled irqs * @irqs_unhandled: stats field for spurious unhandled interrupts * @last_unhandled: aging timer for unhandled count * @lock: locking for SMP * @affinity: IRQ affinity on SMP * @cpu: cpu index useful for balancing * @pending_mask: pending rebalanced interrupts * @dir: /proc/irq/ procfs entry * @affinity_entry: /proc/irq/smp_affinity procfs entry on SMP * @name: flow handler name for /proc/interrupts output */ struct irq_desc { irq_flow_handler_t handle_irq; struct irq_chip *chip; struct msi_desc *msi_desc; void *handler_data; void *chip_data; struct irqaction *action; /* IRQ action list */ unsigned int status; /* IRQ status */ unsigned int depth; /* nested irq disables */ unsigned int wake_depth; /* nested wake enables */ unsigned int irq_count; /* For detecting broken IRQs */ unsigned int irqs_unhandled; unsigned long last_unhandled; /* Aging timer for unhandled count */ spinlock_t lock; #if defined(CONFIG_GENERIC_PENDING_IRQ) || defined(CONFIG_IRQBALANCE) cpumask_t pending_mask; #endif #ifdef CONFIG_PROC_FS struct proc_dir_entry *dir; #endif const char *name; } ____cacheline_internodealigned_in_smp;
|
三、Linux异常处理流程:
1、 硬件发生中断后,怎样找到中断入口函数
我们看下中断向量表它是定义在arch/arm/kernel/entry-armv.S中:
.equ stubs_offset, __vectors_start + 0x200 - __stubs_start @stubs_offset表示中断处理函数与中断向量表的偏移 .globl __vectors_start __vectors_start: swi SYS_ERROR0 b vector_und + stubs_offset ldr pc, .LCvswi + stubs_offset b vector_pabt + stubs_offset b vector_dabt + stubs_offset b vector_addrexcptn + stubs_offset b vector_irq + stubs_offset b vector_fiq + stubs_offset
.globl __vectors_end __vectors_end:
|
从entry-armv.S中的中断向量表定义可以看到,发生中断时系统会跳到 vector_irq + stubs_offset处运行,这个位置实际上就是中断入口函数。
vector_irq已经是中断的入口函数了,为什么又要加上 stubs_offset?是因为b指令实际上是相对当前PC的跳转,也就是说当汇编器看到B指令后会把要跳转的标签转化为相对于当前PC的偏移量写入指令码。从上面的代码可以看到中断向量表和stubs都发生了代码搬移,所以如果中断向量表中仍然写成b vector_irq,那么实际执行的时候就无法跳转到搬移后的vector_irq处,因为指令码里写的是原来的偏移量,所以需要把指令码中的偏移量写成搬移后的。
我们把搬移前的中断向量表中的irq入口地址记irq_PC,它在中断向量表的偏移量就是irq_PC-vectors_start, vector_irq在stubs中的偏移量是vector_irq-stubs_start,这两个偏移量在搬移前后是不变的。搬移后 vectors_start在0xffff0000处,而stubs_start在0xffff0200处,所以搬移后的vector_irq相对于中断向量中的中断入口地址的偏移量就是:200+vector_irq在stubs中的偏移量再减去中断入口在向量表中的偏移量,即200+ vector_irq-stubs_start-irq_PC +vectors_start = (vector_irq-irq_PC) + vectors_start+200-stubs_start,对于括号内的值实际上就是中断向量表中写的vector_irq,减去irq_PC是由汇编器完成的,而后面的 vectors_start+200-stubs_start就应该是stubs_offset,实际上在entry-armv.S中也是这样定义的 。
2、根据原来发生中断时CPU所处的工作模式找到相应的入口函数
arch/arm/kernel/entry-armv.S—vector_irq是通过宏来vector_stub定义的
.macro vector_stub, name, mode, correction=0 @这个宏展开就是各个异常模式的处理函数 .align 5
vector_\name: .if \correction sub lr, lr, #\correction .endif
@ @ Save r0, lr_<exception> (parent PC) and spsr_<exception> @ (parent CPSR)保存前一个状态的lr,spsr,r0因为马要将返回模式改为SVC,防止中断嵌套 @ stmia sp, {r0, lr} @ save r0, lr mrs lr, spsr str lr, [sp, #8] @ save spsr
@ @ Prepare for SVC32 mode. IRQs remain disabled. @ 马上就要进入SVC模式下,准备切换模式,但此时的IRQ还是被禁止的 mrs r0, cpsr eor r0, r0, #(\mode ^ SVC_MODE) msr spsr_cxsf, r0
@ @ the branch table must immediately follow this code @ 分支表必须紧靠下面的代码后面,这是由于表的地址是根据pc计算出来的,这个分支表是为了分别处理各种模式下进入异常的情况 and lr, lr, #0x0f /*lr作为一个分支表的索引值*/ mov r0, sp /*r0保存中断前状态的堆栈指针*/ ldr lr, [pc, lr, lsl #2] movs pc, lr @ 转去执行lr处的分支指令,同时由于带有“S”所以模式会切换到SVC模式下执行指令 .endm
.globl __stubs_start __stubs_start: /* * Interrupt dispatcher */ vector_stub irq, IRQ_MODE, 4
.long __irq_usr @ 0 (USR_26 / USR_32)用户进程被中断 .long __irq_invalid @ 1 (FIQ_26 / FIQ_32) .long __irq_invalid @ 2 (IRQ_26 / IRQ_32) .long __irq_svc @ 3 (SVC_26 / SVC_32)内核进程被中断 .long __irq_invalid @ 4 .long __irq_invalid @ 5 .long __irq_invalid @ 6 .long __irq_invalid @ 7 ......
|
从上面这段代码可以看出,vector_irq把发生中断时的r0,PC-4以及CPSR压栈(注意,压入的的irq模式的堆栈),把中断栈的栈指针赋给 r0,最后根据原来发生中断时CPU所处的工作模式(CPSR的低4位)找到相应的入口函数,在进入svc模式后进一步处理中断。
2、 用户进程被中断的处理情况:
(1)__irq_usr完成的工作
.align 5 __irq_usr: usr_entry /*在当前SVC模式的堆栈上分配一个pt_regs(72个字节)结构,保存被中断模式的寄存器现场*/
get_thread_info tsk #ifdef CONFIG_PREEMPT @在我们4020中没有开进程抢占 ldr r8, [tsk, #TI_PREEMPT] @ get preempt count add r7, r8, #1 @ increment it str r7, [tsk, #TI_PREEMPT] #endif
irq_handler /*查询sep4020的中断寄存器获得中断号,和获得pt_regs,然后调用asm_do_IRQ*/
mov why, #0 b ret_to_user
.ltorg
|
它主要通过调用宏usr_entry进一步保存现场,然后调用irq_handler进行中断处理,保存的用户模式pt_regs的栈现场结构如下:
-1 |
CPSR |
PC-4 |
LR |
SP |
R12 |
... |
R2 |
R1 |
R0 |
其中的LR,SP是USR模式下的,R0,CPSR,PC-4是从中断栈中拷贝的
(2)usr_entry宏调用
.macro usr_entry sub sp, sp, #S_FRAME_SIZE /*sizeof(pt_regs),分配一个pt_regs结构*/ stmib sp, {r1 - r12} /*保存r1-r12*/
ldmia r0, {r1 - r3} /*r0保存的是被中断模式的堆栈指针*/ add r0, sp, #S_PC @ S_SP为sp寄存器在pt_regs中的偏移 mov r4, #-1 @ "" "" "" ""
str r1, [sp] @ r0为中断前的堆栈指针,将成为pt_regs中的sp的值
#if __LINUX_ARM_ARCH__ < 6 && !defined(CONFIG_NEEDS_SYSCALL_FOR_CMPXCHG) #ifndef CONFIG_MMU #warning "NPTL on non MMU needs fixing" #else @ make sure our user space atomic helper is aborted cmp r2, #TASK_SIZE bichs r3, r3, #PSR_Z_BIT #endif #endif
@ @ We are now ready to fill in the remaining blanks on the stack: @ @ r2 - lr_<exception>, already fixed up for correct return/restart @ r3 - spsr_<exception> @ r4 - orig_r0 (see pt_regs definition in ptrace.h) @ @ Also, separately save sp_usr and lr_usr @ stmia r0, {r2 - r4} 分别将lr,spsr,orig_r0压入堆栈 stmdb r0, {sp, lr}^
@ @ Enable the alignment trap while in kernel mode @ alignment_trap r0
@ @ Clear FP to mark the first stack frame @ zero_fp .endm
|
(2)irq_handler的作用
它首先通过宏 get_irqnr_and_base获得中断号,存入r0,然后把上面建立的pt_regs结构的指针,也就是sp值赋给r1,把调用宏 get_irqnr_and_base的位置作为返回地址(为了处理下一个中断),然后调用 asm_do_IRQ进一步处理中断,以上这些操作都在建立在获得中断号的前提下,也就是有中断发生,
即当某个外部硬件触发中断的时候,kernel最终会调用到:asm_do_IRQ
linux/arch/arm/kernel/entry-armv.S /* * Interrupt handling. 中断处理函数入口 Preserves r7, r8, r9 */ .macro irq_handler 1: get_irqnr_and_base r0, r6, r5, lr /*获得4020的中断号*/ movne r1, sp //将sp的值赋给r1,即pt_regs结构的指针 @ @ routine called with r0 = irq number, r1 = struct pt_regs * @ adrne lr, 1b /*返回到1处,asm_do_IRQ返回后将再次查询发生的中断,这样可以处理优先级比较低的中断*/ bne asm_do_IRQ /*调用c语言的中断处理函数asm_do_IRQ*/
#ifdef CONFIG_SMP /* * XXX * * this macro assumes that irqstat (r6) and base (r5) are * preserved from get_irqnr_and_base above */ test_for_ipi r0, r6, r5, lr movne r0, sp adrne lr, 1b bne do_IPI
#ifdef CONFIG_LOCAL_TIMERS test_for_ltirq r0, r6, r5, lr movne r0, sp adrne lr, 1b bne do_local_timer #endif #endif
.endm
|
(3)get_irqnr_and_base获得4020的中断号的宏
下面这个宏查询ISPR(IRQ待定中断服务寄存器,当有需要处理的中断时,这个寄存器的相应位会置位,任意时刻,最多一个位会置位),计算出的中断号放在irqnr指定的寄存器中;这个宏在不同的ARM芯片上是不一样的,下面是sep4020的定义
.macro get_irqnr_and_base, irqnr, irqstat, base, tmp /*获得4020的中断号 */ //gfd ldr \base, =INTC_IFSR_V /*宏参数引用时要用"\"符号 */ ldr \base, [\base] mov \irqnr,#0 1001: cmp \base, #0 @ no flags set? beq 1002f @ keep "eq" status when tst \base, #1 @ lsb set?测试最低位是否为1,这样就可以知道是哪个中断 addeq \irqnr, \irqnr, #1 @ if not, incr IRQ# moveq \base, \base, LSR #1 @ r = r >> 1 beq 1001b 1002: .endm .macro irq_prio_table .endm
|
(4) asm_do_IRQ的c语言中断处理函数:
Arch/arm/kernel/irq.c asmlinkage void asm_do_IRQ(unsigned int irq, struct pt_regs *regs) { struct irqdesc *desc = irq_desc + irq; //从中断数组中获得相应中断类型结构指针
/* * Some hardware gives randomly wrong interrupts. Rather * than crashing, do something sensible. */ if (irq >= NR_IRQS) desc = &bad_irq_desc;
irq_enter(); /*禁止被其他任务抢占*/ spin_lock(&irq_controller_lock); desc_handle_irq(irq, desc, regs);/*调用我们设备驱动中自己注册的中断处理函数*/
/* * Now re-run any pending interrupts. */ if (!list_empty(&irq_pending)) do_pending_irqs(regs);
irq_finish(irq); //中断结束处理函数,在这里我们是空的
spin_unlock(&irq_controller_lock); irq_exit(); }
|
(5)irq_enter()函数
Include/linux/hardirq.h #define irq_enter() \ do { \ account_system_vtime(current); \ add_preempt_count(HARDIRQ_OFFSET); \ } while (0)
|
(6) desc_handle_irq函数
这个函数正真开始调用我们在各个驱动中注册的中断处理函数
Include/asm/mach/irq.h /* * Helpful inline function for calling irq descriptor handlers. */ static inline void desc_handle_irq(unsigned int irq, struct irqdesc *desc, struct pt_regs *regs) { desc->handle(irq, desc, regs);/*调用我们设备驱动中自己注册的中断处理函数*/ }
|
(7)irq_exit函数
linux/kernel/softirq.c /* * Exit an interrupt context. Process softirqs if needed and possible: */ void irq_exit(void) { account_system_vtime(current); sub_preempt_count(IRQ_EXIT_OFFSET); if (!in_interrupt() && local_softirq_pending()) invoke_softirq(); preempt_enable_no_resched(); }
|
3、 内核进程被中断的处理情况(待续) | | |