中断流程老生常谈,但我一直以来也只是知道中断过来之后,会保护现场,跳到中断向量表,执行中断,恢复现场,然后返回。至于更多细节,就不得而知了。这篇文章旨在把更完整的linux 中断处理流程梳理一遍。主要带着以下几个问题:
- 中断向量表是在代码哪里配置的
- 响应中断时,必然有一些读写gic 寄存器的操作,这些操作在什么位置
- /proc/interrupts 这个proc 文件是如何实现的
- linux 进入中断前,会屏蔽其他所有中断,这个是如何实现的
- 系统调用的系统中断0x80,是如何从用户态调用到内核态file_operations接口的?和普通的中断有什么区别
本文代码均来自linux4.19
配置中断向量表
从 arch/arm64/kernel/vmlinux.lds.S 中可以看到,内核ENTRY 为_text,
ENTRY(_text)
...
.head.text : {
_text = .;
HEAD_TEXT
}
HEAD_TEXT定义在如下位置
#define HEAD_TEXT KEEP(*(.head.text))
也就是说linux的入口为指定为.head.text senction 的代码,grep 可以发现
#define __HEAD .section ".head.text","ax"
__HEAD宏仅在在如下位置使用
__HEAD
_head:
...
b stext
...
ENTRY(stext)
...
b __primary_switch
ENDPROC(stext)
__primary_switched:
...
adr_l x8, vectors
msr vbar_el1, x8
isb
...
b start_kernel
ENDPROC(__primary_switched)
从上边的代码中可以看到从linux 入口到设置向量表的完整流程。最终设置向量表的指令,就是将vector 的地址,写入vbar_el1 寄存器。 对于vbar_el1 寄存器的作用,arm 官方文档的描述如下 对于其他vbar_elX 的写入类似,不再赘述。
中断向量表详解
ENTRY(vectors)
...
kernel_ventry 1, sync
kernel_ventry 1, irq
kernel_ventry 1, fiq_invalid
kernel_ventry 1, error
kernel_ventry 0, sync
kernel_ventry 0, irq
kernel_ventry 0, fiq_invalid
kernel_ventry 0, error
...
kernel_ventry 宏定义(忽略所有#ifdef)如下:
.macro kernel_ventry, el, label, regsize = 64
.align 7
sub sp, sp, #S_FRAME_SIZE
b el\()\el\()_\label
.endm
可以看到,“kernel_ventry 1, irq” 最终跳转到 el1_irq去执行
现场保护与中断栈切换
el1_irq:
kernel_entry 1
enable_da_f (msr daifclr, #(8 | 4 | 1))
irq_handler
kernel_exit 1
ENDPROC(el1_irq)
保存现场比较冗长,放到了文末。 接着屏蔽了DAIF中的DAF。这里看起来是不希望处理irq 的时候,被DAF 打断。具体为什么暂不探究了。。。 然后就开始进入中断处理irq_handler 了。处理完成后使用kernel_exit 恢复现场。 irq_handler 代码如下,
.macro irq_handler
ldr_l x1, handle_arch_irq
mov x0, sp
irq_stack_entry
blr x1
irq_stack_exit
.endm
注意,在跳转handle_arch_irq 之前与之后分使用irq_stack_entry 和irq_stack_exit 进行了栈指针切换。irq_stack_entry 代码如下
.macro irq_stack_entry
mov x19, sp
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
mov x26, #IRQ_STACK_SIZE
add x26, x25, x26
mov sp, x26
9998:
.endm
handle_arch_irq 详解
从这里就完全进入c 代码了。先看下handle_arch_irq 是怎么来的。
void (*handle_arch_irq)(struct pt_regs *) __ro_after_init;
IRQCHIP_DECLARE(gic_v3, "arm,gic-v3", gic_of_init);
gic_of_init
gic_init_bases
set_handle_irq(handle_irq=gic_handle_irq)
handle_arch_irq = handle_irq
gic_handle_irq
附:kernel_entry 保护现场详解
.macro kernel_entry, el, regsize = 64
.if \regsize == 32
mov w0, w0
.endif
stp x0, x1, [sp, #16 * 0]
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
clear_gp_regs
mrs x21, sp_el0
ldr_this_cpu tsk, __entry_task, x20
ldr x19, [tsk, #TSK_TI_FLAGS]
disable_step_tsk x19, x20
apply_ssbd 1, x22, x23
.else
add x21, sp, #S_FRAME_SIZE
get_thread_info tsk
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
mrs x22, elr_el1
mrs x23, spsr_el1
stp lr, x21, [sp, #S_LR]
.if \el == 0
stp xzr, xzr, [sp, #S_STACKFRAME]
.else
stp x29, x22, [sp, #S_STACKFRAME]
.endif
add x29, sp, #S_STACKFRAME
#ifdef CONFIG_ARM64_SW_TTBR0_PAN
alternative_if ARM64_HAS_PAN
b 1f
alternative_else_nop_endif
.if \el != 0
mrs x21, ttbr0_el1
tst x21, #TTBR_ASID_MASK
orr x23, x23, #PSR_PAN_BIT
b.eq 1f
and x23, x23, #~PSR_PAN_BIT
.endif
__uaccess_ttbr0_disable x21
1:
#endif
stp x22, x23, [sp, #S_PC]
.if \el == 0
mov w21, #NO_SYSCALL
str w21, [sp, #S_SYSCALLNO]
.endif
.if \el == 0
msr sp_el0, tsk
.endif
.endm
参考资料
1.DDI0487D_b_armv8_arm.pdf 2.DEN0024A_v8_architecture_PG.pdf 3. https://www.jianshu.com/p/a9b5bc27ec83 4. Generic Interrupt Controller Architecture Specification
|