三、arm32中断处理软件子系统
中断软件子系统负责cpu检测到中断以后的处理,总体来看,可以分为三个部分:中断向量函数、中断控制器驱动部分以及用户接口部分;
中断向量函数放在中断向量表里面,每一种中断对应一个中断向量函数,软件在初始化时需要创建一个中断向量表,放在内存中并通过协处理器cp15告诉cpu中断向量表的位置;cpu检测到中断后,会自动将处理器模式切换到对应的中断模式,然后将pc指向对应的中断向量函数。中断向量函数中会将处理器模式切换到svc模式,将被中断前处理器的硬件上下文保存起来,然后跳转到中断控制器驱动注册的处理函数,至此,中断传递到中断控制器驱动;
中断控制器驱动负责管理所有类型的中断,包括外设中断、SMP核间中断以及cpu热插拔等中断;向下提供中断控制器操作接口,向上提供用户处理中断的各种接口;
最后是上层用户接口,上层通过中断控制器提供的接口注册回调函数处理中断事件;
3.1 中断向量表
由于arm处理器处理中断的方式:cpu执行完当前指令,检测到中断后,会自动将工作模式切换到对应的中断模式,然后将pc指向中断向量表中对应中断的中断向量。
因此,软件需要创建一个中断向量表,并通过设置协处理器cp15告诉cpu中断向量表的位置。
中断向量表定义在entry-armv.S文件中:
.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
__vectors_start的地址定义在vmlinux.lds.S文件中,这个是默认地址,内核初始化过程中会重新设置
__vectors_start = .;
.vectors 0xffff0000 : AT(__vectors_start) {
*(.vectors)
}
. = __vectors_start + SIZEOF(.vectors);
__vectors_end = .;
__stubs_start = .;
.stubs ADDR(.vectors) + 0x1000 : AT(__stubs_start) {
*(.stubs)
}
. = __stubs_start + SIZEOF(.stubs);
__stubs_end = .;
devicemaps_init函数中会申请内存,并将中断向量表拷贝到内存,然后,根据协处理器cp15的寄存器C1的bit[13]是否被置1,将中断向量表的物理地址映射到0xffff0000或者0x0,详细函数调用流程如下图所示:

3.2 中断向量函数的实现
中断向量表中的跳转函数都通过.macro vector_stub, name, mode, correction=0宏进行定义,该宏具体如下:
/*
* Vector stubs.
*
* This code is copied to 0xffff1000 so we can use branches in the
* vectors, rather than ldr's. Note that this code must not exceed
* a page size.
*
* 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
.endif
@
@ Save r0, lr_<exception> (parent PC) and spsr_<exception>
@ (parent CPSR)
@
stmia sp, {r0, lr} @ save 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
ARM( ldr lr, [pc, lr, lsl #2] )
movs pc, lr @ branch to handler in SVC mode
ENDPROC(vector_/name)
以IRQ中断向量vector_irq为例,通过vector_stub irq, IRQ_MODE, 4宏定义,如下:
/*
* 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
于是,IRQ中断产生后就跳转到vector_stub irq, IRQ_MODE, 4。
以程序工作在用户模式时被中断为例,在vector_irq函数中,会将处理器的运行模式切换到SVC模式,然后,进入__irq_svc,保存处理器被中断前的硬件上下文,最后,跳转到中断控制器驱动初始化时绑定的中断入口函数handle_arch_irq(进入这个函数时,cpu本地中断处于屏蔽状态),具体内容如下表:
/* 将r0、lr寄存器(lr就是被中断的下一条指令)以及spsr(用户模式的cpsr)保存到irq模式的栈 */
stmia sp, {r0, lr}
mrs lr, spsr --- /* 此时lr保存的是spsr,也就是用户模式的cpsr,也就是被中断前处理器所处的模式 */
str lr, [sp, #8]
/* 将spsr寄存器修改为svc模式,为切换到管理模式做准备 */
mrs r0, cpsr --- /* 此时中断模式的CPSR,中断还是屏蔽状态 */
eor r0, r0, #(/mode ^ SVC_MODE | PSR_ISETSTATE)
msr spsr_cxsf, r0
/* 获取被中断前处理器所处的模式 */
and lr, lr, #0x0f
THUMB(adr r0, 1f)
THUMB(ldr lr, [r0, lr, lsl #2])
/* 让r0寄存器指向中断模式下堆栈的基地址 */
mov r0, sp
/* 将lr设置为__irq_usr(此时的lr只是作为临时寄存器使用),然后跳转到__irq_usr */
ARM(ldr lr, [pc, lr, lsl #2])
movs pc, lr --- /* movs指令会同时将spsr_irq赋值给cpsr,从而实现向svc模式切换 */
__irq_usr:
/* usr_entry进行irq_handler前,硬件上下文的保存*/
sub sp, sp, #S_FRAME_SIZE --- /* 分配svc模式的栈 */
/* r0-r12是所有模式公共的,保存到栈 */
ARM(stmib sp, {r1 - r12})
THUMB(stmia sp, {r0 - r12})
/* 将之前保存在中断模式堆栈中的r0_usr,lr,spsr分别存储到r3-r5中 --- 当前r0是中断模式堆栈的基地址 */
ldmia r0, {r3 - r5}
add r0, sp, #S_PC
mov r6, #-1
/* 保存用户模式下的sp_usr,lr_usr */
stmia r0, {r4 - r6}
ARM(stmdb r0, {sp, lr}^)
THUMB(store_user_sp_lr r0, r1, S_SP - S_PC)
/* 进入irq_handler */
irq_handler
/*
* Interrupt handling.
*/
.macro irq_handler
#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
9997:
.endm
/* 恢复用户模式 */
b ret_to_user_from_irq
3.3 中断控制器驱动
3.3.1 中断控制器驱动初始化

如上图所示,中断控制器驱动主要围绕struct gic_chip_data结构体进行;该结构体中包含两个主要的结构体struct irq_chip和struct irq_domain,其中,struct irq_chip用来表示中断控制器芯片,一个系统中可能使用多片中断控制器,一片中断控制器用一个struct irq_chip结构体表示,这个结构体里面填充了中断控制器的操作函数,比如irq_enable和irq_disable;struct irq_domain用于硬件中断号和虚拟中断号的管理。
中断控制器驱动,主要完成如下3部分功能:
1. 将上层的中断处理函数绑定给中断向量里面的回调函数,比如为IRQ中断向量中的回调函数handle_arch_irq绑定为gic_handle_irq,还有SMP核间交互的回调函数gic_raise_softirq以及cpu热插拔的回调函数gic_starting_cpu;
2. 提供中断控制器芯片的操作接口给上层模块调用;
3. 中断控制器中每个中断以实际的硬件中断号标识,linux内核对每个中断以虚拟中断号标识,因此中断控制器驱动还要管理硬件中断号和虚拟中断号之间的映射。
中断控制器的初始化流程如下图所示:

中断控制器驱动初始化的入口函数是gic_of_init,这个函数放在IRQCHIP_DECLARE宏里面,内核对每一种中断控制器都声明一个IRQCHIP_DECLARE宏,如下,宏里面包含的compatible字段和中断控制器初始化入口函数,of_irq_init(__irqchip_of_table)函数中,将设备树中中断控制器节点的compatible字段和这些宏的compatible比较,找到该中断控制器对应的IRQCHIP_DECLARE宏,然后调用里面的回调函数gic_of_init进行中断控制器驱动的初始化;itop4412中断控制器的设备树节点如下,compatible字段是”arm,cortex-a9-gic”,于是和IRQCHIP_DECLARE(cortex_a9_gic, “arm,cortex-a9-gic”, gic_of_init)匹配。
IRQCHIP_DECLARE(gic_400, "arm,gic-400", gic_of_init); IRQCHIP_DECLARE(arm11mp_gic, "arm,arm11mp-gic", gic_of_init); IRQCHIP_DECLARE(arm1176jzf_dc_gic, "arm,arm1176jzf-devchip-gic", gic_of_init); IRQCHIP_DECLARE(cortex_a15_gic, "arm,cortex-a15-gic", gic_of_init); IRQCHIP_DECLARE(cortex_a9_gic, "arm,cortex-a9-gic", gic_of_init); IRQCHIP_DECLARE(cortex_a7_gic, "arm,cortex-a7-gic", gic_of_init); IRQCHIP_DECLARE(msm_8660_qgic, "qcom,msm-8660-qgic", gic_of_init); IRQCHIP_DECLARE(msm_qgic2, "qcom,msm-qgic2", gic_of_init); IRQCHIP_DECLARE(pl390, "arm,pl390", gic_of_init);
itop4412中断控制器设备树节点:
gic: interrupt-controller@10490000 {
compatible = "arm,cortex-a9-gic";
#interrupt-cells = <3>;
interrupt-controller;
reg = <0x10490000 0x10000>, <0x10480000 0x10000>;
};
3.3.2 中断管理
每个硬件中断在中断控制器中有个固定的硬件中断号,在linux内核中对应一个虚拟中断号,多个外设可以共享一条硬件中断线,那么一个虚拟中断号就可以挂接多个中断处理函数以对应多个外设。
每个硬件中断用中断描述符struct irq_desc描述,这个结构体里面主要包含如下几个内容:
1. struct irq_domain结构体,负责硬件中断号和虚拟中断号的映射管理;
2. struct irq_chip结构体,中断控制器芯片的操作函数;
3. irq_flow_handler_t handle_irq函数,中断产生后,中断向量的回调函数中最终会调用到这个函数;
4. struct irqaction结构体,用户注册中断时,注册接口会创建一个irqaction结构体,将用户注册的中断处理函数绑定到这个结构体,然后放到中断描述符irq_desc的struct irqaction链表里面,当中断产生时,处理函数irq_flow_handler就会遍历这个链表,逐个处理用户注册的中断处理。
用户通过request_irq/request_threaded_irq函数注册中断处理函数,在这个函数里面会根据虚拟中断号获取中断描述符,分配action结构体,并填充,包括中断回调函数、线程回调函数等,进行线程化处理逻辑,如果是非共享中断,进行开中断以及cpu亲和性等处理,然后将action结构体放到中断描述符的链表尾部,最后还会在proc文件系统中添加相关信息,具体过程如下:
/*
* 获取中断描述符
* 分配action结构体,并填充,包括中断回调函数、线程回调函数等
* 创建内核线程
* 如果是非共享中断,进行开中断以及cpu亲和性等处理
* 将action结构体放到中断描述符的链表尾部
* 在proc文件系统中添加相关信息
*/
request_irq
request_threaded_irq
/*
* 虚拟中断号获取中断描述符
*/
desc = irq_to_desc(irq);
/*
* 分配结构体struct irqaction,并填充
*/
action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
action->handler = handler;
action->thread_fn = thread_fn;
action->flags = irqflags;
action->name = devname;
action->dev_id = dev_id;
/*
* __setup_irq(unsigned int irq, struct irq_desc *desc, struct irqaction *new)
* 中断注册主要处理函数
* irq -> 虚拟中断号
* desc -> 中断描述符
* action -> 前面kzalloc分配的结构体
*/
__setup_irq(irq, desc, action)
new->irq = irq;
nested = irq_settings_is_nested_thread(desc);
if (nested)
/* 如果中断绑定在其他中断线程中,需要特别处理 */
new->handler = irq_nested_primary_handler;
else
/* 判断是否能线程化,进行强制线程化处理 */
if (irq_settings_can_thread(desc))
/*
* 强制线程化处理
* 填充前面分配的action结构体
* 创建secondary action
*/
irq_setup_forced_threading(new)
new->flags |= IRQF_ONESHOT;
new->secondary = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
set_bit(IRQTF_FORCED_THREAD, &new->thread_flags);
/*
* 1、将线程回调函数设置成中断处理函数new->handler
* 2、将中断处理函数设置成默认的处理函数irq_default_primary_handler
* ??? 为什么要这样设置???
*/
new->thread_fn = new->handler;
new->handler = irq_default_primary_handler;
/*
* 创建一个内核线程
*/
if (new->thread_fn && !nested)
setup_irq_thread(new, irq, false)
t = kthread_create(irq_thread, new, "irq/%d-%s", irq, new->name);
/* 设置线程的调度策略 */
sched_setscheduler_nocheck(t, SCHED_FIFO, ¶m);
/*
* 中断描述符的action链表为空,也就是第一次挂接action,需要分配资源
*/
if (!desc->action)
irq_request_resources(desc)
struct irq_data *d = &desc->irq_data;
struct irq_chip *c = d->chip;
/* 调用gic驱动的回调函数申请资源 */
c->irq_request_resources ? c->irq_request_resources(d) : 0;
/*
* 共享中断
* 在已经挂接action的情况下,将当前action挂接到链表中
*/
old_ptr = &desc->action;
old = *old_ptr;
if (old)
do {
/*
* Or all existing action->thread_mask bits,
* so we can find the next zero bit for this
* new action.
*/
thread_mask |= old->thread_mask;
old_ptr = &old->next;
old = *old_ptr;
} while (old); --- /* 循环结束后,old指向链表尾部,指针内容为NULL */
shared = 1;
/*
* 非共享中断要特殊处理
*/
if (!shared)
init_waitqueue_head(&desc->wait_for_threads);
__irq_set_trigger(desc, new->flags & IRQF_TRIGGER_MASK);
irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS);
if (new->flags & IRQF_PERCPU)
irqd_set(&desc->irq_data, IRQD_PER_CPU);
irq_settings_set_per_cpu(desc);
/* Exclude IRQ from balancing if requested */
if (new->flags & IRQF_NOBALANCING)
irq_settings_set_no_balancing(desc);
irqd_set(&desc->irq_data, IRQD_NO_BALANCING);
if (irq_settings_can_autoenable(desc))
/*
* 开启中断,并进行亲和性设置
*/
irq_startup(desc, IRQ_RESEND, IRQ_START_COND)
if (irqd_is_started(d)) {
irq_enable(desc);
} else {
switch (__irq_startup_managed(desc, aff, force)) {
case IRQ_STARTUP_NORMAL:
ret = __irq_startup(desc);
irq_setup_affinity(desc);
break;
case IRQ_STARTUP_MANAGED:
irq_do_set_affinity(d, aff, false);
ret = __irq_startup(desc);
break;
case IRQ_STARTUP_ABORT:
return 0;
}
}
/* 将action结构体放到中断描述符链表的尾部 */
*old_ptr = new;
/*
* 将内核线程设置为可执行状态
*/
wake_up_process(new->thread);
/* 在proc文件系统中添加相关信息 */
register_irq_proc(irq, desc);
irq_add_debugfs_entry(irq, desc);
new->dir = NULL;
register_handler_proc(irq, new);
3.4 完整的中断处理流程
处理器检测到中断后,会将工作模式切换到对应的中断模式,然后将pc指向中断向量,从而跳转到中断向量执行;在中断向量中,软件会将工作模式切换到SVC模式,保存硬件上下文,然后跳转到控制器驱动初始化时注册的回调函数;在这个回调函数里面会通过硬件中断号找到对应的虚拟中断号,从而找到该中断对应的中断描述符irq_desc;最后依次调用中断描述符irq_desc中用户注册的中断处理函数。
以处理器工作在用户模式被IRQ中断为例,具体流程如下:
1. 处理器切换到IRQ中断模式,跳转到中断向量vector_irq
W(b) vector_irq
Vector_irq展开后为宏定义vector_stub irq, IRQ_MODE, 4
2. 在vector_irq中,将处理器模式切换到SVC模式(需要指出的时,此时的IRQ中断通过CPSR寄存器被屏蔽掉了),跳转到__irq_usr
将lr设置为__irq_usr(此时的lr只是作为临时寄存器使用),然后跳转到__irq_usr
ARM(ldr lr, [pc, lr, lsl #2])
movs pc, lr --- movs指令会同时将spsr_irq赋值给cpsr,从而实现向svc模式切换
3. 在__irq_usr中,调用irq_handler,然后,在irq_handler里面跳转到handle_arch_irq,handle_arch_irq这个函数指针在中断控制器驱动初始化的时候被赋值为gic_handle_irq
set_handle_irq(gic_handle_irq)
void __init set_handle_irq(void (*handle_irq)(struct pt_regs *))
handle_arch_irq = handle_irq;
4. gic_handle_irq这个函数里面会进行循环处理完所有待处理的中断,
从中断控制器的寄存器读取硬件中断号,
irqstat = readl_relaxed(cpu_base + GIC_CPU_INTACK);
irqnr = irqstat & GICC_IAR_INT_ID_MASK;
当硬件中断号小于16,表示核间IPI中断,调用handle_IPI
if (irqnr < 16) {
writel_relaxed(irqstat, cpu_base + GIC_CPU_EOI);
if (static_key_true(&supports_deactivate))
writel_relaxed(irqstat, cpu_base + GIC_CPU_DEACTIVATE);
#ifdef CONFIG_SMP
/*
* Ensure any shared data written by the CPU sending
* the IPI is read after we've read the ACK register
* on the GIC.
*
* Pairs with the write barrier in gic_raise_softirq
*/
smp_rmb();
handle_IPI(irqnr, regs);
#endif
continue;
}
当硬件中断号大于15小于1020,表示共享和CPU私有中断,调用handle_domain_irq
if (likely(irqnr > 15 && irqnr < 1020)) {
if (static_key_true(&supports_deactivate))
writel_relaxed(irqstat, cpu_base + GIC_CPU_EOI);
isb();
handle_domain_irq(gic->domain, irqnr, regs);
continue;
}
当读取的硬件中断号无效,则退出while循环
5. IRQ中断,进入handle_domain_irq函数
6. handle_domain_irq函数中,首先会调用irq_enter函数进入中断上下文
irq_enter();
7. irq_enter函数,将处理器preempt_count变量的HARDIRQ部分+1表示进入硬件中断上下文;系统会根据preempt_count变量来判断是否可以调度及抢占,只有preempt_count值为0时,才可以调度和抢占;那么handle_domain_irq函数在退出前,系统一直处于不可抢占状态,那么当前中断就一直使用被中断进程的上下文,比如内核栈、current以及preempt_count等都一直是被中断进程的上下文
#define __irq_enter() /
do { /
account_irq_enter_time(current); /
preempt_count_add(HARDIRQ_OFFSET); /
trace_hardirq_enter(); /
} while (0)
8. 取出虚拟中断号
irq = irq_find_mapping(domain, hwirq)
9. 进一步调用上层的处理函数generic_handle_irq
10. 通过generic_handle_irq_desc调用中断描述符的handle_irq,在建立硬件中断号和虚拟中断号的映射关系时,gic_irq_domain_map函数中给handle_irq绑定了处理函数,
硬件中断号小于32时,handle_irq被绑定为handle_percpu_devid_irq
if (hw < 32) {
irq_set_percpu_devid(irq);
irq_domain_set_info(d, irq, hw, &gic->chip, d->host_data,
handle_percpu_devid_irq, NULL, NULL);
irq_set_status_flags(irq, IRQ_NOAUTOEN);
}
硬件中断号大于等于32时,handle_irq被绑定为handle_fasteoi_irq
irq_domain_set_info(d, irq, hw, &gic->chip, d->host_data,
handle_fasteoi_irq, NULL, NULL);
11. 进入handle_fasteoi_irq函数,使用raw_spin_lock对中断描述符访问加自旋锁
raw_spin_lock(&desc->lock);
修改中断状态
desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);
12. 如果用户没有注册中断处理函数(即action链表为空),或者该中断处于屏蔽状态,那么将中断状态修改为IRQS_PENDING,调用mask_irq在中断控制器级屏蔽该中断线,然后,退出handle_fasteoi_irq函数
/*
* If its disabled or no action available
* then mask it and get out of here:
*/
if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {
desc->istate |= IRQS_PENDING;
mask_irq(desc);
goto out;
}
13. 如果是IRQS_ONESHOT类型中断,那么屏蔽该中断
if (desc->istate & IRQS_ONESHOT)
mask_irq(desc);
14. 进一步调用handle_irq_event
handle_irq_event(desc);
15. 进入handle_irq_event函数
将中断状态修改为IRQD_IRQ_INPROGRESS
desc->istate &= ~IRQS_PENDING;
irqd_set(&desc->irq_data, IRQD_IRQ_INPROGRESS);
进一步调用handle_irq_event_percpu
16. 进入handle_irq_event_percpu函数
扫描action链表,依次调用用户注册的中断处理函数
for_each_action_of_desc(desc, action)
irqreturn_t res;
/*
* arm32处理器,此时读出的cpsr寄存器bit[7]被置1,说明cpu本地IRQ中断被禁止了,那就是不会发生IRQ硬件中断嵌套的情况了?
*/
res = action->handler(irq, action->dev_id);
根据返回值res判断,是否进行了线程化,如果进行了线程化,则唤醒对应的内核线程
switch (res)
case IRQ_WAKE_THREAD:
__irq_wake_thread(desc, action);
非线程化,则修改flag,然后返回
case IRQ_HANDLED:
17. generic_handle_irq函数返回有,__handle_domain_irq会调用irq_exit函数退出中断上下文
irq_exit();
18. 进入irq_exit函数
如果本地中断没有被屏蔽,则会产生告警
#ifndef __ARCH_IRQ_EXIT_IRQS_DISABLED
local_irq_disable();
#else
WARN_ON_ONCE(!irqs_disabled());
#endif
统计硬件中断退出的次数
account_irq_exit_time(current);
退出硬件中断上下文
preempt_count_sub(HARDIRQ_OFFSET);
如果不在硬件上下文,并且有软中断需要处理,那么开始执行软中断
如果没有设置软中断强制线程化,那么直接调用软中断的回调函数执行,此时中断使用的栈还没有完全释放干净,因此使用的还是硬件堆栈
__do_softirq();
如果设置了软中断强制线程化,那么调度软中断内核线程运行,每个cpu都绑定了一个软中断内核线程
wakeup_softirqd();
本文大部分内容参考如下博客,梳理总结只为自己更好地理解,如有侵权,请联系删除
参考资料:
https://www.cnblogs.com/LoyenWang/p/12996812.html
原创文章,作者:745907710,如若转载,请注明出处:https://blog.ytso.com/tech/aiops/270438.html