三、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