Interrupt Pipeline系列文章大纲-CSDN博客
2.3 el0_irq
2.3.1 el0_irq代码框架
2.3.2 kernel_entry 0 与kernel_exit 0
2.3.3 el0_irq_naked与enable_da_f
2.3.4 trace_hardirqs_off与trace_hardirqs_on
2.3.5 irq_handler
2.3.6 返回用户空间
2.3.5 irq_handler
2.3.5.1 概要
在原生irq_handler中,通过ldr_l加载函数handle_arch_irq的地址到通用寄存器X1。其中,ldr_l并不是ARM64指令,而是内核引入宏定义,定义在arch/arm64/include/asm/assembler.h,通过adrp和ldr指令的组合,加载当前PC值一定范围[-4GB~4GB]内的符号地址到寄存器。
如果打开了CONFIG_IPIPE配置项,那么通过ldr伪指令,加载函数handle_arch_irq_pipelined到通用寄存器X1中。
通过mov x0, sp指令,把当前的栈指针存入通用寄存器X0,作为函数的入参。之前已经调用kernel_entry 0,将进程运行的用户态现场按照struct pt_regs的格式存入进程内核栈。所以函数的入参就是struct pt_regs *regs。
调用irq_stack_entry,切换到内核中断栈;通过blr x1执行handle_arch_irq_pipelined函数,函数返回后,返回值存在W0;然后调用irq_stack_exit退出内核中断栈。
2.3.5.2 irq_stack_entry/irq_stack_exit
ARM64的Linux内核在2015年之前中断没有自己独立的栈,都是借用线程的内核栈。独立的中断栈是在commit 8e23dacd12 arm64: Add do_softirq_own_stack() and enable irq_stacks中引入的。
如果打开了CONFIG_IPIPE配置项,实时内核co-kernel(Xenomai)允许在中断栈上进行上下文切换,并且允许更多的中断发生在sibling栈上下文中。那么原先判断当前是否在中断栈的汇编代码将不可用,所以I-pipe实现了一套新的方法,如果per cpu变量irq_nesting为0,则说明此时不在中断栈中,否则已经在中断栈中。
- irq_stack_entry
1 | .macro | irq_stack_entry | ||
2 | mov | x19, sp | // preserve the original sp | |
3 | ||||
4 | #ifdef CONFIG_IPIPE | |||
5 | /* | |||
6 | * When the pipeline is enabled, context switches over the irq | |||
7 | * stack are allowed (for the co-kernel), and more interrupts | |||
8 | * can be taken over sibling stack contexts. So we need a not so | |||
9 | * subtle way of figuring out whether the irq stack was actually | |||
10 | * exited, which cannot depend on the current task pointer. | |||
11 | */ | |||
12 | adr_this_cpu x25, irq_nesting, x26 | |||
13 | ldr | w26, [x25] | ||
14 | cmp | w26, #0 | ||
15 | add | w26, w26, #1 | ||
16 | str | w26, [x25] | ||
17 | b.ne | 9998f | ||
18 | #else | |||
19 | /* | |||
20 | * Compare sp with the base of the task stack. | |||
21 | * If the top ~(THREAD_SIZE - 1) bits match, we are on a task stack, | |||
22 | * and should switch to the irq stack. | |||
23 | */ | |||
24 | ldr | x25, [tsk, TSK_STACK] | ||
25 | eor | x25, x25, x19 | ||
26 | and | x25, x25, #~(THREAD_SIZE - 1) | ||
27 | cbnz | x25, 9998f | ||
28 | #endif | |||
29 | ||||
30 | ldr_this_cpu x25, irq_stack_ptr, x26 | |||
31 | mov | x26, #IRQ_STACK_SIZE | ||
32 | add | x26, x25, x26 | ||
33 | ||||
34 | /* switch to the irq stack */ | |||
35 | mov | sp, x26 | ||
36 | 9998: | |||
37 | .endm |
第2行,保存当前栈指针SP_EL1到X19.
第12行,加载per cpu变量irq_nesting的地址到X25. 变量定义在arch/arm64/kernel/ipipe.c,用于对每个CPU核进出中断栈进行计数。
/* irq_nesting tracks the interrupt nesting level for a CPU. */
DEFINE_PER_CPU(int, irq_nesting);
第13行,把per cpu变量irq_nesting的值加载到W26,即X26的低32位。
第14行,用cmp指令比较W26和立即数0,比较结果影响CPSR中的条件标志位Z。如果二者相等,则条件标志位Z为1;否则为0.
第15~16行,对per cpu变量irq_nesting进行加1。使用了ADD指令,对CPSR中的条件标志位Z无影响。
第17行,如果第14行cmp指令比较结果为二者相等(条件标志位Z为1),则说明per cpu变量irq_nesting的值为0,即从未进入到中断栈,那么继续向下执行第30行。per cpu变量irq_nesting的值大于或等于1,即已经进入中断栈,不需要进行进程内核栈到中断栈的切换,直接向前(forward)跳转到9998f,退出宏。
第30行,加载per cpu变量irq_stack_ptr的地址到X25。irq_stack_ptr定义和初始化在arch/arm64/kernel/irq.c,它指向中断栈的栈顶(低地址),因此X25里面存放的是中断栈栈顶地址。如果定义了CONFIG_VMAP_STACK,那么中断栈是运行时动态申请的;否则,中断栈是编译时就定义的per cpu数组irq_stack。
DEFINE_PER_CPU(unsigned long *, irq_stack_ptr);
第31~32行,ARM64的中断栈的栈底地址到栈顶地址,是由高到低的。将X25存放的中断栈栈顶地址向上偏移#IRQ_STACK_SIZE,得到中断栈栈底地址,存入X26。
第35行,让SP_EL1栈指针指向中断栈栈底。接下来调用irq_handler函数,函数里面会自动把SP_EL1向下压栈。
- irq_stack_exit
1 | /* | ||
2 | * x19 should be preserved between irq_stack_entry and | ||
3 | * irq_stack_exit. I-pipe: have to preserve w0 too. | ||
4 | */ | ||
5 | .macro | irq_stack_exit | |
6 | mov | sp, x19 | |
7 | #ifdef CONFIG_IPIPE | ||
8 | adr_this_cpu x1, irq_nesting, x2 | ||
9 | ldr | w2, [x1] | |
10 | add | w2, w2, #-1 | |
11 | str | w2, [x1] | |
12 | #endif | ||
13 | .endm |
第3行,此处注释提到了I-pipe必须避免在irq_stack_exit中修改W0。W0中存放着blr x1执行后的返回值,会影响后续代码的执行。
第6行,从通用寄存器X19中,恢复原来的内核进程栈到SP_EL0。
第8~11行,对per cpu变量irq_nesting的值进行减1操作,可以参考irq_stack_entry的解读。
2.3.5.3 handle_arch_irq_pipelined函数
如果打开了CONFIG_IPIPE配置项,那么函数调用顺序irq_handler->handle_arch_irq变成了irq_handler->handle_arch_irq_pipelined->handle_arch_irq。
//arch/arm64/kernel/irq.c #ifdef CONFIG_IPIPE asmlinkage int handle_arch_irq_pipelined(struct pt_regs *regs) { handle_arch_irq(regs); //仍然调用了原来该调用的函数 return __ipipe_root_p && !irqs_disabled(); //注意返回值 } #endif |
新增的handle_arch_irq_pipelined除了调用原先的handle_arch_irq之外,就是改变了irq_handler的返回值。根据返回值的不同,el0_irq后续的代码走向是不同的,后面章节会相信分析,这里重点分析handle_arch_irq。
全局函数指针handle_arch_irq(),是中断处理程序C语言部分的入口,由函数set_handle_irq进行初始化。
void (*handle_arch_irq)(struct pt_regs *) __ro_after_init; int __init set_handle_irq(void (*handle_irq)(struct pt_regs *)) { if (handle_arch_irq) return -EBUSY; handle_arch_irq = handle_irq; return 0; } |
以ARM的GIC V3中断控制器为例,irq-gic-v3.c中调用set_handle_irq(gic_handle_irq),设置handle_arch_irq指向gic_handle_irq。
start_kernel <init/main.c> | ||||||
-> | init_IRQ <arch/arm64/kernel/irq.c> | |||||
-> | irqchip_init <drivers/irqchip/irqchip.c> | |||||
-> | of_irq_init(__irqchip_of_table) <drivers/of/irq.c> | |||||
-> | gic_of_init <drivers/irqchip/irq-gic-v3.c> | |||||
-> | gic_init_bases <drivers/irqchip/irq-gic-v3.c> | |||||
-> | set_handle_irq(gic_handle_irq) |
2.3.5.4 irq_handler返回值
W0寄存器里面存放着irq_handler的返回值。根据返回值不同将走向不同的返回用户空间流程。
__ipipe_root_p用于判断当前是在root domain还是head domain。如果el0_irq发生时,用户进程是非实时进程,或实时进程切换到了root domain运行,此时__ipipe_root_p为1;否则代表实时进程在head domain运行,此时__ipipe_root_p为0。
#define __ipipe_root_p (__ipipe_current_domain == ipipe_root_domain) |
函数irqs_disabled()是一个宏定义,定义在include/linux/irqflags.h。为简化分析,在关闭CONFIG_TRACE_IRQFLAGS_SUPPORT的情况下,展开分析。
#define irqs_disabled() raw_irqs_disabled()
#define raw_irqs_disabled() (arch_irqs_disabled())
I-pipe没有使用默认的函数arch_irqs_disabled(),直接定义了此函数,在arch/arm64/include/asm/ipipe_hwirq.h:
#define arch_irqs_disabled() ipipe_test_root()
ipipe_test_root()在kernel/ipipe/core.c中定义:
1 | unsigned long ipipe_test_root(void) | |
2 | { | |
3 | unsigned long flags; | |
4 | int x; | |
5 | ||
6 | flags = hard_smp_local_irq_save(); | |
7 | x = test_bit(IPIPE_STALL_FLAG, &__ipipe_root_status); | |
8 | hard_smp_local_irq_restore(flags); | |
9 | ||
10 | return x; | |
11 | } | |
12 | EXPORT_SYMBOL(ipipe_test_root); |
第7行,对__ipipe_root_status中的IPIPE_STALL_FLAG(bit 0)进行判断,如果是1则返回1,否则返回0。IPIPE_STALL_FLAG是root域的虚拟中断标识,如果是1,代表虚拟中断屏蔽;如果是0,代表虚拟中断打开。
#define IPIPE_STALL_FLAG 0 /* interrupts (virtually) disabled. */
ipipe_stall_root:__set_bit(IPIPE_STALL_FLAG, &__ipipe_root_status); //屏蔽
ipipe_unstall_root:__clear_bit(IPIPE_STALL_FLAG, &p->status); //打开
综合二者,可知返回值有如下组合:
场景 | __ipipe_root_p | IPIPE_STALL_FLAG | irq_handler返回值 |
位于root域, | 1 | 0 | 1 |
位于root域, | 1 | 1 | 0 |
0 | x | 0 |
接下来根据返回值进行分析返回用户空间的流程。
点击查看系列文章 =》 Interrupt Pipeline系列文章大纲-CSDN博客
原创不易,需要大家多多鼓励!您的关注、点赞、收藏就是我的创作动力!