参考:https://www.cnblogs.com/douzi2/p/5112743.html
当异常中断发生时,系统执行完当前指令后,将跳转到相应的异常中断处理程序处执行。在异常中断处理程序执行完成后,程序返回到发生中断的指令的下一条指令处执行。
Kernel版本:4.14.111
ARM处理器,Contex-A7
在《ARM体系结构与编程》第9章中说到,ARM 中有个概念叫做“异常中断”,也就是包括外部中断在内
的各种异常。显然,ARM体系的“异常中断”概念更加接近MIPS体系中的“异常”概念。
中断向量
中断服务程序的入口地址。在某些计算机中,中断向量的位置存放一条跳转到中断服务程序入口地址的跳转指令。
来存放中断向量(共256个),称这一片内存区为中断向量表,地址范围是0~3FFH
中断向量地址:
存储中断向量的存储单元地址
中断向量:
中断向量的集合,按中断类型号从小到大的顺序存放到存储器的某一区域内,这个存放中断向量的存储区叫做中断向量表,即中断服务程序入口地址表。
CPU是根据中断号获取中断向量值,即对应中断服务程序的入口地址值。因此为了让CPU由中断号查找到对应的中断向量,就需要在内存中建立一张查询表
说明
Kernel版本:4.14.111
ARM处理器,Contex-A7
在《ARM体系结构与编程》第9章中说到,ARM 中有个概念叫做“异常中断”,也就是包括外部中断在内
的各种异常。显然,ARM体系的“异常中断”概念更加接近MIPS体系中的“异常”概念。
ARM异常中断向量表
ARM的异常中断向量表可以是高端向量表,也可以是低端向量表,两者取其一。区别是基地址不同。高端向量是ARM架构可选配置,可以通过硬件外部输入管脚来配置是低端向量还是高端向量,不能通过指令来改变向量的位置,但如果ARM芯片内部有标准ARM协处理器,那么协处理器CP15的寄存器C1的bit13可以用来切换低端和高端向量地址,等于0时为低端向量,等于1时为高端向量。
Linux内核分用户空间、内核空间,通常32位处理器,用户空间0-3G,内核空间3-4G,所以Linux内核使用高端向量表。
ARM的一个非常重要的寄存器——CPSR程序状态寄存器
cpsr这个寄存器用来设定进入哪种模式
向量中断 非向量中断
向量者,矢量也,即指方向,门路。
向量中断------由硬件提供中断服务程序入口地址;
非向量中断------由软件件提供中断服务程序入口地址;
推荐向量中断就是不同的中断有不同的入口地址,非向量中断就只有一个入口地址,进去了再判断中断标志来识别具体是哪个中断。向量中断实时性好,非向量中断简单
对于ARM,ARM没有NMI,外部中断走的是非向量中断,即走注册的中断服务例程ISP,这个是驱动工程师自己注册的request_irq的irq_handle
但是无论是非向量中断还是向量中断,都会走到__vectors_start,然后根据中断类型走到对于的分类,例如IRQ就是走到了__vectors_start的vendor_irq,然后最终走到了irq_desc(中断例程描述符表),里面就定义了irqaction,irqaction链表指向了irq_handler_t中断服务程序,即reques_irq注册的中断服务程序
而对于其他类型的异常,如RESET,硬件已经做好了现成的处理code,根据向量表直接到达代码的地址,然后执行。区别于非向量的IRQ是统一的地址,进入这个地址后,再由中断例程描述符表区分中断
在vmlinux.lds.S描述了__vectors_start的起始位置,从0xffff0000开始
/*
* The vectors and stubs are relocatable code, and the
* only thing that matters is their relative offsets
*/
__vectors_start = .;
.vectors 0xffff0000 : AT(__vectors_start) { (1)
*(.vectors)
}
. = __vectors_start + SIZEOF(.vectors);
__vectors_end = .;
__stubs_start = .;
.stubs ADDR(.vectors) + 0x1000 : AT(__stubs_start) {
*(.stubs) (2)
}
. = __stubs_start + SIZEOF(.stubs);
__stubs_end = .;
1)链接时,将.vectors段内容链接到虚拟地址0xffff0000地址。(这里我理解为在vmlinux镜像中.vectors段连续,夹在__vectors_start和__vectors_end 中间,但是链接的虚拟地址指向0xffff0000)
2)同上。
arch/arm/kernel/entry-armv.S 中.vectors段保存了异常向量表。
.section .vectors, "ax", %progbits
.L__vectors_start:
W(b) vector_rst
W(b) vector_und
W(ldr) pc, .L__vectors_start + 0x1000
W(b) vector_pabt
W(b) vector_dabt
W(b) vector_addrexcptn
W(b) vector_irq
W(b) vector_fiq
(软件中的向量表和硬件中的offset定义的一致性)
上面是中断向量表,而具体的向量是在vector_stub中,例如,vector_irq
/*
* 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
.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
参考:http://blog.chinaunix.net/uid-29045944-id-3968667.html
根据异常向量表,有异常的时候就可以跳转了;但是跳到哪里呢?
上面说到,当有IRQ中断(我们在7种异常中断类型举IRQ为例)时,CPU根据中断向量表进入vector_irq,vector_irq的描述在vector_stub中,假如IRQ中断时在用户usr模式,则进入
__irq_usr,__irq_usr
__irq_usr:
usr_entry @保存中断上下文
kuser_cmpxchg_check
irq_handler @调用中断处理程序
get_thread_info tsk @获取当前进程的进程描述符中的成员变量thread_info的地址,并将该地址保存到寄存器tsk(r9)(在entry-header.S中定义)
mov why, #0
b ret_to_user_from_irq @中断处理完成,恢复中断上下文并返回中断产生的位置
UNWIND(.fnend )
ENDPROC(__irq_usr)
如果发生中断前处于核心态则进入__irq_svc,其定义如下
__irq_svc:
svc_entry @保存中断上下文
irq_handler @调用中断处理程序
#ifdef CONFIG_PREEMPT
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 @如果不等于0,说明发生内核抢占,需要重新调度。
#endif
svc_exit r5, irq = 1 @恢复中断上下文
UNWIND(.fnend )
ENDPROC(__irq_svc)
上面代码中的usr_entry和svc_entry是一个宏定义,主要用于保护中断上下文到栈中
保存中断上下文后则进入中断处理程序——irq_handler,定义在arch/arm/kernel/entry_armv.S文件中:
#ifdef CONFIG_MULTI_IRQ_HANDLER
ldr r1, =handle_arch_irq
mov r0, sp
badr lr, 9997f
ldr pc, [r1]
#else
arch_irq_handler_default
#endif
这里32位走arch_irq_handler_default,64位走handle_arch_irq
(1 )arch_irq_handler_default:
arch_irq_handler_default ——>asm_do_IRQ()—>handle_IRQ()—>__handle_domain_irq()—>generic_handle_irq()—>调用用户request_irq注册的中断处理函数
(2)handle_arch_irq :
handle_arch_irq---->set_handle_irq---->gic_handle_irq---->handle_domain_irq()---->__handle_domain_irq()---->generic_handle_irq()---->generic_handle_irq_desc()
2种最终都调用到了desc->handle_irq(desc)
(kernel/include/linux/irqdesc.h)
static inline void generic_handle_irq_desc(struct irq_desc *desc)
{
desc->handle_irq(desc); // 这里真正调用到,用户request_irq注册的中断处理函数
}
generic_handle_irq_desc调用中断描述符的handle_irq回调函数。对于不同的中断类型,handle_irq回调函数可能是handle_simple_irq、handle_level_irq、handle_fasteoi_irq、handle_edge_irq、handle_edge_eoi_irq、handle_percpu_irq。
假如我们用reques_irq注册的是gpio电平触发中断,那么这里的desc->handle_irq就是handle_level_irq。(省略转换流程)
最终,handle_level_irq----> handle_irq_event(desc)-----> handle_irq_event_percpu------> res = action->handler(irq, action->dev_id); /* 调用action->handler,即request_irq 时注册的handler 函数 */
这里我们关注struct irq_desc,称为中断描述符,该数据结构保存了关于所有IRQ的中断描述符信息
struct irq_desc {
irq_flow_handler_t handle_irq; //指向中断函数, 中断产生后,就会执行这个handle_irq
struct irq_chip *chip; //指向irq_chip结构体,用于底层的硬件访问,下面会介绍
struct msi_desc *msi_desc;
void *handler_data;
void *chip_data;
struct irqaction *action; /* IRQ action list */ //action链表,用于中断处理函数
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;
spinlock_t lock;
... ...
const char *name; //产生中断的硬件名字
} ;
其中的成员*chip的结构体,用于底层的硬件访问, irq_chip类型如下:
struct irq_chip {
const char *name;
unsigned int (*startup)(unsigned int irq); //启动中断
void (*shutdown)(unsigned int irq); //关闭中断
void (*enable)(unsigned int irq); //使能中断
void (*disable)(unsigned int irq); //禁止中断
void (*ack)(unsigned int irq); //响应中断,就是清除当前中断使得可以再接收下个中断
void (*mask)(unsigned int irq); //屏蔽中断源
void (*mask_ack)(unsigned int irq); //屏蔽和响应中断
void (*unmask)(unsigned int irq); //开启中断源
... ...
int (*set_type)(unsigned int irq, unsigned int flow_type); //将对应的引脚设置为中断类型的引脚
... ...
#ifdef CONFIG_IRQ_RELEASE_METHOD
void (*release)(unsigned int irq, void *dev_id); //释放中断服务函数
#endif
};
其中的成员struct irqaction *action,主要是用来存用户注册的中断处理函数,
一个中断可以有多个处理函数 ,当一个中断有多个处理函数,说明这个是共享中断.
所谓共享中断就是一个中断的来源有很多,这些来源共享同一个引脚。
所以在irq_desc结构体中的action成员是个链表,以action为表头,若是一个以上的链表就是共享中断
irqaction结构定义如下:
struct irqaction {
irq_handler_t handler; //等于用户注册的中断处理函数,中断发生时就会运行这个中断处理函数
unsigned long flags; //中断标志,注册时设置,比如上升沿中断,下降沿中断等
cpumask_t mask; //中断掩码
const char *name; //中断名称,产生中断的硬件的名字
void *dev_id; //设备id
struct irqaction *next; //指向下一个成员
int irq; //中断号,
struct proc_dir_entry *dir; //指向IRQn相关的/proc/irq/
};
由硬件电路决定,一般固定设置异常中断向量表在0地址开始,即复位异常触发的地址