每个 IRQ 同时有 “irq” 和 “hwirq” 两个编号。这里 “hwirq” 是硬件中断号,或者说是物理中断号,也就是芯片手册上写的那个号码,而 “irq” 是软件使用的中断号,或者说是逻辑/虚拟中断号。
如果芯片中有多个中断控制器,假设它们各自都对应 16
个 IRQ,这 16
个 IRQ
的编号都是从 0
到 15
,那 CPU 收到中断后,软件单凭中断号是分不清这个中断到底是从哪个中断控制器派发过来的,它需要的是中断控制器自身的编号加上 IRQ
的编号。
为此,我们需要一种将中断控制器 local
的物理中断号转换成 Linux
全局的虚拟中断号的机制,而接下来要介绍的 struct irq_domain
就承担着这个转换的角色。
Linux
中描述中断控制器的数据结构是 struct irq_chip
,因为不同芯片的中断控制器对其挂接的 IRQ
有不同的控制方法,因而这个结构体主要是由一组用于回调(callback
),指向系统实际的中断控制器所使用的控制方法的函数指针构成。
struct irq_chip {
const char *name;
void (*irq_enable)(struct irq_data *data);
void (*irq_disable)(struct irq_data *data);
void (*irq_mask)(struct irq_data *data);
void (*irq_unmask)(struct irq_data *data);
void (*ipi_send_single)(struct irq_data *data, unsign);
void (*ipi_send_mask)(struct irq_data *data, const s);
...
};
/proc/interupts
"中看到的那个;既然说到对每个 IRQ 不同的控制,那就不得不说下对 ** IRQ 的描述**,IRQ 是由 struct irq_desc
表示的:
struct irq_desc {
const char *name;
unsigned int depth;
struct irqaction *action;
struct irq_data irq_data;
struct cpumask *percpu_enabled;
...
};
/proc/interupts
"中查看;0
表示可启用中断;action
” 是 IRQ 对应的中断处理函数(ISR - Interrupt Service Routine
);"irq_data
" 结构体irq_set_chip()
函数完成的。下文以 SPI 中断为例:
当中断产生后,中断信号送给 GIC, GIC 组件进行管理和仲裁,然后再将中断信息发送给相应的 cpu, cpu 立刻进入 FIQ
或 IRQ
异常程序(这里是可以通过寄存器来控制的)。
ARM64 的异常向量表 vectors
中设置了各种异常的入口(位于arm/arm64/kernel/entry.S
)。
目前有效的异常入口有:
el0_sync
, el1_sync
;el0_irq
, el1_irq
;el0_irq
和 el1_irq
的具体实现略有不同,但处理流程大致是相同的。ENTRY(vectors)
kernel_ventry 1, sync_invalid // Synchronous EL1t
kernel_ventry 1, irq_invalid // IRQ EL1t
kernel_ventry 1, fiq_invalid // FIQ EL1t
kernel_ventry 1, error_invalid // Error EL1t
kernel_ventry 1, sync // el1下的同步异常,例如指令执行异常、缺页中断等。
kernel_ventry 1, irq // el1下的异步异常,硬件中断。 1代表异常等级
kernel_ventry 1, fiq_invalid // FIQ EyL1h
kernel_ventry 1, error // Error EL1h
kernel_ventry 0, sync //el0下的同步异常,例如指令执行异常、缺页中断(跳转地址或者取地址)、系统调用等。
kernel_ventry 0, irq //el0下的异步异常,硬件中断。0代表异常等级
kernel_ventry 0, fiq_invalid // FIQ 64-bit EL0
kernel_ventry 0, error // Error 64-bit EL0
......
END(vectors)
先看下 kernel_ventry 1
的实现:
.macro kernel_ventry, el, label, regsize = 64
.align 7
sub sp, sp, #S_FRAME_SIZE ----------------------->(1)
#ifdef CONFIG_VMAP_STACK
这里省略掉检查栈溢出的代码
#endif
b el\()\el\()_\label ----------------------------->(2)
.endm
(1) 将 sp 预留一个 fram_size, 这个 size 就是 struct pt_regs
的大小, 结构 struct pt_regs
用以在堆栈中保存异常发生时的现场寄存器信息,其具体定义与 cpu 架构相关;内核发生异常时输出的 debug 信息就是通过 show_regs(regs)
来打印的(实际上并步严谨,有些上下文中可能无法获取到 pt_regs 时使用 dump_stack()
)。
预留后堆栈指针 sp 指向这个 pt_reg 的起始地址。
struct pt_regs 实现如下:
arch/arm64/include/asm/ptrace.h
/*
* This struct defines the way the registers are stored on the stack during an
* exception. Note that sizeof(struct pt_regs) has to be a multiple of 16 (for
* stack alignment). struct user_pt_regs must form a prefix of struct pt_regs.
*/
struct pt_regs {
union {
struct user_pt_regs user_regs;
struct {
u64 regs[31];
u64 sp;
u64 pc;
u64 pstate;
};
};
u64 orig_x0;
#ifdef __AARCH64EB__
u32 unused2;
s32 syscallno;
#else
s32 syscallno;
u32 unused2;
#endif
u64 orig_addr_limit;
u64 unused; // maintain 16 byte alignment
u64 stackframe[2];
};
(2) 跳转到对应级别的异常处理函数,如 kernel_entry 1, irq
展开后会跳转 el1_irq
。此外,在 kernel_entry
这个宏中还会将该次异常发生前的 x0~x29
寄存器、sp
、lr
、elr_el1
、spsr_el1
等等寄存器存放到堆栈的struct pt_regs
内存中。
如:当 cpu 运行在 el1 等级时,突然来了一个外设中断(non-secure)系统会进入上面中断向量表的:kernel_ventry 1, irq
,展开后如下:
linux/arch/arm64/kernel/entry.S
.align 6
el1_irq:
kernel_entry 1 ----------------------->(1)
msr daifclr, #4
enable_dbg
#ifdef CONFIG_TRACE_IRQFLAGS
bl trace_hardirqs_off
#endif
irq_handler -------------------------->(2)
#ifdef CONFIG_PREEMPT
ldr w24, [tsk, #TSK_TI_PREEMPT] // get preempt count
cbnz w24, 1f // preempt count != 0
ldr x0, [tsk, #TSK_TI_FLAGS] // get flags
tbz x0, #TIF_NEED_RESCHED, 1f // needs rescheduling?
bl el1_preempt
1:
#endif
#ifdef CONFIG_TRACE_IRQFLAGS
bl trace_hardirqs_on
#endif
kernel_exit 1 ------------------------->(3)
ENDPROC(el1_irq)
(1) 如上文描述 kernel_entry 1
中是保存现场的行为,这里我们看下它具体是如何保存现场的:
.macro kernel_entry, el, regsize = 64
stp x0, x1, [sp, #16 * 0] // 用 stp 指令将 x0-x29 保存到预留的栈中,
stp x2, x3, [sp, #16 * 1]
stp x4, x5, [sp, #16 * 2]
stp x6, x7, [sp, #16 * 3]
stp x8, x9, [sp, #16 * 4]
stp x10, x11, [sp, #16 * 5]
stp x12, x13, [sp, #16 * 6]
stp x14, x15, [sp, #16 * 7]
stp x16, x17, [sp, #16 * 8]
stp x18, x19, [sp, #16 * 9]
stp x20, x21, [sp, #16 * 10]
stp x22, x23, [sp, #16 * 11]
stp x24, x25, [sp, #16 * 12]
stp x26, x27, [sp, #16 * 13]
stp x28, x29, [sp, #16 * 14]
.if \el == 0 // 如果 el 为 0 表示从用户态产生的异常
clear_gp_regs // 清除 x0-x29 寄存器
mrs x21, sp_el0 // 将用户态的 sp 指针保存到 x21 寄存器
ldr_this_cpu tsk, __entry_task, x20 // 从当前 per_cpu 读取当前的 task_struct 地址 ------------> TODO
ldr x19, [tsk, #TSK_TI_FLAGS] // 获取 task->flag 标记
disable_step_tsk x19, x20 // exceptions when scheduling.
.else // 从内核状态产生的异常
add x21, sp, #S_FRAME_SIZE // X21 保存压入 pt_regs 数据之前的栈地址,也就是异常时,内核的栈地址;
/* 这里是从 sp_el0 从获取到当前 task_struct 结构,内核状态的时候,sp_el0 用于保存内核的 task_struct 结构,*/
/* 用户态的时候, 这个 sp_el0 是用户态的 sp */
get_thread_info tsk
/* 保存 task's original addr_limit 然后设置 USER_DS */
ldr x20, [tsk, #TSK_TI_ADDR_LIMIT]
str x20, [sp, #S_ORIG_ADDR_LIMIT]
mov x20, #USER_DS
str x20, [tsk, #TSK_TI_ADDR_LIMIT]
.endif /* \el == 0 */
// x22 保存异常地址
mrs x22, elr_el1
// x23 保存程序状态寄存器
mrs x23, spsr_el1
stp lr, x21, [sp, #S_LR] // 将 lr 和 sp 保存到 pt_regs->x[30], pt_rets->sp
// 如果发生在 el1 模式,则将 x29 和异常地址保存到 pt_regs->stackframe
.if \el == 0
stp xzr, xzr, [sp, #S_STACKFRAME]
.else
stp x29, x22, [sp, #S_STACKFRAME]
.endif
add x29, sp, #S_STACKFRAME
stp x22, x23, [sp, #S_PC] // 将异常和程序状态 保存到pt_regs->pstate 和 pt_regs->pc
// 如果是el0->el1发了变迁, 那么将当前的 task_struct 给 sp_el0 保存
.if \el == 0
msr sp_el0, tsk
.endif
/*
* Registers that may be useful after this macro is invoked:
*
* x21 - aborted SP
* x22 - aborted PC
* x23 - aborted PSTATE
*/
.endm
其实,当异常发生时(进入vectors前)还会有硬件上的处理。armv8手册讲到的进入异常前的处理:
PSTATE
到 SPSR_ELx
寄存器;PSTATE
中的 DAIF
全部屏蔽;ELR_ELx
寄存器。(2) 然后就会进入对应的 宏 irq_handler
/*
* Interrupt handling.
*/
.macro irq_handler
ldr_l x1, handle_arch_irq
mov x0, sp
irq_stack_entry
blr x1
irq_stack_exit
.endm
.text
上面主要的三个动作:
.macro irq_stack_entry
mov x19, sp // 保存当前 sp 到 x19
/* 判断当前栈是不是中断栈, 如果是任务栈,就从 per_cpu 中读取中断栈地址,并切换到中断栈 */
ldr x25, [tsk, TSK_STACK]
eor x25, x25, x19
and x25, x25, #~(THREAD_SIZE - 1)
cbnz x25, 9998f
ldr_this_cpu x25, irq_stack_ptr, x26 // 读取 per_cpu 的 irq_stack_ptr
mov x26, #IRQ_STACK_SIZE
add x26, x25, x26
/* 切换到中断栈 */
mov sp, x26
9998:
.endm
handle_arch_irq
;hand_arch_irq
是在gic 初始化的时候进行赋值的:static int __int gic_of_init(struce device_node *node)
{
...
set_handle_irq(gic_handle_irq);
...
}
(3) kernel_exit 基本上是 kernel_entry 的逆过程,最后使用 eret 退出异常。
GIC 作为一个具体的中断控制器,从 Linux 的角度来看,相当于是一个外设,因而其对GIC的功能实现是放在"./drivers/irqchip/irq-gic-v3.c
" 系列文件中的。
gic_handle_irq()
就是 GIC 中断处理的入口了,它首先通过 gic_read_iar()
读取 IAR 寄存器做出 ACK应答,而后判断中断源的类型。
drivers/irqchip/irq-gic-v3.c
void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)
u32 irqnr = gic_read_iar(); //ACK(pending --> active)
// PPI, SPI or LPI
if (likely(irqnr > 15 && irqnr < 1020) || irqnr >= 8192) {
gic_write_eoir(irqnr);
handle_domain_irq(gic_data.domain, irqnr, regs);
}
if (irqnr < 16) { // SGI
gic_write_eoir(irqnr);
#ifdef CONFIG_SMP
handle_IPI(irqnr, regs);
#else
WARN_ONCE(true, "Unexpected SGI received!\n");
#endif
}
}
当中断经过中断控制器 到达 CPU 后,Linux
会首先通过 irq_find_mapping()
函数,根据物理中断号 “hwirq
” 的值,查找上文讲到的包含映射关系的 radix tree
或者 线性数组,得到 “hwirq
” 对应的虚拟中断号 “irq
”。
handle_domain_irq
-->__handle_domain_irq
-->irq = irq_find_mapping(domain, hwirq);
-->generic_handle_irq(irq);
TODO: irq_find_mapping()
generic_handle_irq()
回调 IRQ 第一级处理函数(desc->handle_irq
)。
generic_handle_irq()
-->struct irq_desc *desc = irq_to_desc(irq);
-->generic_handle_irq_desc(desc);
-->desc->handle_irq(desc);
-->handle_level_irq(struct irq_desc *desc)
-->handle_irq_event(desc);
其中 generic_handle_irq(irq)
函数通过中断号找到全局中断描述符数组 irq_desc[NR_IRQS]
中的一项,然后执行该 irq 号注册的 action
。
(1) 对于电平触发,注册的 “handle_irq
” 是 handle_level_irq()
;
(2) 对于边沿触发,注册的 “handle_irq
” 是 handle_edge_irq()
;
相关代码位于"/kernel/irq/chip.c
"。自此,ISR 第一级中断处理流程完成。
第一级的中断处理完成后,就是遍历 IRQ 线上的链表,依次执行通过前面讲的 request_threaded_irq()
安装的各个 "irq_action
",也就是第二级的处理函数(action->handler
)。这里需要判断是不是自己的设备产生的中断,
-->handle_irq_event_percpu(desc);
while (action) {
...
action->handler(irq, action->dev_id);
action = action->next;
...
}
推荐阅读:
https://zhuanlan.zhihu.com/p/85353687
https://zhuanlan.zhihu.com/p/83709066
https://zhuanlan.zhihu.com/p/185851980