1. 内核中断介绍
1.1 中断简介
所有支持Linux的平台都采用了中断(interrupt)的概念,以便(因种种原因)引入周期性的中断。需要区分两种类型的中断。
1. 硬件中断(hardware interrupt):由系统自身和与之连接的外设自动产生。它们用于支持更高效地实现设备驱动程序,也用于引起处理器自身对异常或错误的关注,这些是需要与内核代码进行交互的。
2. 软中断(SoftIRQ):用于有效实现内核中的延期操作。
在Linux中用于处理中断和系统调用相关部分的代码中,汇编和C代码交织在一起, 以解决C语言无法独立处理的一些特殊问题。在中断处理中涉及到许多C代码和底层的硬件交互代码。
在C6678处理器上,硬件中断的最大数目通常是15,这个值可不怎么大,还有考虑到有些中断编号已经永久性地分配给了标准的系统组件(键盘、定时器等),因而限制了可用于其他外部设备的中断编号数目。
外设可共享同一个中断号这个现象称为中断共享(interrupt sharing)。但必须硬件和内核同时支持才能使用共享中断,因为必须要识别出中断来源于哪个设备。
1.2 中断处理
在CPU得知发生中断后,它将进一步的处理委托给一个软件例程,该例程可能会修复故障、提供专门的处理或将外部事件通知用户进程。
由于每个中断和异常都有唯一的编号,内核使用一个数组,数组项是指向处理程序函数的指针。相关的中断号根据数组项在数组中的位置判断。
▲图1.1 中断处理过程
因为需要C语言代码和汇编语言代码之间的交互,所以必须特别小心,才能正确设计在汇编语言层次和C语言层次上的数据交换。对应的代码位于arch/arch/kernel/entry.S中,彻底利用了各个处理器的具体特性。
1.3 中断处理程序
中断处理程序可能会遇到困难,特别是在处理程序执行期间,发生了其他中断。尽管可以通过在处理程序执行期间禁用中断来防止,但这会引起其他问题,如遗漏重要的中断。屏蔽(Masking, 这个术语用于表示选择性地禁用一个或多个中断)因而只能短时间使用。
中断处理数据结构:IRQ相关信息管理的关键点是一个全局数组,每个数组项对应一个IRQ编号。因为数组位置和中断号是相同的,很容易定位与特定的IRQ相关的数组项:IRQ 0在位置0,IRQ 15在位置15,等等。IRQ最终映射到哪个处理器中断。irq_desc存储了中断相关信息。
action链表提供了一个操作链,需要在中断发生时执行。由中断通知的设备驱动程序,可以将与之相关的处理程序函数放置在此处。有一个专门的数据结构用于表示这些操作。
chip硬件处理和芯片相关操作被封装在chip中。为此引入了一个专门的数据结构用来处理硬件相关操作。
1.4 处理程序函数的表示
irqaction结构定义如下,每个处理程序函数都对应该结构的一个实例:
<code>
该结构中最重要的成员是处理程序函数本身,即handler成员,这是一个函数指针,位于结构的起始处。在设备请求一个系统中断,而中断控制器通过引发中断将该请求转发到处理器的时候,内核将调用该处理程序函数。
在考虑如何注册处理程序函数时,我们再仔细考察其参数的语义。但请注意,处理程序的类型为irq_handler_t,与电流处理程序的类型irq_flow_handler_t显然是不同的。
name和dev_id唯一地标识一个中断处理程序。name是一个短字符串,用于标识设备(例如, 「e100」、「ncr53c8xx」等等),而dev_id是一个指针,指向在所有内核数据结构中唯一标识了该设备的数据结构实例,例如网卡的net_device实例。如果几个设备共享一个IRQ,那么IRQ编号自身不能标识该设备,此时,在删除处理程序函数时,将需要上述信息。
▲图1.2 中断结构体关系描述图
2. 中断向量表以及中断子程序
2.1 中断子程序实现
在汇编源文件arch/c6x/kernel/vectors.S 中定义了中断子程序,使用宏IRQVEC可以实现不同的中断子程序:
arch/c6x/kernel/vectors.S第 30 行实现了中断处理宏定义IRQVEC
▲图2.1 中断处理向量宏定义
根据如上IRQVEC宏定义结合实际代码使用情况可以生成不同的中断子程序。
▲图2.2 生成中断向量
例如:
79行 IRQVEC INT4,_int4_handler可生成如下中断子程序。
<code>
当中断发生时,进入到汇编代码中的中断子程序,首先保存寄存器A0的值保存在栈中,然后拷贝相应的中断子程序入口地址到A0寄存器中,然后跳转到相应的中断子程序,中断子程序执行完毕后从栈中恢复寄存器A0的值。
在中断子程序_int4_handler中执行如下指令
<code>
SAVE_ALL_INT此行作用为保存所有的寄存器到栈中,然后调用MASK_SYSCALL关闭系统调用,使用CALL_INT 4先将中断号存入A4,然后调用kernel 中的 c6x_do_IRQ 中断处理函数。调用结束后将返回结果存入寄存器B3。
▲图2.3 CALL_INT宏定义
在arch/c6x/kernel/entry.S 中定义了诸多 _DEFINE_MACRO 汇编宏定义。
1. 220行 _DEFINE_MACRO(SAVE_ALL_INT) 中断保存所有寄存器。
2. 325行 _DEFINE_MACRO(MASK_SYSCALL) 屏蔽系统调用。
2.1 c6x_do_IRQ处理流程
c6x_do_IRQ函数的是实现在arch/c6x/kernel/irq.c文件中
64行 asmlinkage void c6x_do_IRQ(unsigned int prio, struct pt_regs *regs)
▲图2.4 c6x_do_IRQ中断函数
获取kernel中断号:
c6x_do_IRQ 获取中断号prio,将中断号通过hw_to_kernel_irq将硬件中断号转换为kernel中断号,转换后传入kernel的中断处理函数generic_handle_irq。
在代码中hw_to_kernel_irq如何将硬件中断号转换为kernel中断号?
在中断初始化的过程中构建prio_to_irq的中断对应数组,将硬件中断号和kernel中断号产生对应关系。当使用hw_to_kernel_irq的时候,hw_to_kernel_irq从中断对应数组 prio_to_irq[(hw)] 中取出kernel中断号。
常规内核处理函数generic_handle_irq 处理中断流程:
在include/linux/irq.h 头文件中实现了generic_handle_irq和
generic_handle_irq_desc两个内联函数。
generic_handle_irq 使用kernel中断号irq获取irq中断号相对应的irq_to_desc,既irq_desc结构体数组,然后调用generic_handle_irq_desc将中断号,和获取的irq_desc结构体数组传给generic_handle_irq_desc。
▲图2.5 generic_handle_irq_desc
在generic_handle_irq_desc函数中检测irq对应的irq_desc结构体中是否有中断处理函数handle_irq。
如果有handle_irq就执行此中断的中断处理函数,这里执行的是handle_level_irq函数,在irq_desc->handle_irq中我们无法直接看到其对应的中断处理函数,可以通过init_IRQ 初始化函数中获得此处填入的中断处理函数名称。
▲图2.6 init_IRQ 中断初始化函数
handle_level_irq函数中执行的流程如下:
1. lock中断。
2. 检测中断是否为处理中状态:IRQ_INPROGRESS。
3. 如果中断为正在处理的状态直接跳到结尾处unlock中断并推出函数。
4. 如果中断为触发状态,将中断设置为正在处理的状态,并且调用handle_IRQ_event执行相应中断处理函数。
如果没有handle_irq就说明此中断为共享中断(IRQ线)执行__do_IRQ(irq)。
__do_IRQ 处理所有普通设备的中断,每一个设备都有其特定的中断处理函数,存在放action列表中。
在kernel/irq/handler.c文件中实现了__do_IRQ函数。__do_IRQ函数主要遍历irq_desc 结构体中的action链表处理中断。
449行 unsigned int __do_IRQ(unsigned int irq)
接下来使用共享中断处理函数:handle_IRQ_event 来遍历irq_desc 里的 action共享中断链表。
handle_IRQ_event函数:
在kernel/irq/handle.c 368行irqreturn_t handle_IRQ_event( unsigned int irq,struct irqaction *action)Handle_IRQ_event 用来处理irq实际的action链表。