ARM64 中断处理流程

2018年6月22日 9.68k 次阅读 0 条评论 9 人点赞

目录

如果说 Linux 内核是个身体,保罗万象,那么中断就如同是其中更为关键的神经系统,有了他的存在,中央处理器才能了解各个外设所处的状态,并且及时的给与支持响应。

本章的重点不是介绍如何使用中断与中断 API,更不是为了了解软件中断、硬件中断、tasklet 微任务以及工作队列有何分别,我们仅仅只是为了观察中断时如何一步步传递上来又是如何处理的流程的呢?

中断注册

中断即 CPU 在执行进程上下文期间,出现了某些突发情况,导致 CPU 必须暂停任务转而执行中断上下文。从定义上来讲,其实只要是突然中断 CPU 执行的都可以叫做中断,那么中断到底是如何一步步完成的呢?

首先,当然是需要触发中断,我们假设以鼠标驱动为例,在按下按键的瞬间,中断就发生了。代码位于 drivers/input/mouse/amimouse.c

static int amimouse_open(struct input_dev *dev)
{
    unsigned short joy0dat;
    int error;

    joy0dat = amiga_custom.joy0dat;

    amimouse_lastx = joy0dat & 0xff;
    amimouse_lasty = joy0dat >> 8;

    error = request_irq(IRQ_AMIGA_VERTB, amimouse_interrupt, 0, "amimouse",
                dev);
    if (error)
        dev_err(&dev->dev, "Can't allocate irq %d\n", IRQ_AMIGA_VERTB);

    return error;
}

通过 request_irq 函数申请一个 IRQ 号为 IRQ_AMIGA_VERTB 的中断,绑定中断设置触发后回调函数为 amimouse_interrupt,只要返回值等于 0 则表示注册成功。

如何申请中断?

尽管我们只需要关心 request_irq 是否执行成功,则可以了解中断是否注册成功,但是作为技术研讨,我们需要更为深层次的了解到底是如何做到中断注册的呢?

进入到 request_irq 这个函数本体,观察代码上下文 include/linux/interrupt.h

static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
            const char *name, void *dev)
{
        return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}

可以看出,request_irq 其实就是将 request_threaded_irq 函数的第三个参数置为 NULL,关于 request_threaded_irq 这个函数原本是属于内核上游的 realtime 分支的中断底半部实现方式,传统的中断底半部实现方式包括软中断tasklet(微任务)以及 workqueue(工作队列),软中断和 tasklet 都是运行在中断上下文,所以不能上锁,而且不能睡眠等限制条件,工作队列可以睡眠,所以一般需要进行超长时间的任务或者需要睡眠的任务都交给工作队列。中断线程化可以认为与工作队列类似,与工作队列相比,他的接口更为简单,编码更为方便,由于它是运转在进程上下文,所以他与系统中的其他进程一同争用 CPU 时间,所以不会强制打断其他程序的执行,可以获得最佳的系统实时性。

中断函数执行

中断的触发是个很有意思的过程,这需要与特定的 CPU 进行结合,所以在中断如何往上传递的过程是一个非通用的代码(非 CPU 通用,但是同架构的可以通用),所以这样的代码自然只能是存在在各自的 arch 相关目录下面。

CPU 如何找到异常向量表

我们都知道,对于一些硬件资源的访问,一般都是存在一些寄存器提供访问的,同时为了让 CPU 也能按照预期执行,会设置一些基址供给 CPU 在某些情况下调用,比如异常向量表就是这样的一个寄存器,他需要你告诉 CPU 在发生异常时如何处理。那么到底是什么情况?一起来看一下 arch/arm64/kernel/entry.S

        .align  11
ENTRY(vectors)
        ventry  el1_sync_invalid                // Synchronous EL1t
        ventry  el1_irq_invalid                 // IRQ EL1t
        ventry  el1_fiq_invalid                 // FIQ EL1t
        ventry  el1_error_invalid               // Error EL1t

        ventry  el1_sync                        // Synchronous EL1h
        ventry  el1_irq                         // IRQ EL1h
        ventry  el1_fiq_invalid                 // FIQ EL1h
        ventry  el1_error_invalid               // Error EL1h

        ventry  el0_sync                        // Synchronous 64-bit EL0
        ventry  el0_irq                         // IRQ 64-bit EL0
        ventry  el0_fiq_invalid                 // FIQ 64-bit EL0
        ventry  el0_error_invalid               // Error 64-bit EL0

#ifdef CONFIG_COMPAT
        ventry  el0_sync_compat                 // Synchronous 32-bit EL0
        ventry  el0_irq_compat                  // IRQ 32-bit EL0
        ventry  el0_fiq_invalid_compat          // FIQ 32-bit EL0
        ventry  el0_error_invalid_compat        // Error 32-bit EL0
#else
        ventry  el0_sync_invalid                // Synchronous 32-bit EL0
        ventry  el0_irq_invalid                 // IRQ 32-bit EL0
        ventry  el0_fiq_invalid                 // FIQ 32-bit EL0
        ventry  el0_error_invalid               // Error 32-bit EL0
#endif
END(vectors)

上面的代码就是异常向量表,可以清晰的看出来在代码段内定义了这样的一个规整的跳转列表1,既然有了跳转向量表,那么仍然需要将它注册给 CPU,在 MMU 单元启用的汇编中设置了这样的一个寄存器arch/arm64/kernel/head.S2

        .section        ".idmap.text", "ax"
__enable_mmu:
        mrs     x1, ID_AA64MMFR0_EL1
        ubfx    x2, x1, #ID_AA64MMFR0_TGRAN_SHIFT, 4
        cmp     x2, #ID_AA64MMFR0_TGRAN_SUPPORTED
        b.ne    __no_granule_support
        ldr     x5, =vectors
        msr     vbar_el1, x5
        msr     ttbr0_el1, x25                  // load TTBR0
        msr     ttbr1_el1, x26                  // load TTBR1
        isb
        msr     sctlr_el1, x0
        isb
        /*
        ¦* Invalidate the local I-cache so that any instructions fetched
        ¦* speculatively from the PoC are discarded, since they may have
        ¦* been dynamically patched at the PoU.
        ¦*/
        ic      iallu
        dsb     nsh
        isb
        br      x27
ENDPROC(__enable_mmu)

关键的一句 ldr x5, =vectors; msr vbar_el1, x5,将 vectors 的值付给了 vbar_el1 寄存器,相当于告诉 CPU 我的异常向量表位置何在。CPU 通过自己的判断,观察异常发生的类别,跳转到对应的异常处理函数。

接下来中断又是如何找到注册的回调呢?

通过上一节已经知道中断可以通过异常向量表的形式提交给 CPU,CPU 在发生硬件中断时跳转到 el1_irq 或者 el0_irq 这样的函数体内,EL0 与 EL1 的差异在于异常等级,ARM64 支持 4 个异常等级 E0 - E3,数字越大权限越高。

entry.S 文件的函数 el1_irq 中进入 irq_handler 这个宏函数

        .align  6
el1_irq:
        kernel_entry 1
        enable_dbg
#ifdef CONFIG_TRACE_IRQFLAGS
        bl      trace_hardirqs_off
#endif

        irq_handler

#ifdef CONFIG_PREEMPT
        get_thread_info tsk
        ldr     w24, [tsk, #TI_PREEMPT]         // get preempt count
        cbnz    w24, 1f                         // preempt count != 0
        ldr     x0, [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
ENDPROC(el1_irq)

这个 irq_handler 函数调用到了 handler_arch_irq,很明显,这是一个与架构相关的 irq 处理函数。

        .macro  irq_handler
        adrp    x1, handle_arch_irq
        ldr     x1, [x1, #:lo12:handle_arch_irq]
        mov     x0, sp
        blr     x1
        .endm

继续往下追踪,在文件 arch/arm64/kernel/irq.c 中,handle_arch_irq 是一个全局函数变量,通过函数 set_handle_irq 将该变量设置上并提供给 setup.S 中的 irq_handler 执行,也就是异常向量表也就可以执行到当前位置。

void (*handle_arch_irq)(struct pt_regs *) = NULL;

void __init set_handle_irq(void (*handle_irq)(struct pt_regs *))
{
        if (handle_arch_irq)
                return;

        handle_arch_irq = handle_irq;
}

现在我们往下找到设置这个 set_handle_irq 的位置,并且观察当前系统是调用的何种回调函数,在文件 drivers/irqchip/irq-gic-v3.c 中:


static int __init gic_init_bases(void __iomem *dist_base, ¦struct redist_region *rdist_regs, ¦u32 nr_redist_regions, ¦u64 redist_stride, ¦struct fwnode_handle *handle) { ... set_handle_irq(gic_handle_irq); ... }

GIC 中断处理驱动中将 gic_handle_irq 注册给 handle_arch_irq 中,所以当 CPU 捕获到中断异常之后,最终可以进入到 gic_handle_irq 函数,这个就是当前的中断处理的入口函数,通过它可以查表等获取到最终是哪一个中断号发生的中断。


  1. ventry 就是一个简单的占据 8 个字节的跳转指令,具体实现逻辑参考 arch/arm64/include/asm/assembler.h 下的定义 ↩︎
  2. 细心的朋友发现我之前给的链接都是 4.15 版本的,但是这个位置给的是 4.4 的,其实在 4.15 上面原理是一致的,不过由于我翻越的代码是 4.4 的,所以此处列举的是 4.4 的代码,4.15 的函数上下文稍有不同。 ↩︎
标签:
最后编辑:2020年12月30日