【ARMv8 异常模型入门及渐进5 - IRQ 异常处理流程】

文章目录

    • 1.1 IRQ number/irq_domain
      • 1.1.1 中断控制器linux描述
      • 1.1.2 中断linux描述
    • 1.2 linux 内核 GIC中断管理
      • 1.2.1 linux GIC中断处理流程
      • 1.2.2 gic_handle_irq 的实现
      • 1.2.3 ISR 的执行
      • 1.2.4 ISR第一级处理函数
      • 1.2.5 ISR第二级处理

1.1 IRQ number/irq_domain

每个 IRQ 同时有 “irq”“hwirq” 两个编号。这里 “hwirq”硬件中断号,或者说是物理中断号,也就是芯片手册上写的那个号码,而 “irq” 是软件使用的中断号,或者说是逻辑/虚拟中断号

如果芯片中有多个中断控制器,假设它们各自都对应 16IRQ,这 16IRQ 的编号都是从 015,那 CPU 收到中断后,软件单凭中断号是分不清这个中断到底是从哪个中断控制器派发过来的,它需要的是中断控制器自身的编号加上 IRQ 的编号。

为此,我们需要一种将中断控制器 local 的物理中断号转换成 Linux 全局的虚拟中断号的机制,而接下来要介绍的 struct irq_domain 就承担着这个转换的角色。

1.1.1 中断控制器linux描述

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);
	...
};
  • name" 是中断控制器的名称,就是我们在"/proc/interupts"中看到的那个;
  • irq_enable/irq_unmask 用于中断使能;
  • irq_disable/irq_mask 用于中断屏蔽。

1.1.2 中断linux描述

既然说到对每个 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;	
	...
};
  • “name” 是这个 IRQ 的名称,同样可以在"/proc/interupts"中查看;
  • “depth” 是关闭该 IRQ 的嵌套深度,正值表示禁用中断,而 0 表示可启用中断;
  • irq_desc" 中的 “action” 是 IRQ 对应的中断处理函数(ISR - Interrupt Service Routine);
  • “irq_data”中断控制器紧密联系的这部分被单独提取出来,构成了 "irq_data" 结构体
    两者的绑定是通过 irq_set_chip() 函数完成的。

1.2 linux 内核 GIC中断管理

下文以 SPI 中断为例:
当中断产生后,中断信号送给 GICGIC 组件进行管理和仲裁,然后再将中断信息发送给相应的 cpu, cpu 立刻进入 FIQIRQ 异常程序(这里是可以通过寄存器来控制的)。
【ARMv8 异常模型入门及渐进5 - IRQ 异常处理流程】_第1张图片

1.2.1 linux GIC中断处理流程

ARM64 的异常向量表 vectors 中设置了各种异常的入口(位于arm/arm64/kernel/entry.S)。
目前有效的异常入口有:

  • 两个同步异常 el0_sync, el1_sync
  • 两个异步异常 el0_irq, el1_irq
    el0_irqel1_irq 的具体实现略有不同,但处理流程大致是相同的。
    其他异常入口暂时都 invalid
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 寄存器、splrelr_el1spsr_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手册讲到的进入异常前的处理:

  • 保存 PSTATESPSR_ELx 寄存器;
  • PSTATE 中的 DAIF全部屏蔽;
  • 保存 PC 寄存器的值到 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

上面主要的三个动作:

  1. 进入中断栈;
.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
  1. 执行中断控制器的 handle_arch_irq;
    hand_arch_irq 是在gic 初始化的时候进行赋值的:
static int __int gic_of_init(struce device_node *node)
{
    ...
    set_handle_irq(gic_handle_irq);
    ...
}
  1. 退出中断栈。

(3) kernel_exit 基本上是 kernel_entry 的逆过程,最后使用 eret 退出异常。

1.2.2 gic_handle_irq 的实现

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
        }
}
  • 如果中断号小于16,说明是SGI,就按照 IPI 核间中断 的方式进行处理,当然前提是这是一个SMP的系统。
  • 如果中断号介于 16 和 1020 之间,或者大于 8192,说明是 PPI, SPI或者 LPI,那么就按照普通流程处理。

1.2.3 ISR 的执行

当中断经过中断控制器 到达 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()

1.2.4 ISR第一级处理函数

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 第一级中断处理流程完成。

1.2.5 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

你可能感兴趣的:(#,ARM,System,Exception,linux,arm)