上一篇博客在最后,我们大致介绍了一下中断子系统的执行流程,这一节我们将从Linux源码层面去中断是如何原型。

一、裸机中断

我们首先回忆一下裸机程序中的中断流程是怎样的,以Mini2440按键K1外部中断为例

1、使能外部,开启外部中断EINTMASK 、中断源INTMSK 、开启IRQ总中断;

2、按键按下,触发中断源EINT8_23、CPU接收到中断信号;

3、CPU跳转到中断向量入口执行(0x18中断地址处);

  • 使用stmdb将寄存器值保存在栈顶(保护现场);
  • 执行中断处理函数;
  • 使用ldmia将栈顶处数据读出到寄存器中,并使pc=lr(恢复现场);

二、Linux中的中断处理

这一节我们将分析linux中CPU跳转到中断向量入口执行之后的流程:

  • 异常向量表0xFFFF0018中断地址处定义的vector_irq;
  • irq_handler中断处理函数的执行,在该中断处理函数中会根据IRQ编号找到绑定的我们自己的中断处理函数;

关于中断的注册(使能)、卸载(关闭)、以及每一个中断和对应中断处理函数的绑定会在后面小节一一介绍。

2.1 高端异常向量模式

实际上,ARM的异常向量表有两种选择,一种是低端向量,向量地址位于0x00000000,另一种是高端地址,向量地址0xffff0000,linux中选择使用高端向量模式,也就是说,当异常发生时,CPU会把PC指针自动跳转到始于0xffff0000开始的某一个地址上:

地址异常模式
FFFF0000 复位 管理模式(linux内核运行模式)
FFFF0004 未定义指令 未定义模式
FFFF0008 软中断(swi) 管理模式
FFFF000C Prefetch abort 终止模式
FFFF0010 Data abort 终止模式
FFFF0014 保留 保留
FFFF0018 IRQ 中断模式
FFFF001C FIQ 快中断模式

2.2 异常向量表

异常向量表在arch/arm/kernel/entry-armv.S中定义,为了方便讨论,下面只列出部分关键的代码:

.L__vectors_start:
        W(b)    vector_rst
        W(b)    vector_und
        W(ldr)  pc, .L__vectors_start + 0x1000
        W(b)    vector_pabt
        W(b)    vector_dabt
        W(b)    vector_addrexcptn
        W(b)    vector_irq
        W(b)    vector_fiq

__vectors_start是向量表基地址,在arch/arm/kernel/vmlinux.lds中定义了__vectors_start的链接地址为0xffff0000:

 __vectors_start = .; 
.vectors 0xffff0000 : AT(__vectors_start) { 
    *(.vectors) 
} 
. = __vectors_start + SIZEOF(.vectors);
 __vectors_end = .;
__stubs_start = .; 
.stubs ADDR(.vectors) + 0x1000 : AT(__stubs_start) { 
    *(.stubs) 
} 
. = __stubs_start + SIZEOF(.stubs); 
__stubs_end = .; 

这里定义两个段:

.vectors段:起始地址为__vectors_start(0xffff0000),结束地址为__vectors_end;

.stubs段:起始地址为__stubs_start(0xffff0000+0x1000) ,结束地址为__stubs_end ;

在链接脚本中定义的变量是可以直接在汇编代码中使用的,比如这里的__vectors_start、__vectors_end 等。

2.3 vector_sub宏

异常向量表中的vector_rst、vector_und等是通过vector_stub宏定义的:

/*
 * Vector stubs.
 *
 * This code is copied to 0xffff1000 so we can use branches in the
 * vectors, rather than ldr's.  Note that this code must not exceed
 * a page size.
 *
 * Common stub entry macro:
 *   Enter in IRQ mode, spsr = SVC/USR CPSR, lr = SVC/USR PC
 *
 * SP points to a minimal amount of processor-private memory, the address
 * of which is copied into r0 for the mode specific abort handler.
 */
        .macro  vector_stub, name, mode, correction=0
        .align  5

vector_name:                                                   @定义不同的宏 比如vector_irq
        .if correction                                         @判断correction是否为0      
        sub     lr, lr, #correction                            @计算返回地址
        .endif

        @
        @ Save r0, lr_<exception> (parent PC) and spsr_<exception>
        @ (parent CPSR)
        @
        stmia   sp, {r0, lr}            @ save r0, lr
        mrs     lr, spsr
        str     lr, [sp, #8]            @ save spsr

        @
        @ Prepare for SVC32 mode.  IRQs remain disabled.
        @
        mrs     r0, cpsr
        eor     r0, r0, #(mode ^ SVC_MODE | PSR_ISETSTATE)
        msr     spsr_cxsf, r0

        @
        @ the branch table must immediately follow this code
        @
        and     lr, lr, #0x0f
 THUMB( adr     r0, 1f                  )
 THUMB( ldr     lr, [r0, lr, lsl #2]    )
        mov     r0, sp
 ARM(   ldr     lr, [pc, lr, lsl #2]    )
        movs    pc, lr                  @ branch to handler in SVC mode
ENDPROC(vector_name)
.align 2
@ handler address follow this label
1:
.endm

定义带有三个参数的宏vector_stub。比如下面语句:

 vector_stub     irq, IRQ_MODE, 4

宏展开后就变成了:

vector_irq:
        sub     lr, lr, 4                     @计算返回地址  lr=保存的是进入中断前PC的值,就是发生中断所在的指令地址+8  这里减44,也就是正常执行的下一条指令地址

        @
        @ Save r0, lr_<exception> (parent PC) and spsr_<exception>
        @ (parent CPSR)
        @
        stmia   sp, {r0, lr}            @ save r0, lr   @保存r0、lr
        mrs     lr, spsr                @ lr=spsr 这里保存的是上一个模式下的cpsr的值
        @在保存现场时,如果处于svc模式下时,cpsr寄存器是写入irq模式下的spsr_irq寄存器,而不是svc模式下的spsr_svc,这样,在中断模式下恢复的话,
        @将spsr_irq寄存器里的内容写入cpsr,就能恢复到svc模式了,因为,spsr_irq寄存器里的内容就是svc模式下的状态
        str     lr, [sp, #8]            @ save spsr     @保存spsr

        @
        @ Prepare for SVC32 mode.  IRQs remain disabled.
        @
        mrs     r0, cpsr                                 @r0=cpsr
        eor     r0, r0, #(IRQ_MODE ^ SVC_MODE | PSR_ISETSTATE)  
        msr     spsr_cxsf, r0                            @修改当前模式下的spsr 写回

         @
         @ the branch table must immediately follow this code
         @
         and    lr, lr, #0x0f               @lr=lr&0x0f 获取进入irq模式前的cpsr的模式位
         mov  r0, sp
         ldr     lr, [pc, lr, lsl #2]       @如果进入irq前是usr,则lr=*(PC+0<<2),即__irq_usr, 如果进入irq前是svc,lr=*(PC+3<<2),即__irq_svc

         movs pc, lr                        @跳转到下面某处,且目标寄存器是pc,指令S结尾,最后会恢复cpsr.
     
         .long __irq_usr                              @  0  (USR_26 / USR_32)
         .long __irq_invalid                          @  1  (FIQ_26 / FIQ_32)

 


         .long __irq_invalid                          @  2  (IRQ_26 / IRQ_32)
         .long __irq_svc                              @  3  (SVC_26 / SVC_32)
         .long __irq_invalid                          @  4
         .long __irq_invalid                          @  5
         .long __irq_invalid                          @  6
         .long __irq_invalid                          @  7
         .long __irq_invalid                          @  8
         .long __irq_invalid                          @  9
         .long __irq_invalid                          @  a
         .long __irq_invalid                          @  b
         .long __irq_invalid                          @  c
         .long __irq_invalid                          @  d
         .long __irq_invalid                          @  e
         .long __irq_invalid                          @  f

从上面代码中的注释可以看出:

  • 将发生异常前的各个寄存器值保存在SP栈里,若是中断异常,则PC=PC-4,也就是CPU下个要运行的位置处;
  • 然后根据进入中断前的工作模式不同,程序下一步将跳转到_irq_usr 、或__irq_svc等位置;

执行完后,栈空间信息如下:

2.4 irq_handler

对于系统的外部设备来说,通常都是使用IRQ中断,所以我们只关注__irq_usr和__irq_svc,两者的区别是进入和退出中断时是否进行用户栈和内核栈之间的切换,还有进程调度和抢占的处理等。

__irq_usr、__irq_svc实现均在arch/arm/kernel/entry-armv.S:

__irq_usr:
        usr_entry        @ 栈中保护中断上下文 r0~r12、sp、lr、pc、cpsr
        kuser_cmpxchg_check
        irq_handler
        get_thread_info tsk
        mov     why, #0
        b       ret_to_user_from_irq
__irq_svc:
        svc_entry
        irq_handler

#ifdef CONFIG_PREEMPT                           @ 配置了抢占 
        ldr     r8, [tsk, #TI_PREEMPT]          @ get preempt count
        ldr     r0, [tsk, #TI_FLAGS]            @ get flags
        teq     r8, #0                          @ if preempt count != 0
        movne   r0, #0                          @ force flags to 0
        tst     r0, #_TIF_NEED_RESCHED
        blne    svc_preempt
#endif

        svc_exit r5, irq = 1                    @ return from exception

两个函数最终都会进入irq_handler这个宏:

        .macro  irq_handler
#ifdef CONFIG_GENERIC_IRQ_MULTI_HANDLER
        ldr     r1, =handle_arch_irq
        mov     r0, sp
        badr    lr, 9997f
        ldr     pc, [r1]
#else
        arch_irq_handler_default
#endif
9997:
        .endm

如果选择了CONFIG_GENERIC_MULTI_IRQ_HANDLER配置项,则意味着允许平台的代码可以动态设置irq处理程序,平台代码可以修改全局变量:handle_arch_irq,从而可以修改irq的处理程序。

这里我们先讨论默认的实现:arch_irq_handler_default。

2.5 arch_irq_handler_default

arch_irq_handler_default它位于arch/arm/include/asm/entry-macro-multi.S中:

/*
 * Interrupt handling.  Preserves r7, r8, r9
 */
        .macro  arch_irq_handler_default
        get_irqnr_preamble r6, lr
1:      get_irqnr_and_base r0, r2, r6, lr            @获取硬件中断号,r0=硬件中断号
        movne   r1, sp                               #r1=sp (栈中存放的寄存器上下文信息,对应结构体struct pt_regs) 
        @
        @ routine called with r0 = irq number, r1 = struct pt_regs *
        @
        badrne  lr, 1b
        bne     asm_do_IRQ                       

#ifdef CONFIG_SMP                                 @配置了多核
        /*
         * XXX
         *
         * this macro assumes that irqstat (r2) and base (r6) are
         * preserved from get_irqnr_and_base above
         */
        ALT_SMP(test_for_ipi r0, r2, r6, lr)
        ALT_UP_B(9997f)
        movne   r1, sp
        badrne  lr, 1b
        bne     do_IPI
#endif
9997:
        .endm

get_irqnr_preamble和get_irqnr_and_base两个宏由machine级的代码定义,目的就是从中断控制器中获得硬件中断号,紧接着就调用asm_do_IRQ,从这个函数开始,中断程序进入C代码中,传入的参数是硬件中断号和寄存器结构指针,这个函数在arch/arm/kernel/irq.c中实现:

/*
 * handle_IRQ handles all hardware IRQ's.  Decoded IRQs should
 * not come via this function.  Instead, they should provide their
 * own 'handler'.  Used by platform code implementing C-based 1st
 * level decoding.
 */
void handle_IRQ(unsigned int irq, struct pt_regs *regs)    // 参数1为硬件中断号,参数2寄存器结构指针,保存现场时的寄存器信息
{
        __handle_domain_irq(NULL, irq, false, regs);
}

/*
 * asm_do_IRQ is the interface to be used from assembly code.
 */
asmlinkage void __exception_irq_entry
asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
{
        handle_IRQ(irq, regs);
}

到这里,中断程序完成了从asm代码到C代码的传递,并且获得了引起中断的硬件中断号。

2.6 __handle_domain_irq

__handle_domain_irq定义在文件kernel/irq/irqdesc.c中:

/**
 * __handle_domain_irq - Invoke the handler for a HW irq belonging to a domain
 * @domain:     The domain where to perform the lookup
 * @hwirq:      The HW irq number to convert to a logical one
 * @lookup:     Whether to perform the domain lookup or not
 * @regs:       Register file coming from the low-level handling code
 *
 * Returns:     0 on success, or -EINVAL if conversion has failed
 */
int __handle_domain_irq(struct irq_domain *domain, unsigned int hwirq,
                        bool lookup, struct pt_regs *regs)
{
        struct pt_regs *old_regs = set_irq_regs(regs);
        unsigned int irq = hwirq;
        int ret = 0;

        irq_enter();                  //  更新一些系统统计信息,禁止内核抢占

#ifdef CONFIG_IRQ_DOMAIN
        if (lookup)
                irq = irq_find_mapping(domain, hwirq);  //根据中断域和硬件中断号获取IRQ编号
#endif

        /*
         * Some hardware gives randomly wrong interrupts.  Rather
         * than crashing, do something sensible.
         */
        if (unlikely(!irq || irq >= nr_irqs)) {    // 中断号超出范围
                ack_bad_irq(irq);
                ret = -EINVAL;
        } else {
                generic_handle_irq(irq);       // 重点时这里
        }

        irq_exit();               // 退出的时候会检查是否由软中断,如果有。则调用软中断
        set_irq_regs(old_regs);
        return ret;
}

在函数内部首先调用set_irq_regs保存被中断的线程的上下文,线程的上下文被保存在old_regs中,以便中断退出之后可以恢复。

irq_enter()和irq_exit()函数分别用于标记generic handler的进入和退出。

generic_handle_irq是通用逻辑层提供的API,通过该API,中断的控制被传递到了与体系结构无关的中断流控层:

int generic_handle_irq(unsigned int irq)
{
    struct irq_desc *desc = irq_to_desc(irq);   // 通过IRQ编号获取中断描述符
 
    if (!desc)
        return -EINVAL;
    generic_handle_irq_desc(irq, desc);
    return 0;
}

最终会进入该irq中断描述符的流控处理回调中:

static inline void generic_handle_irq_desc(unsigned int irq, struct irq_desc *desc)
{
    desc->handle_irq(irq, desc);
}

handle_irq使用chip结构中的函数清除、屏蔽或者重新使能中断,还要调用用户在action链表中注册的中断处理函数。

三、内核启动中断初始化

3.1 start_kernel

我们之前介绍到u-boot将linux内核加载到内存空间后,会跳转到内核执行,内核执行的起始代码实际上是start_kernel函数。

  • 首先,在setup_arch函数中,early_trap_init被调用,主要进行异常向量表和异常处理函数的的代码重定位工作;
  • 然后,start_kernel发出early_irq_init调用,early_irq_init属于与硬件和平台无关的通用逻辑层,它完成irq_desc结构的内存申请,为它们其中某些字段填充默认值,完成后调用体系相关的arch_early_irq_init函数完成进一步的初始化工作,不过ARM体系没有实现arch_early_irq_init;
  • 接着,start_kernel发出init_IRQ调用,它会直接调用所属板子machine_desc结构体中的init_irq回调。machine_desc通常在板子的特定代码中,使用MACHINE_START和MACHINE_END宏进行定义;
  • machine_desc->init_irq完成对中断控制器的初始化,为每个irq_desc结构安装合适的流控handler,为每个irq_desc结构安装irq_chip指针,使他指向正确的中断控制器所对应的irq_chip结构的实例,同时,如果该平台中的中断线有多路复用(多个中断公用一个irq中断线)的情况,还应该初始化irq_desc中相应的字段和标志,以便实现中断控制器的级联;

3.1 初始化异常向量表

early_trap_init位于arch/arm/kernel/traps.c:

void __init early_trap_init(void *vectors_base)
{
#ifndef CONFIG_CPU_V7M
        unsigned long vectors = (unsigned long)vectors_base;
        extern char __stubs_start[], __stubs_end[];
        extern char __vectors_start[], __vectors_end[];
        unsigned i;

        vectors_page = vectors_base;

        /*
         * Poison the vectors page with an undefined instruction.  This
         * instruction is chosen to be undefined for both ARM and Thumb
         * ISAs.  The Thumb version is an undefined instruction with a
         * branch back to the undefined instruction.
         */
        for (i = 0; i < PAGE_SIZE / sizeof(u32); i++)
                ((u32 *)vectors_base)[i] = 0xe7fddef1;

        /*
         * Copy the vectors, stubs and kuser helpers (in entry-armv.S)
         * into the vector page, mapped at 0xffff0000, and ensure these
         * are visible to the instruction stream.
         */
        memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start);
        memcpy((void *)vectors + 0x1000, __stubs_start, __stubs_end - __stubs_start);

        kuser_init(vectors_base);

        flush_icache_range(vectors, vectors + PAGE_SIZE * 2);
#else /* ifndef CONFIG_CPU_V7M */
        /*
         * on V7-M there is no need to copy the vector table to a dedicated
         * memory area. The address is configurable and so a table in the kernel
         * image can be used.
         */
#endif
}

以上两个memcpy会把__vectors_start开始的代码拷贝到0xffff0000处,也就是.vectors段的内容,即异常向量表的内容;

把__stubs_start开始的代码拷贝到0xffff0000+0x1000处,也就是.stubs段的内容,也就是处理跳转的部分;

这样,异常到来时,CPU就可以正确地跳转到相应异常向量入口并执行他们。

3.2  基于数组方式分配中断描述符

early_irq_init位于kernel/irq/irqdesc.c文件:

int __init early_irq_init(void)
{
        int count, i, node = first_online_node;
        struct irq_desc *desc;

        init_irq_default_affinity();

        printk(KERN_INFO "NR_IRQS: %dn", NR_IRQS);   // 输出IRQ数量

        desc = irq_desc;
        count = ARRAY_SIZE(irq_desc);                 // 中断描述符数组大小 

        for (i = 0; i < count; i++) {                 // 遍历每一个中断描述符、初始化一些成员
                desc[i].kstat_irqs = alloc_percpu(unsigned int);  // 分配per-CPU变量
                alloc_masks(&desc[i], node);
                raw_spin_lock_init(&desc[i].lock);                   // 自旋锁初始化
                lockdep_set_class(&desc[i].lock, &irq_desc_lock_class);
                mutex_init(&desc[i].request_mutex);                  // 互斥锁初始化
                desc_set_defaults(i, &desc[i], node, NULL, NULL);
        }
        return arch_early_irq_init();   // 抵用arch相关的初始化函数,这里为空
}

内核默认采用基于数组的方式来创建irq_desc数组(kernel/irq/irqdesc.c):

struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = {  // 分配irq_desc
    [0 ... NR_IRQS-1] = {
        .handle_irq    = handle_bad_irq,
        .depth        = 1,
        .lock        = __RAW_SPIN_LOCK_UNLOCKED(irq_desc->lock),
    }
};

还记的之前将linux内核移植到Mini2440开发板启动时的输出信息么:

NR_IRQS: 111
S3C2440: IRQ Support
irq: clearing pending status 00000002

可以发现NR_IRQS数量为111,定义在arch/arm/mach-s3c24xx/include/mach/irqs.h:

#define IRQ_S3C2416_I2S1        S3C2416_IRQ(7)        // 7 + 58 + 29 + 16 = 110

#if defined(CONFIG_CPU_S3C2416)     // 24xx系列 定义了这个
#define NR_IRQS (IRQ_S3C2416_I2S1 + 1)     // 111
#else
#define NR_IRQS (IRQ_S3C2443_AC97 + 1)
#endif

注意:

如果配置了CONFIG_SPARSE_IRQ,内核使用基数树(radix tree)来动态分配irq_desc结构:

int __init early_irq_init(void)
{
        int i, initcnt, node = first_online_node;
        struct irq_desc *desc;

        init_irq_default_affinity();

        /* Let arch update nr_irqs and return the nr of preallocated irqs */
        initcnt = arch_probe_nr_irqs();                     // 体系结构相关的代码来决定预先分配的中断描述符的个数
        printk(KERN_INFO "NR_IRQS: %d, nr_irqs: %d, preallocated irqs: %dn",
               NR_IRQS, nr_irqs, initcnt);

        if (WARN_ON(nr_irqs > IRQ_BITMAP_BITS))
                nr_irqs = IRQ_BITMAP_BITS;

        if (WARN_ON(initcnt > IRQ_BITMAP_BITS))
                initcnt = IRQ_BITMAP_BITS;

        if (initcnt > nr_irqs)       // initctn是需要在初始化的时候预分配的IRQ的个数,nr_irqs是当前系统中IR编号的最大值
                nr_irqs = initcnt;

        for (i = 0; i < initcnt; i++) {
                desc = alloc_desc(i, node, 0, NULL, NULL);   // 动态分配irq_desc
                set_bit(i, allocated_irqs);
                irq_insert_desc(i, desc);
        }
        return arch_early_irq_init();
}
View Code

early_irq_init用于动态申请irq_desc结构的内存空间,具体分配多少个中断描述符通过arch_probe_nr_irqs得到的:

int __init arch_probe_nr_irqs(void)
{
    nr_irqs = machine_desc->nr_irqs ? machine_desc->nr_irqs : NR_IRQS;
    return nr_irqs;
}

3.3 初始化中断描述符

init_IRQ在文件arch/arm/kernel/irq.c定义:

void __init init_IRQ(void)
{
        int ret;

        if (IS_ENABLED(CONFIG_OF) && !machine_desc->init_irq)
                irqchip_init();
        else
                machine_desc->init_irq();   // 执行

        if (IS_ENABLED(CONFIG_OF) && IS_ENABLED(CONFIG_CACHE_L2X0) &&
            (machine_desc->l2c_aux_mask || machine_desc->l2c_aux_val)) {
                if (!outer_cache.write_sec)
                        outer_cache.write_sec = machine_desc->l2c_write_sec;
                ret = l2x0_of_init(machine_desc->l2c_aux_val,
                                   machine_desc->l2c_aux_mask);
                if (ret && ret != -ENODEV)
                        pr_err("L2C: failed to init: %dn", ret);
        }

        uniphier_cache_init();
}

这里我们没有使用device tree,但是这边定义了machine_desc->init_irq,因此这里直接调用所属板子machine_desc结构体中的init_irq回调。

3.3.1 machine desc

machine_desc通常在板子的特定代码中,使用MACHINE_START和MACHINE_END宏进行定义。

比如在arch/arm/mach-s3c24xx/mach-smdk2440.c文件中定义:

MACHINE_START(S3C2440, "SMDK2440")
        /* Maintainer: Ben Dooks <ben-linux@fluff.org> */
        .atag_offset    = 0x100,

        .init_irq       = s3c2440_init_irq,
        .map_io         = smdk2440_map_io,
        .init_machine   = smdk2440_machine_init,
        .init_time      = smdk2440_init_time,
MACHINE_END

struct machine_desc ,这个结构体在内核移植中起到相当重要的作用,内核通过machine_desc结构体来控制系统体系架构相关部分的初始化。具体可以参考博客MACHINE_START与MACHINE_END

3.3.2 s3c2440_init_irq

由于machine_desc->init_irq初始化为s3c2440_init_irq,定位到文件drivers/irqchip/irq-s3c24xx.c:

void __init s3c2440_init_irq(void)
{
        pr_info("S3C2440: IRQ Supportn");

#ifdef CONFIG_FIQ
        init_FIQ(FIQ_START);
#endif

        s3c_intc[0] = s3c24xx_init_intc(NULL, &init_s3c2440base[0], NULL,    // 为主中断控制器分配一个s3c_irq_intc
                                        0x4a000000);
        if (IS_ERR(s3c_intc[0])) {
                pr_err("irq: could not create main interrupt controllern");
                return;
        }

        s3c24xx_init_intc(NULL, &init_eint[0], s3c_intc[0], 0x560000a4);      // 为外部子中断控制器分配一个s3c_irq_intc,其父为主中断控制器
        s3c_intc[1] = s3c24xx_init_intc(NULL, &init_s3c2440subint[0],         // 为内部子中断控制器分配一个s3c_irq_intc,其父为主中断控制器
                                        s3c_intc[0], 0x4a000018);
}

在前已经动态分配了中断描述符,这里将会调用s3c24xx_init_intc进行中断控制器的初始化工作,包括主中断控制器、内部子中断控制器、外部子中断控制器。

我们下面以这行代码为例讲解:

     s3c_intc[0] = s3c24xx_init_intc(NULL, &init_s3c2440base[0], NULL,
                                        0x4a000000);                // 源挂起寄存器  SRCPND

其中init_s3c2440base、init_s3c2440subint、init_eint这些数组是用来描述S3C2440芯片的60个中断源的,这些中断又分为三类:

  • 主中断32个;
  • 内部子中断15个;
  • 外部中断24个;
static struct s3c_irq_data init_s3c2440base[32] = {   // 对应S3C2440的32个主中断源
        { .type = S3C_IRQTYPE_EINT, }, /* EINT0 */
        { .type = S3C_IRQTYPE_EINT, }, /* EINT1 */
        { .type = S3C_IRQTYPE_EINT, }, /* EINT2 */
        { .type = S3C_IRQTYPE_EINT, }, /* EINT3 */
        { .type = S3C_IRQTYPE_LEVEL, }, /* EINT4to7 */
        { .type = S3C_IRQTYPE_LEVEL, }, /* EINT8to23 */
        { .type = S3C_IRQTYPE_LEVEL, }, /* CAM */
        { .type = S3C_IRQTYPE_EDGE, }, /* nBATT_FLT */
        { .type = S3C_IRQTYPE_EDGE, }, /* TICK */
        { .type = S3C_IRQTYPE_LEVEL, }, /* WDT/AC97 */
        { .type = S3C_IRQTYPE_EDGE, }, /* TIMER0 */
        { .type = S3C_IRQTYPE_EDGE, }, /* TIMER1 */
        { .type = S3C_IRQTYPE_EDGE, }, /* TIMER2 */
        { .type = S3C_IRQTYPE_EDGE, }, /* TIMER3 */
        { .type = S3C_IRQTYPE_EDGE, }, /* TIMER4 */
        { .type = S3C_IRQTYPE_LEVEL, }, /* UART2 */
        { .type = S3C_IRQTYPE_EDGE, }, /* LCD */
        { .type = S3C_IRQTYPE_EDGE, }, /* DMA0 */
        { .type = S3C_IRQTYPE_EDGE, }, /* DMA1 */
        { .type = S3C_IRQTYPE_EDGE, }, /* DMA2 */
        { .type = S3C_IRQTYPE_EDGE, }, /* DMA3 */
        { .type = S3C_IRQTYPE_EDGE, }, /* SDI */
        { .type = S3C_IRQTYPE_EDGE, }, /* SPI0 */
        { .type = S3C_IRQTYPE_LEVEL, }, /* UART1 */
        { .type = S3C_IRQTYPE_LEVEL, }, /* NFCON */
        { .type = S3C_IRQTYPE_EDGE, }, /* USBD */
        { .type = S3C_IRQTYPE_EDGE, }, /* USBH */
        { .type = S3C_IRQTYPE_EDGE, }, /* IIC */
        { .type = S3C_IRQTYPE_LEVEL, }, /* UART0 */
        { .type = S3C_IRQTYPE_EDGE, }, /* SPI1 */
        { .type = S3C_IRQTYPE_EDGE, }, /* RTC */
        { .type = S3C_IRQTYPE_LEVEL, }, /* ADCPARENT */
};

static struct s3c_irq_data init_s3c2440subint[32] = {    // S3C2440带有子中断的内部中断 一共15个中断源   对应6个主中断源
        { .type = S3C_IRQTYPE_LEVEL, .parent_irq = 28 }, /* UART0-RX */
        { .type = S3C_IRQTYPE_LEVEL, .parent_irq = 28 }, /* UART0-TX */
        { .type = S3C_IRQTYPE_LEVEL, .parent_irq = 28 }, /* UART0-ERR */
        { .type = S3C_IRQTYPE_LEVEL, .parent_irq = 23 }, /* UART1-RX */
        { .type = S3C_IRQTYPE_LEVEL, .parent_irq = 23 }, /* UART1-TX */
        { .type = S3C_IRQTYPE_LEVEL, .parent_irq = 23 }, /* UART1-ERR */
        { .type = S3C_IRQTYPE_LEVEL, .parent_irq = 15 }, /* UART2-RX */
        { .type = S3C_IRQTYPE_LEVEL, .parent_irq = 15 }, /* UART2-TX */
        { .type = S3C_IRQTYPE_LEVEL, .parent_irq = 15 }, /* UART2-ERR */
        { .type = S3C_IRQTYPE_EDGE, .parent_irq = 31 }, /* TC */
        { .type = S3C_IRQTYPE_EDGE, .parent_irq = 31 }, /* ADC */
        { .type = S3C_IRQTYPE_LEVEL, .parent_irq = 6 }, /* CAM_C */
        { .type = S3C_IRQTYPE_LEVEL, .parent_irq = 6 }, /* CAM_P */
        { .type = S3C_IRQTYPE_LEVEL, .parent_irq = 9 }, /* WDT */
        { .type = S3C_IRQTYPE_LEVEL, .parent_irq = 9 }, /* AC97 */
};
static struct s3c_irq_data __maybe_unused init_eint[32] = {     // 24个外部中断源 对应5个主中断源
        { .type = S3C_IRQTYPE_NONE, }, /* reserved */
        { .type = S3C_IRQTYPE_NONE, }, /* reserved */
        { .type = S3C_IRQTYPE_NONE, }, /* reserved */
        { .type = S3C_IRQTYPE_NONE, }, /* reserved */
        { .type = S3C_IRQTYPE_EINT, .parent_irq = 4 }, /* EINT4 */
        { .type = S3C_IRQTYPE_EINT, .parent_irq = 4 }, /* EINT5 */
        { .type = S3C_IRQTYPE_EINT, .parent_irq = 4 }, /* EINT6 */
        { .type = S3C_IRQTYPE_EINT, .parent_irq = 4 }, /* EINT7 */
        { .type = S3C_IRQTYPE_EINT, .parent_irq = 5 }, /* EINT8 */
        { .type = S3C_IRQTYPE_EINT, .parent_irq = 5 }, /* EINT9 */
        { .type = S3C_IRQTYPE_EINT, .parent_irq = 5 }, /* EINT10 */
        { .type = S3C_IRQTYPE_EINT, .parent_irq = 5 }, /* EINT11 */
        { .type = S3C_IRQTYPE_EINT, .parent_irq = 5 }, /* EINT12 */
        { .type = S3C_IRQTYPE_EINT, .parent_irq = 5 }, /* EINT13 */
        { .type = S3C_IRQTYPE_EINT, .parent_irq = 5 }, /* EINT14 */
        { .type = S3C_IRQTYPE_EINT, .parent_irq = 5 }, /* EINT15 */
        { .type = S3C_IRQTYPE_EINT, .parent_irq = 5 }, /* EINT16 */
        { .type = S3C_IRQTYPE_EINT, .parent_irq = 5 }, /* EINT17 */
        { .type = S3C_IRQTYPE_EINT, .parent_irq = 5 }, /* EINT18 */
        { .type = S3C_IRQTYPE_EINT, .parent_irq = 5 }, /* EINT19 */
        { .type = S3C_IRQTYPE_EINT, .parent_irq = 5 }, /* EINT20 */
        { .type = S3C_IRQTYPE_EINT, .parent_irq = 5 }, /* EINT21 */
        { .type = S3C_IRQTYPE_EINT, .parent_irq = 5 }, /* EINT22 */
        { .type = S3C_IRQTYPE_EINT, .parent_irq = 5 }, /* EINT23 */
};
View Code

我们顺便看一下sec_irq_data、s3c_irq_init的结构:

struct s3c_irq_data {        // 用于保存中断源信息
        unsigned int type;
        unsigned long offset;
        unsigned long parent_irq;           // 如果是子中断,这里保存子中断对应的主中断硬件编号

        /* data gets filled during init */
        struct s3c_irq_intc *intc;           // 存放中断控制器信息
        unsigned long sub_bits;              // 如果当前是主中断源,并且有多个子中断,那么子中断对应的硬件编号为置 1
        struct s3c_irq_intc *sub_intc;       // 子中断中断控制器信息
};

/*
 * Structure holding the controller data
 * @reg_pending         register holding pending irqs
 * @reg_intpnd          special register intpnd in main intc
 * @reg_mask            mask register
 * @domain              irq_domain of the controller
 * @parent              parent controller for ext and sub irqs
 * @irqs                irq-data, always s3c_irq_data[32]
 */
struct s3c_irq_intc {                                   // 保存中断控制器数据
        void __iomem            *reg_pending;           // 指定源挂起寄存器
        void __iomem            *reg_intpnd;            // 指定中断挂起寄存器
        void __iomem            *reg_mask;              // 指定中断屏蔽寄存器
        struct irq_domain       *domain;
        struct s3c_irq_intc     *parent;
        struct s3c_irq_data     *irqs;
};

/*
 * Array holding pointers to the global controller structs
 * [0] ... main_intc
 * [1] ... sub_intc
 * [2] ... main_intc2 on s3c2416
 */
static struct s3c_irq_intc *s3c_intc[3];

这里使用s3c_irq_init保存中断控制器的信息,比如寄存器信息、中断域、以及当前中断控制器所控制的中断源。

我们后面也将这个结构称为中断控制器,这个结构不同于irq_chip(irq_chip保存的主要是关中断、开中断、屏蔽中断等回调函数)。

实际上S3C2440物理只有一个中断控制器,这里软件抽象出来3个中断控制器:

  • 一个根中断控制器管理主中断源;
  • 两个子中断控制器,一个用于管理外部中断源、另一个管理内部子中断源;

为了方便理解,我们将这三个中断控制器以级联的方式展示:

3.4 s3c24xx_init_intc

我们继续看一下s3c24xx_init_intc函数,该函数位于drivers/irqchip/irq-s3c24xx.c:

static struct s3c_irq_intc * __init s3c24xx_init_intc(struct device_node *np,
                                       struct s3c_irq_data *irq_data, // 中断源信息数组 
                                       struct s3c_irq_intc *parent,   // 指定父中断控制器
                                       unsigned long address)         // 指定中断控制寄存器基地址
{
        struct s3c_irq_intc *intc;
        void __iomem *base = (void *)0xf6000000; /* static mapping */
        int irq_num;
        int irq_start;
        int ret;

        intc = kzalloc(sizeof(struct s3c_irq_intc), GFP_KERNEL);   // 动态分配src_irq_init结构
        if (!intc)
                return ERR_PTR(-ENOMEM);

        intc->irqs = irq_data;

        if (parent)
                intc->parent = parent;                                 // 设置父中断控制器  

        /* select the correct data for the controller.
         * Need to hard code the irq num start and offset
         * to preserve the static mapping for now    
* 根据实际物理地址,配置MMU映射后的中断相关寄存器地址
*/ switch (address) { case 0x4a000000: // SRCPND 源挂起寄存器 pr_debug("irq: found main intcn"); intc->reg_pending = base; // 指定SRCPND intc->reg_mask = base + 0x08; // 指定INTMASK intc->reg_intpnd = base + 0x10; // 指定INTPND irq_num = 32; // 中断源数量 irq_start = S3C2410_IRQ(0); // S3C2410_IRQ定义为((X) + S3C2410_CPUIRQ_OFFSET) 最终等于0+16=16 break; case 0x4a000018: // SUBSRCPND 次级源挂起寄存器 pr_debug("irq: found subintcn"); intc->reg_pending = base + 0x18; // 指定SUBSRCPND intc->reg_mask = base + 0x1c; // 指定INTSUBMSK irq_num = 29; // 中断源数量 为啥是29? irq_start = S3C2410_IRQSUB(0); // 0+ 16 + 58 = 74 break; case 0x4a000040: pr_debug("irq: found intc2n"); intc->reg_pending = base + 0x40; intc->reg_mask = base + 0x48; intc->reg_intpnd = base + 0x50; irq_num = 8; irq_start = S3C2416_IRQ(0); break; case 0x560000a4: // EINTMASK 外部中断屏蔽寄存器 pr_debug("irq: found eintcn"); base = (void *)0xfd000000; intc->reg_mask = base + 0xa4; // EINTMASK intc->reg_pending = base + 0xa8; // EINTPND irq_num = 24; // 中断源数量 irq_start = S3C2410_IRQ(32); // 32+16=48 break; default: pr_err("irq: unsupported controller addressn"); ret = -EINVAL; goto err; } /* now that all the data is complete, init the irq-domain */ s3c24xx_clear_intc(intc); // 清除中断挂起 intc->domain = irq_domain_add_legacy(np, irq_num, irq_start, // 初始化中断域,中断域存储了硬件中断号到IRQ编号的映射 irq_start为当前中断域对应的IRQ起始编号 0, &s3c24xx_irq_ops, intc); if (!intc->domain) { pr_err("irq: could not create irq-domainn"); ret = -EINVAL; goto err; } set_handle_irq(s3c24xx_handle_irq); // 设置irq总中断入口程序为s3c24xx_handle_irq return intc; err: kfree(intc); return ERR_PTR(ret); }

这个函数主要做了以下事情:

  • 为每一个中断控制器动态分配s3c_irq_intc;
  • 初始化s3c_irq_intc中断相关寄存器:源挂起寄存器、中断屏蔽寄存器、中断挂起寄存器;
  • 清除中断挂起状态;
  • 为每一个中断控制器分配irq_domain,并进行初始化:
    • 追加到全局链表irq_domain_list;
    • 执行domain->ops->map(domain, virq, hwirq),设置中断流控处理函数以及irq_chip;
    • 创建硬件中断号到IRQ编号的映射;
  • 设置中断统一入口程序为s3c24xx_handle_irq;

代码执行完之后s3c_intc结构大致如下:

四、s3c24xx_init_intc代码分析

由于s3c24xx_init_intc里面主要是s3c2440处理器中断相关的初始化代码,并且内容比较多,且比较重要,因此我们单独进行分析每一块内容。

4.1 s3c24xx_clear_intc

 其中s3c24xx_clear_intc通过向INTPND或者EINTPND相应位写1来清除中断的挂起状态:

static void s3c24xx_clear_intc(struct s3c_irq_intc *intc)
{
        void __iomem *reg_source;
        unsigned long pend;
        unsigned long last;
        int i;

        /* if intpnd is set, read the next pending irq from there */
        reg_source = intc->reg_intpnd ? intc->reg_intpnd : intc->reg_pending;  // 中断挂起期存器?中断挂起期存器:源挂起寄存器

        last = 0;
        for (i = 0; i < 4; i++) {
                pend = readl_relaxed(reg_source);    // 读取寄存器的值

                if (pend == 0 || pend == last)
                        break;

                writel_relaxed(pend, intc->reg_pending);   // 写1、清除挂起状态
                if (intc->reg_intpnd)
                        writel_relaxed(pend, intc->reg_intpnd);

                pr_info("irq: clearing pending status %08xn", (int)pend);
                last = pend;
        }
}

4.2 创建并初始化中断域

intc->domain = irq_domain_add_legacy(np, irq_num, irq_start,
                                             0, &s3c24xx_irq_ops,
                                             intc);

其中参数s3c24xx_irq_ops类型为irq_domain_ops,这个我们在上一篇博客中介绍过:

static const struct irq_domain_ops s3c24xx_irq_ops = {
        .map = s3c24xx_irq_map,          // 比较重要  s3c24xx_irq_map函数主要工作就是设置每个中断的流控处理函数   单独介绍
        .xlate = irq_domain_xlate_twocell,
};

irq_domain_add_legacy函数在kernel/irq/irqdomain.c中定义,该函数用于分配struct irq_domain,这里通过host_data字段绑定了中断控制器s3c_irq_intc和中断域的关系,最后将创建好的struct irq_domain添加到全局链表irq_domain_list:

/**
 * irq_domain_add_legacy() - Allocate and register a legacy revmap irq_domain.
 * @of_node: pointer to interrupt controller's device tree node.
 * @size: total number of irqs in legacy mapping
 * @first_irq: first number of irq block assigned to the domain
 * @first_hwirq: first hwirq number to use for the translation. Should normally
 *               be '0', but a positive integer can be used if the effective
 *               hwirqs numbering does not begin at zero.
 * @ops: map/unmap domain callbacks
 * @host_data: Controller private data pointer
 *
 * Note: the map() callback will be called before this function returns
 * for all legacy interrupts except 0 (which is always the invalid irq for
 * a legacy controller).
 */
struct irq_domain *irq_domain_add_legacy(struct device_node *of_node,
                                         unsigned int size,            //中断数量 以主中断为例 32
                                         unsigned int first_irq,       //起始IRQ编号  16
                                         irq_hw_number_t first_hwirq,  //起始硬件中断号   0
                                         const struct irq_domain_ops *ops,
                                         void *host_data)              // 可以传递与中断域相关的任何主机数据,用于设置中断域的host_data私有字段 
{
        struct irq_domain *domain;

        domain = __irq_domain_add(of_node_to_fwnode(of_node), first_hwirq + size,   // 创建irq_domain
                                  first_hwirq + size, 0, ops, host_data);
        if (domain)
                irq_domain_associate_many(domain, first_irq, first_hwirq, size);    // 创建映射

        return domain;
}

需要注意的是irq_domain_add_legacy并不是标准的注册irq domain的接口函数,标准函数有irq_domain_add_linear、irq_domain_add_tree、irq_domain_add_nomap,当然这些函数内部都是调用的__irq_domain_add。

4.2.1 创建中断域

__irq_domain_add用于创建一个通用中断域结构体并返回其指针。定义如下:

/**
 * __irq_domain_add() - Allocate a new irq_domain data structure
 * @fwnode: firmware node for the interrupt controller
 * @size: Size of linear map; 0 for radix mapping only
 * @hwirq_max: Maximum number of interrupts supported by controller
 * @direct_max: Maximum value of direct maps; Use ~0 for no limit; 0 for no
 *              direct mapping
 * @ops: domain callbacks
 * @host_data: Controller private data pointer
 *
 * Allocates and initialize and irq_domain structure.
 * Returns pointer to IRQ domain, or NULL on failure.
 */
struct irq_domain *__irq_domain_add(struct fwnode_handle *fwnode, int size,     // size为线性映射表大小 0+32=32
                                    irq_hw_number_t hwirq_max, int direct_max,  // hwiq_max硬件中断号最大值 0+32=32
                                    const struct irq_domain_ops *ops,           // 该结构存放大量函数指针
                                    void *host_data)                             //中断控制器    
{
        struct device_node *of_node = to_of_node(fwnode);
        struct irqchip_fwid *fwid;
        struct irq_domain *domain;

        static atomic_t unknown_domains;

        domain = kzalloc_node(sizeof(*domain) + (sizeof(unsigned int) * size), // 动态分配struct irq_domain、最后4*size是线性lookup table表大小
                              GFP_KERNEL, of_node_to_nid(of_node));
        if (WARN_ON(!domain))
                return NULL;

        if (fwnode && is_fwnode_irqchip(fwnode)) {
                fwid = container_of(fwnode, struct irqchip_fwid, fwnode);

                switch (fwid->type) {
                case IRQCHIP_FWNODE_NAMED:
                case IRQCHIP_FWNODE_NAMED_ID:
                        domain->name = kstrdup(fwid->name, GFP_KERNEL);
                        if (!domain->name) {
                                kfree(domain);
                                return NULL;
                        }
                        domain->flags |= IRQ_DOMAIN_NAME_ALLOCATED;
                        break;
                default:
                        domain->fwnode = fwnode;
                        domain->name = fwid->name;
                        break;
                }
        } else if (of_node) {
                char *name;

                /*
                 * DT paths contain '/', which debugfs is legitimately
                 * unhappy about. Replace them with ':', which does
                 * the trick and is not as offensive as ''...
                 */
                name = kasprintf(GFP_KERNEL, "%pOF", of_node);
                if (!name) {
                        kfree(domain);
                        return NULL;
                }

                strreplace(name, '/', ':');

                domain->name = name;
                domain->fwnode = fwnode;
                domain->flags |= IRQ_DOMAIN_NAME_ALLOCATED;
        }

        if (!domain->name) {
                if (fwnode)
                        pr_err("Invalid fwnode type for irqdomainn");
                domain->name = kasprintf(GFP_KERNEL, "unknown-%d",
                                         atomic_inc_return(&unknown_domains));
                if (!domain->name) {
                        kfree(domain);
                        return NULL;
                }
                domain->flags |= IRQ_DOMAIN_NAME_ALLOCATED;
        }

        of_node_get(of_node);
        /* Fill structure */
        INIT_RADIX_TREE(&domain->revmap_tree, GFP_KERNEL);
        mutex_init(&domain->revmap_tree_mutex);
        domain->ops = ops;                     // 设置操作集,用于创建映射、翻译等 
        domain->host_data = host_data;         // 绑定中断控制器
        domain->hwirq_max = hwirq_max;        // 设置硬件中断号最大值
        domain->revmap_size = size;           // 设置查找表lookup table大小 
        domain->revmap_direct_max_irq = direct_max;
        irq_domain_check_hierarchy(domain);   // 检查中断域是否级联

        mutex_lock(&irq_domain_mutex);
        debugfs_add_domain_dir(domain);
        list_add(&domain->link, &irq_domain_list);   // 添加到连表,以便发生中断时能能够找到对应的中断域
        mutex_unlock(&irq_domain_mutex);

        pr_debug("Added domain %sn", domain->name);
        return domain;
}

该函数接受 6 个参数:

  • fwnode:表示与中断域相关联的固件节点(Firmware Node);
  • size:新建中断域所需的中断号数量;
  • hwirq_max:硬件中断号的最大值,一般等于size-1;
  • direct_max:直接中断号的最大值,通常设置为0;
  • ops:一个指向 irq_domain_ops 结构体的指针,该结构体定义了中断域的行为。
  • host_data:一个指针,可以传递与中断域相关的任何主机数据,用于设置中断域的host_data私有字段;;

调用该函数之后,如果成功,将返回一个指向新创建中断域的指针。

该函数适用于需要更复杂的中断管理、或支持不同种类的中断控制器的场景。比如,一个 SoC 中可以有多个中断控制器,每个中断控制器都需要一个中断域管理其所管辖的硬件设备的中断号,这时就可以使用 __irq_domain_add创建一个中断域并将其与对应的中断控制器关联。

4.2.2 创建中断映射

创建中断域以后,需要向中断域添加硬件中断号到IRQ编号的映射,这里调用了irq_domain_associate_many函数来实现:

void irq_domain_associate_many(struct irq_domain *domain, unsigned int irq_base,
                               irq_hw_number_t hwirq_base, int count)
{
        struct device_node *of_node;
        int i;

        of_node = irq_domain_get_of_node(domain);
        pr_debug("%s(%s, irqbase=%i, hwbase=%i, count=%i)n", __func__,
                of_node_full_name(of_node), irq_base, (int)hwirq_base, count);

        // 遍历中断,在domain线性表,其实就是lookup table,建立硬件中断号到IRQ编号的映射,硬件中断号作为index,通过查表可以获取对应的IRQ编号。
        for (i = 0; i < count; i++) {
                irq_domain_associate(domain, irq_base + i, hwirq_base + i);
        }
}

第一个参数为中断域,第二个参数为起始IRQ编号,第三个参为起始硬件中断号,最后一个参数为中断数。

内部调用了irq_domain_associate函数:

int irq_domain_associate(struct irq_domain *domain, unsigned int virq,
                         irq_hw_number_t hwirq)
{
        // 根据IRQ编号获取中断数据
        struct irq_data *irq_data = irq_get_irq_data(virq);
        int ret;

        if (WARN(hwirq >= domain->hwirq_max,
                 "error: hwirq 0x%x is too large for %sn", (int)hwirq, domain->name))
                return -EINVAL;
        if (WARN(!irq_data, "error: virq%i is not allocated", virq))
                return -EINVAL;
        if (WARN(irq_data->domain, "error: virq%i is already associated", virq))
                return -EINVAL;

        mutex_lock(&irq_domain_mutex);
        irq_data->hwirq = hwirq;                     // 设置硬件中断号
        irq_data->domain = domain;                   // 设置中断域  
        if (domain->ops->map) {
                // 调用irq domain的map callback函数,即s3c24xx_irq_map
                ret = domain->ops->map(domain, virq, hwirq);
                if (ret != 0) {
                        /*
                         * If map() returns -EPERM, this interrupt is protected
                         * by the firmware or some other service and shall not
                         * be mapped. Don't bother telling the user about it.
                         */
                        if (ret != -EPERM) {
                                pr_info("%s didn't like hwirq-0x%lx to VIRQ%i mapping (rc=%d)n",
                                       domain->name, hwirq, virq, ret);
                        }
                        irq_data->domain = NULL;
                        irq_data->hwirq = 0;
                        mutex_unlock(&irq_domain_mutex);
                        return ret;
                }

                /* If not already assigned, give the domain the chip's name */
                if (!domain->name && irq_data->chip)
                        domain->name = irq_data->chip->name;
        }

        domain->mapcount++;
        irq_domain_set_mapping(domain, hwirq, irq_data);  // 把硬件中断号到IRQ编号的映射添加到中断域的线性表linear_revmap
        mutex_unlock(&irq_domain_mutex);

        irq_clear_status_flags(virq, IRQ_NOREQUEST);   // 该中断已经可以申请了,因此clear相关flag

        return 0;
}

第一个参数为中断域,第二个参数为IRQ编号,第三个参为硬件中断号。重点流程:

  • 在该函数内部回调了domain->ops->map(domain, virq, hwirq)。对于给定的映射,该函数只被调用一次,它通常使用irq_set_chip_and_handler 将IRQ编号与指定的处理程序进行映射,这样调用generic_handle_irq将触发正确的处理程序;
  • 此外该函数调用irq_domain_set_mapping把硬件中断号到IRQ编号的映射添加到中断域的线性表linear_revmap中;

irq_domain_set_mapping定义如下:

static void irq_domain_set_mapping(struct irq_domain *domain,
                                   irq_hw_number_t hwirq,
                                   struct irq_data *irq_data)
{
        if (hwirq < domain->revmap_size) {     // 硬件中断号 < 线性lookup table的大小
                domain->linear_revmap[hwirq] = irq_data->irq;     // 填写线性映射lookup table的数据
        } else {
                mutex_lock(&domain->revmap_tree_mutex);
                radix_tree_insert(&domain->revmap_tree, hwirq, irq_data);   // 向radix tree插入一个node
                mutex_unlock(&domain->revmap_tree_mutex);
        }
}

第一个参数为中断域,第二个参为硬件中断号,第三个参数为该硬件中断对应的irq_data。

值得一提的是,linux内核提供了为中断域某个硬件中断号动态分配IRQ编号的函数irq_create_mapping,这里之所以没有使用的主要原因是由于在旧的linux kernel中,各个外设的IRQ是固定的(比如S3C2440,有大量针对IRQ编号的宏定义),也就是说,硬件中断号和IRQ编号的关系是固定的,为了兼容低版本,因此不能使用该方法。

unsigned int irq_create_mapping(struct irq_domain *domain, irq_hw_number_t hwirq);

irq_create_mapping函数输入参数是中断域和硬件中断号,返回IRQ编号。该函数:

  • 首先判断hwirq映射是否存在,如果存在,直接返回;
  • 调用irq_domain_alloc_descs为硬件中断动态申请中断描述符,将其与hwirq关联,同时为硬件中断号申请一个全局IRQ编号,并返回IRQ编号;
  • 然后调用irq_domain_associate把硬件中断号到IRQ编号的映射添加到中断域;同时调用domain->ops->map回调,以便驱动程序可以执行任何所需的硬件设置;

函数调用流程:

irq_create_mapping(domain, hwirq) 
     virq  = irq_find_mapping(domain, hwirq)    // 判断hwirq映射是否存在,如果存在,直接返回
     irq_domain_alloc_descs(-1, 1, hwirq, of_node_to_nid(of_node), NULL) // 为硬件中断动态申请中断描述符
     irq_domain_associate(domain, virq, hwirq)   // 为中断域添加hwirq到virq的映射
         domain->ops->map(domain, virq, hwirq)   // 回调domain操作集map函数  
         irq_domain_set_mapping(domain, hwirq, irq_data); // 把硬件中断号到IRQ编号的映射添加到中断域的线性表linear_revmap  
4.2.3 查找映射

中断处理程序需要根据硬件中断号查找IRQ编号,内核提供了函数irq_find_mapping,输入参数是中断域和硬件中断号,返回IRQ编号。函数定义在kernel/irq/irqdomain.c:

/**
 * irq_find_mapping() - Find a linux irq from a hw irq number.
 * @domain: domain owning this hardware interrupt
 * @hwirq: hardware irq number in that domain space
 */
unsigned int irq_find_mapping(struct irq_domain *domain,
                              irq_hw_number_t hwirq)
{
        struct irq_data *data;

        /* Look for default domain if nececssary */
        if (domain == NULL)
                domain = irq_default_domain;
        if (domain == NULL)
                return 0;

        if (hwirq < domain->revmap_direct_max_irq) {
                data = irq_domain_get_irq_data(domain, hwirq);  
                if (data && data->hwirq == hwirq)
                        return hwirq;
        }

        /* Check if the hwirq is in the linear revmap. */
        if (hwirq < domain->revmap_size)
                return domain->linear_revmap[hwirq];  // 线性映射表 数组索引为硬件中断号

        rcu_read_lock();
        data = radix_tree_lookup(&domain->revmap_tree, hwirq);
        rcu_read_unlock();
        return data ? data->irq : 0;
}

收到中断时,应使用irq_find_mapping函数根据hwirq号查找IRQ编号。当然,必须在返回之前存在该映射,IRQ编号始终与irq_desc数据结构绑定。

4.3 设置中断流控处理函数

domain->ops指向了s3c24xx_irq_ops,其成员map指向了s3c24xx_irq_map,也是定义在drivers/irqchip/irq-s3c24xx.c:

static int s3c24xx_irq_map(struct irq_domain *h, unsigned int virq,    
                                                        irq_hw_number_t hw)  
{
        struct s3c_irq_intc *intc = h->host_data;         // 获取中断域绑定的中断控制器
        struct s3c_irq_data *irq_data = &intc->irqs[hw];  // 根据硬件中断号获取中断源信息
        struct s3c_irq_intc *parent_intc;
        struct s3c_irq_data *parent_irq_data;
        unsigned int irqno;

        /* attach controller pointer to irq_data */
        irq_data->intc = intc;
        irq_data->offset = hw;                   // 硬件中断在中断相关寄存器的偏移位

        parent_intc = intc->parent;              // 如果有父中断控制器

        /* set handler and flags */
        switch (irq_data->type) {    // 根据中断源类型设置中断处理函数
        case S3C_IRQTYPE_NONE:
                return 0;
        case S3C_IRQTYPE_EINT:                // 外部中断
                /* On the S3C2412, the EINT0to3 have a parent irq
                 * but need the s3c_irq_eint0t4 chip
                 */
                if (parent_intc && (!soc_is_s3c2412() || hw >= 4))             // 对于子中断控制器,走这里
                        irq_set_chip_and_handler(virq, &s3c_irqext_chip,
                                                 handle_edge_irq);
                else                                          
                        irq_set_chip_and_handler(virq, &s3c_irq_eint0t4,
                                                 handle_edge_irq);
                break;
        case S3C_IRQTYPE_EDGE:            // 双边沿触发中断 
                if (parent_intc || intc->reg_pending == S3C2416_SRCPND2)   // 对于子中断控制器,走这里
                        irq_set_chip_and_handler(virq, &s3c_irq_level_chip,
                                                 handle_edge_irq);
                else
                        irq_set_chip_and_handler(virq, &s3c_irq_chip,
                                                 handle_edge_irq);
                break;
        case S3C_IRQTYPE_LEVEL:            // 电平触发  
                if (parent_intc)                    // 对于子中断控制器走这里
                        irq_set_chip_and_handler(virq, &s3c_irq_level_chip,
                                                 handle_level_irq);
                else
                        irq_set_chip_and_handler(virq, &s3c_irq_chip,
                                                 handle_level_irq);
                break;
        default:
                pr_err("irq-s3c24xx: unsupported irqtype %dn", irq_data->type);
                return -EINVAL;
        }
        irq_set_chip_data(virq, irq_data);

        if (parent_intc && irq_data->type != S3C_IRQTYPE_NONE) {  // 有父级中断控制器,即子中断控制器,需要修改主中断源的中断处理函数为s3c_irq_demux
                if (irq_data->parent_irq > 31) {
                        pr_err("irq-s3c24xx: parent irq %lu is out of rangen",
                               irq_data->parent_irq);
                        return -EINVAL;
                }

                parent_irq_data = &parent_intc->irqs[irq_data->parent_irq];  // 获取子中断对应的主中断源数据
                parent_irq_data->sub_intc = intc;                  // 设置子中断中断控制器
                parent_irq_data->sub_bits |= (1UL << hw);

                /* attach the demuxer to the parent irq */
                irqno = irq_find_mapping(parent_intc->domain,   // 根据主中断硬件中断号和中断域获取主中断编号
                                         irq_data->parent_irq);
                if (!irqno) {
                        pr_err("irq-s3c24xx: could not find mapping for parent irq %lun",
                               irq_data->parent_irq);
                        return -EINVAL;
                }
                irq_set_chained_handler(irqno, s3c_irq_demux);   // 设置子中断的主中断源的中断处理函数
        }

        return 0;
}

第一个参数为中断域、第二个参数为IRQ编号,第三个为硬件中断号。

这里做了以下事情:

(1) 通过中断域irq_domain获取到其关联的中断控制器s3c_irq_intc;

(2) 根据硬件中断号hw从中断控制器的irqs获取中断源信息;

(3) 然后调用irq_set_chip_and_handler,根据IRQ编号(虚拟中断号)找到irq_desc,然后根据不同的中断触发类型,设置其成员handle_irq回调和irq_chip指针;

(4) 如果是子中断,还需要调用irq_set_chained_handler设置主中断所对应的irq_desc->handle_irq回调函数;

  • 主要是因为子中断控制器在把多个irq汇集起来后,输出端连接到根中断控制器的其中一个irq中断线输入脚,这意味着,每个子中断控制器的中断发生时,CPU一开始只会得到根中断控制器的irq编号,然后进入该irq编号对应的irq_desc->handle_irq回调,该回调我们不能使用流控层定义好的几个流控函数,而是要自己实现一个函数,比如这里的s3c_irq_demux函数:
  • s3c_irq_demux函数负责从子中断控制器中获得irq的中断源,并计算出对应新的irq编号,然后调用新irq所对应的irq_desc->handle_irq回调,这个回调使用流控层的标准实现;
4.3.1 s3c_irq_demux

s3c_irq_demux为S3C2440主中断源对应的中断处理函数,定义在drivers/irqchip/irq-s3c24xx.c,由于一个主中断源一般对应多个子中断;

因此这里首先会获取到子中断控制器,然后读取中断挂起寄存器,根据寄存器请求位,找到子中断IRQ编号,然后调用generic_handle_irq。

static void s3c_irq_demux(struct irq_desc *desc)
{
        // 根据中断描述符获取中断控制器irq_chip 这个是主中断源对应的中断控制器
        struct irq_chip *chip = irq_desc_get_chip(desc);     // desc->irq_data.chip
        struct s3c_irq_data *irq_data = irq_desc_get_chip_data(desc);
        // 获取中断控制器s3c_irq_intc
        struct s3c_irq_intc *intc = irq_data->intc;
        // 获取子中断控制器
        struct s3c_irq_intc *sub_intc = irq_data->sub_intc;
        unsigned int n, offset, irq;
        unsigned long src, msk;

        /* we're using individual domains for the non-dt case
         * and one big domain for the dt case where the subintc
         * starts at hwirq number 32.
         */
        offset = irq_domain_get_of_node(intc->domain) ? 32 : 0;

        chained_irq_enter(chip, desc);
 
        src = readl_relaxed(sub_intc->reg_pending);   // 读取中断挂起寄存器
        msk = readl_relaxed(sub_intc->reg_mask);      // 读取中断屏蔽寄存器

        src &= ~msk;
        src &= irq_data->sub_bits;

        while (src) {
                n = __ffs(src);
                src &= ~(1 << n);
                // 获取子中断控制器sub_intc硬件中断offset + n对应的IRQ编号
                irq = irq_find_mapping(sub_intc->domain, offset + n);
                // 执行子中断中断处理函数
                generic_handle_irq(irq);
        }

        chained_irq_exit(chip, desc);
}
4.3.2 struct irq_chip

s3c24xx_irq_map函数根据中断触发类型,为每个中断描述符绑定不同的struct irq_chip;比如:

  • 对于中断触发类型为S3C_IRQTYPE_EINT的子中断,其对应的irq_desc->irq_data.chip设置为s3c_irqext_chip;覆盖的中断范围为外部中断EINT4~EINT23;
  • 对于中断触发类型为S3C_IRQTYPE_EINT的主中断,其对应的irq_desc->irq_data.chip设置为s3c_irq_eint0t4;覆盖的中断范围为外部中断EINT0~EINT3;
  • 对于中断触发类型为S3C_IRQTYPE_LEVEL的子中断,其对应的irq_desc->irq_data.chip设置为s3c_irq_level_chip;覆盖的中断范围为带有子中断的内部中断;
  • 对于中断触发类型为S3C_IRQTYPE_LEVEL的主中断,其对应的irq_desc->irq_data.chip设置为s3c_irq_chip;覆盖的中断范围为没有子中断的内部中断;
  • 对于中断触发类型为S3C_IRQTYPE_EDGE的子中断,其对应的irq_desc->irq_data.chip设置为s3c_irq_level_chip;覆盖的中断范围为带有子中断的内部中断;
  • 对于中断触发类型为S3C_IRQTYPE_EDGE的主中断,其对应的irq_desc->irq_data.chip设置为s3c_irq_chip;覆盖的中断范围为没有子中断的内部中断;

s3c_irqext_chip、s3c_irq_eint0t4、s3c_irq_level_chip、s3c_irq_chip均是struct irq_chip类型:

static struct irq_chip s3c_irq_chip = {
        .name           = "s3c",
        .irq_ack        = s3c_irq_ack,
        .irq_mask       = s3c_irq_mask,
        .irq_unmask     = s3c_irq_unmask,
        .irq_set_type   = s3c_irq_type,
        .irq_set_wake   = s3c_irq_wake
};

static struct irq_chip s3c_irq_level_chip = {
        .name           = "s3c-level",
        .irq_mask       = s3c_irq_mask,
        .irq_unmask     = s3c_irq_unmask,
        .irq_ack        = s3c_irq_ack,
        .irq_set_type   = s3c_irq_type,
};

static struct irq_chip s3c_irqext_chip = {
        .name           = "s3c-ext",
        .irq_mask       = s3c_irq_mask,
        .irq_unmask     = s3c_irq_unmask,
        .irq_ack        = s3c_irq_ack,
        .irq_set_type   = s3c_irqext_type,
        .irq_set_wake   = s3c_irqext_wake
};

static struct irq_chip s3c_irq_eint0t4 = {
        .name           = "s3c-ext0",
        .irq_ack        = s3c_irq_ack,
        .irq_mask       = s3c_irq_mask,
        .irq_unmask     = s3c_irq_unmask,
        .irq_set_wake   = s3c_irq_wake,
        .irq_set_type   = s3c_irqext0_type,
};

struct irq_chip对中断控制器的接口抽象;其中的成员大多用于操作底层硬件,比如设置寄存器以屏蔽中断,使能中断,清除中断等。

这里以s3c_irq_mask为例,用于屏蔽与s3c_irq_chip中断控制器关联的中断;

  • 对于主中断:其本质就是根据硬件中断号设置主中断控制器中断屏蔽寄存器对应位为1;
  • 对于子中断:其本质就是根据硬件中断号设置子中断控制器中断屏蔽寄存器对应位为1;

代码如下:

static void s3c_irq_mask(struct irq_data *data)  // data为当前中断的数据
{
        struct s3c_irq_data *irq_data = irq_data_get_irq_chip_data(data);
        struct s3c_irq_intc *intc = irq_data->intc;
        struct s3c_irq_intc *parent_intc = intc->parent;  // 父中断控制器
        struct s3c_irq_data *parent_data;
        unsigned long mask;
        unsigned int irqno;

        mask = readl_relaxed(intc->reg_mask);
        mask |= (1UL << irq_data->offset);
        writel_relaxed(mask, intc->reg_mask);

        if (parent_intc) {   // 如果是子中断,判断该子中断所属的主中断是不是还有其它子中断在使用,如果没有的话,也会把该主中断也给屏蔽了
                parent_data = &parent_intc->irqs[irq_data->parent_irq];

                /* check to see if we need to mask the parent IRQ
                 * The parent_irq is always in main_intc, so the hwirq
                 * for find_mapping does not need an offset in any case.
                 */
                if ((mask & parent_data->sub_bits) == parent_data->sub_bits) {
                        irqno = irq_find_mapping(parent_intc->domain,
                                         irq_data->parent_irq);
                        s3c_irq_mask(irq_get_irq_data(irqno));
                }
        }
}
4.3.3 irq_set_chip_and_handler

irq_set_chip_and_handler定义在include/linux/irq.h文件中,用于根据IRQ编号设置中断流控处理函数、以及中断控制器(struct irq_chip);

static inline void irq_set_chip_and_handler(unsigned int irq, struct irq_chip *chip,
                                            irq_flow_handler_t handle)
{
        irq_set_chip_and_handler_name(irq, chip, handle, NULL);
}

该函数接收IRQ编号、中断控制器(struct irq_chip)以及中断流控处理函数。

irq_set_chip_and_handler_name定义在 kernel/irq/chip.c:

void irq_set_chip_and_handler_name(unsigned int irq, struct irq_chip *chip,
                              irq_flow_handler_t handle, const char *name)
{
        irq_set_chip(irq, chip);
        __irq_set_handler(irq, handle, 0, name);
}

首先调用irq_set_chip绑定irq到chip;然后调用__irq_set_handler初始化中断描述符的handle_irq回调函数:

void
__irq_set_handler(unsigned int irq, irq_flow_handler_t handle, int is_chained,
                  const char *name)
{
        unsigned long flags;
        struct irq_desc *desc = irq_get_desc_buslock(irq, &flags, 0);

        if (!desc)
                return;

        __irq_do_set_handler(desc, handle, is_chained, name);
        irq_put_desc_busunlock(desc, flags);
}

4.4  设置中断统一入口函数handle_arch_irq

s3c24xx_clear_intc函数最后调用set_handle_irq(s3c24xx_handle_irq)设置中断统一入口函数为s3c24xx_handle_irq,位于kernel/irq/handle.c文件

#ifdef CONFIG_GENERIC_IRQ_MULTI_HANDLER
int __init set_handle_irq(void (*handle_irq)(struct pt_regs *))
{
        if (handle_arch_irq)
                return -EBUSY;

        handle_arch_irq = handle_irq;
        return 0;
}

可以看到如果我们定义了CONFIG_GENERIC_IRQ_MULTI_HANDLER,将会设置handle_arch_irq为s3c24xx_handle_irq,有没有发现这里很熟悉,这是因为在之前我们介绍irq_hanlder中就是调用的handle_arch_irq函数进行中断处理的。

.macro  irq_handler
#ifdef CONFIG_GENERIC_IRQ_MULTI_HANDLER
        ldr     r1, =handle_arch_irq
        mov     r0, sp
        badr    lr, 9997f
        ldr     pc, [r1]
#else
        arch_irq_handler_default
#endif
9997:
        .endm

之前我们分析的是中断处理的默认实现:arch_irq_handler_default。

既然看到这里了,那我顺便也看一下s3c24xx_handle_irq的实现,位于drivers/irqchip/irq-s3c24xx.c:

asmlinkage void __exception_irq_entry s3c24xx_handle_irq(struct pt_regs *regs)
{
        do {
                if (likely(s3c_intc[0]))
                        if (s3c24xx_handle_intc(s3c_intc[0], regs, 0))   // 执行这里
                                continue;

                if (s3c_intc[2])
                        if (s3c24xx_handle_intc(s3c_intc[2], regs, 64))
                                continue;

                break;
        } while (1);
}

传入的struct pt_regs *regs参数其实就是之前保存现场的那些寄存器:

static inline int s3c24xx_handle_intc(struct s3c_irq_intc *intc,              
                                      struct pt_regs *regs, int intc_offset)   // 硬件中断号起始偏移
{
        int pnd;
        int offset;

        pnd = readl_relaxed(intc->reg_intpnd);  // 读取中断挂起寄存器,从而知道触发了哪些中断源
        if (!pnd)
                return false;

        /* non-dt machines use individual domains */
        if (!irq_domain_get_of_node(intc->domain))
                intc_offset = 0;

        /* We have a problem that the INTOFFSET register does not always
         * show one interrupt. Occasionally we get two interrupts through
         * the prioritiser, and this causes the INTOFFSET register to show
         * what looks like the logical-or of the two interrupt numbers.
         *
         * Thanks to Klaus, Shannon, et al for helping to debug this problem
         */
        offset = readl_relaxed(intc->reg_intpnd + 4);    // 读取中断偏移寄存器 获取硬件中断号

        /* Find the bit manually, when the offset is wrong.
         * The pending register only ever contains the one bit of the next
         * interrupt to handle.
         */
        if (!(pnd & (1 << offset)))                    // 再次校验发生了中断
                offset =  __ffs(pnd);

        handle_domain_irq(intc->domain, intc_offset + offset, regs);
        return true;
}

s3c24xx_handle_intc先读取中断挂起寄存器,判断有没有中断发生,然后读取中断偏移寄存器,根据寄存器请求位,找到的中断号,然后调用handle_domain_irq。

handle_domain_irq函数定义在include/linux/irqdesc.h文件:

static inline int handle_domain_irq(struct irq_domain *domain,
                                    unsigned int hwirq, struct pt_regs *regs)
{
        return __handle_domain_irq(domain, hwirq, true, regs);
}

参数如下:

  • 第一个参数传入了中断域;
  • 第二个参数传入了硬件中断号;
  • 第三个参数传入了寄存器结构指针,即中断发生时的寄存器信息。

这里调用了__handle_domain_irq,前面内容已经分析过了,这里就不再重复分析了。

五、中断流控处理回调handle_irq

linux中断子系统对几种常见的流控类型进行了抽象,并为它们实现了相应的标准函数,我们只要选择相应的函数,赋值给irq所对应的irq_desc结构的handle_irq字段中即可。这些标准的回调函数都是irq_flow_handler_t类型,定义在include/linux/irqhandler.h:

typedef void (*irq_flow_handler_t)(struct irq_desc *desc);

5.1 标准流控回调函数

目前在linux内核实现了以下这些标准流控回调函数,定义在kernel/irq/chip.c中:

  • handle_nested_irq:用于处理使用线程的嵌套中断;
  • handle_simple_irq: Simple and software-decoded IRQs.
  • handle_untracked_irq:Simple and software-decoded IRQs.
  • handle_level_irq:电平触发类型的中断处理函数;
  • handle_fasteoi_irq:用于需要响应eoi的中断控制器;
  • handle_fasteoi_nmi:irq handler for NMI interrupt lines;
  • handle_edge_irq:边沿触发类型的中断处理函数;
  • handle_percpu_irq:Per CPU local irq handler;
  • ...

驱动程序和板级代码可以通过以下几个API设置中断的流控回调函数:

  • irq_set_handler;
  • irq_set_chip_and_handler;
  • irq_set_chip_and_handler_name;

5.2 handle_simple_irq

/**
 *      handle_simple_irq - Simple and software-decoded IRQs.
 *      @desc:  the interrupt description structure for this irq
 *
 *      Simple interrupts are either sent from a demultiplexing interrupt
 *      handler or come from hardware, where no interrupt hardware control
 *      is necessary.
 *
 *      Note: The caller is expected to handle the ack, clear, mask and
 *      unmask issues if necessary.
 */
void handle_simple_irq(struct irq_desc *desc)
{
        raw_spin_lock(&desc->lock);  // 首先获取自旋锁

        if (!irq_may_run(desc))
                goto out_unlock;

        desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);

        if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {  // 没有指定中断处理函数、或者中断禁止
                desc->istate |= IRQS_PENDING;   // 挂起标志位置1
                goto out_unlock;
        }

        kstat_incr_irqs_this_cpu(desc);
        handle_irq_event(desc);

out_unlock:
        raw_spin_unlock(&desc->lock);  // 释放锁
}

该函数主要进行了以下操作:

  • 首先获取中断描述符的自旋锁;
  • 判断当前中断处理程序是已经执行,如果是退出;
  • 清除状态位IRQS_REPLAY 、IRQS_WAITING;
  • 调用handle_irq_event处理irq_desc中的action链表;
  • 释放自旋锁;

在irq_may_run中检测IRQD_IRQ_INPROGRESS标志位,在SMP系统中,同一个中断信号有可能发往多个CPU,但是中断处理只应该处理一次,所以设置状态为IRQD_IRQ_INPROGRESS,其他CPU执行此中断时都会先检查此状态。

static bool irq_may_run(struct irq_desc *desc)
{
        unsigned int mask = IRQD_IRQ_INPROGRESS | IRQD_WAKEUP_ARMED;

        /*
         * If the interrupt is not in progress and is not an armed
         * wakeup interrupt, proceed.
         */
        if (!irqd_has_set(&desc->irq_data, mask))
                return true;

        /*
         * If the interrupt is an armed wakeup source, mark it pending
         * and suspended, disable it and notify the pm core about the
         * event.
         */
        if (irq_pm_check_wakeup(desc))
                return false;

        /*
         * Handle a potential concurrent poll on a different core.
         */
        return irq_check_poll(desc);
}

5.3 handle_level_irq

/**
 *      handle_level_irq - Level type irq handler
 *      @desc:  the interrupt description structure for this irq
 *
 *      Level type interrupts are active as long as the hardware line has
 *      the active level. This may require to mask the interrupt and unmask
 *      it after the associated handler has acknowledged the device, so the
 *      interrupt line is back to inactive.
 */
void handle_level_irq(struct irq_desc *desc)
{
        raw_spin_lock(&desc->lock);
        mask_ack_irq(desc);

        if (!irq_may_run(desc))  
                goto out_unlock;

        desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);

        /*
         * If its disabled or no action available
         * keep it masked and get out of here
         */
        if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {   // 没有设置处理程序,或者中断禁止
                desc->istate |= IRQS_PENDING;         
                goto out_unlock;
        }

        kstat_incr_irqs_this_cpu(desc);
        handle_irq_event(desc);

        cond_unmask_irq(desc);

out_unlock:
        raw_spin_unlock(&desc->lock);
}

该函数用于处理电平中断,电平型触发中断在有效电平下会保持其中断请求状态。那么这需要在中断处理函数首先将该中断关闭(reg_mask寄存器),同时清除中断挂起(reg_pending寄存器),最后再开启该中断(reg_mask寄存器)。由于在开始将该中断关闭,在最后将该中断打开,在这期间,是不会有来自同一中断源的中断来干扰这段程序的执行的,也就该中断源不会产生新的中断,中断不会嵌套。

该函数主要进行了以下操作:

  • 首先获取中断描述符的自旋锁;
  • mask_ack_irq屏蔽中断,实际调用的是irq_chip 的irq_mask_ack(&desc->irq_data),通过写中断屏蔽寄存器reg_mask、reg_pending实现
  • 判断当前中断处理程序是否在执行,如果是退出;
  • 清除状态位IRQS_REPLAY 、IRQS_WAITING;
  • 调用handle_irq_event处理irq_desc中的action链表;
  • cond_unmask_irq取消中断屏蔽,实际调用的是irq_chip 的irq_unmask(&desc->irq_data),通过写中断屏蔽寄存器reg_mask实现;;
  • 释放自旋锁;

六、handle_irq_event

在linux的流控处理函数中均会执行handle_irq_event函数,该函数定义在kernel/irq/handle.c文件中,在该函数内部会执行irq_desc中的action链表。

6.1 handle_irq_event

irqreturn_t handle_irq_event(struct irq_desc *desc)
{
        irqreturn_t ret;

        desc->istate &= ~IRQS_PENDING;                      // 清除挂起标志位
        irqd_set(&desc->irq_data, IRQD_IRQ_INPROGRESS);     // 设置中断处理标志位
        raw_spin_unlock(&desc->lock);                       // 释放锁

        ret = handle_irq_event_percpu(desc);                // 中断处理

        raw_spin_lock(&desc->lock);                         // 获取锁
        irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS);   // 清除中断处理标志位
        return ret;
}

在SMP系统中,同一个中断信号有可能发往多个CPU,但是中断处理只应该处理一次,所以设置状态为IRQD_IRQ_INPROGRESS,其他CPU执行此中断时都会先检查此状态。在函数执行完毕会将标志位清除。

6.2 handle_irq_event_percpu

irqreturn_t handle_irq_event_percpu(struct irq_desc *desc)
{
        irqreturn_t retval;
        unsigned int flags = 0;

        retval = __handle_irq_event_percpu(desc, &flags);

        add_interrupt_randomness(desc->irq_data.irq, flags);

        if (!noirqdebug)
                note_interrupt(desc, retval);
        return retval;
}

6.3 __handle_irq_event_percpu

irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc, unsigned int *flags)
{
        irqreturn_t retval = IRQ_NONE;
        unsigned int irq = desc->irq_data.irq;
        struct irqaction *action;

        record_irq_time(desc);

        for_each_action_of_desc(desc, action) {
                irqreturn_t res;

                trace_irq_handler_entry(irq, action);
                res = action->handler(irq, action->dev_id);  // 调用action->handler、即request_threaded_irq时注册的primary handler
                trace_irq_handler_exit(irq, action, res);

                if (WARN_ONCE(!irqs_disabled(),"irq %u handler %pS enabled interruptsn",  // 如果当前CPU总中断被禁止,返回非0,否则返回0
                              irq, action->handler))
                        local_irq_disable();       // 关闭当前CPU所有中断

                switch (res) {
                case IRQ_WAKE_THREAD:      // 唤醒中断线程
                        /*
                         * Catch drivers which return WAKE_THREAD but
                         * did not set up a thread function
                         */
                        if (unlikely(!action->thread_fn)) {
                                warn_no_thread(irq, action);
                                break;
                        }

                        __irq_wake_thread(desc, action);   // 唤醒中断线程

                        /* Fall through - to add to randomness */
                case IRQ_HANDLED:
                        *flags |= action->flags;
                        break;

                default:
                        break;
                }

                retval |= res;
        }

        return retval;
}

6.4 __irq_wake_thread

void __irq_wake_thread(struct irq_desc *desc, struct irqaction *action)
{
        /*
         * In case the thread crashed and was killed we just pretend that
         * we handled the interrupt. The hardirq handler has disabled the
         * device interrupt, so no irq storm is lurking.
         */
        if (action->thread->flags & PF_EXITING)  // 设置了PF_EXITING标志 退出
                return;

        /*
         * Wake up the handler thread for this action. If the
         * RUNTHREAD bit is already set, nothing to do.
         */
        if (test_and_set_bit(IRQTF_RUNTHREAD, &action->thread_flags)) // 已经在执行
                return;

        /*
         * It's safe to OR the mask lockless here. We have only two
         * places which write to threads_oneshot: This code and the
         * irq thread.
         *
         * This code is the hard irq context and can never run on two
         * cpus in parallel. If it ever does we have more serious
         * problems than this bitmask.
         *
         * The irq threads of this irq which clear their "running" bit
         * in threads_oneshot are serialized via desc->lock against
         * each other and they are serialized against this code by
         * IRQS_INPROGRESS.
         *
         * Hard irq handler:
         *
         *      spin_lock(desc->lock);
         *      desc->state |= IRQS_INPROGRESS;
         *      spin_unlock(desc->lock);
         *      set_bit(IRQTF_RUNTHREAD, &action->thread_flags);
         *      desc->threads_oneshot |= mask;
         *      spin_lock(desc->lock);
         *      desc->state &= ~IRQS_INPROGRESS;
         *      spin_unlock(desc->lock);
         *
         * irq thread:
         *
         * again:
         *      spin_lock(desc->lock);
         *      if (desc->state & IRQS_INPROGRESS) {
         *              spin_unlock(desc->lock);
         *              while(desc->state & IRQS_INPROGRESS)
         *                      cpu_relax();
         *              goto again;
         *      }
         *      if (!test_bit(IRQTF_RUNTHREAD, &action->thread_flags))
         *              desc->threads_oneshot &= ~mask;
         *      spin_unlock(desc->lock);
         *
         * So either the thread waits for us to clear IRQS_INPROGRESS
         * or we are waiting in the flow handler for desc->lock to be
         * released before we reach this point. The thread also checks
         * IRQTF_RUNTHREAD under desc->lock. If set it leaves
         * threads_oneshot untouched and runs the thread another time.
         */
        desc->threads_oneshot |= action->thread_mask;

        /*
         * We increment the threads_active counter in case we wake up
         * the irq thread. The irq thread decrements the counter when
         * it returns from the handler or in the exit path and wakes
         * up waiters which are stuck in synchronize_irq() when the
         * active count becomes zero. synchronize_irq() is serialized
         * against this code (hard irq handler) via IRQS_INPROGRESS
         * like the finalize_oneshot() code. See comment above.
         */
        atomic_inc(&desc->threads_active);    // 线程唤醒次数

        wake_up_process(action->thread);   // 唤醒线程
}

最后调用wake_up_process唤醒中断线程。

七、总结

7.1 中断处理流程总结

通过上面的分析我们大概了解到内核启动的时候,进行了如下操作:

  • 为每个中断源分配struct irq_desc,以S3C2440为例,这里一共分配至111个中断,其中包含16个软件中断、32个主中断、15个内部子中断、24个外部中断等;
  • 由于主中断和内部子中断、外部中断存在级联的关系,初始化三个struct s3c_irq_intc结构,保存这三类中断的信息,并通过s3c24xx_init_intc函数进行中断初始化;以32个主中断为例:
    • irqs字段保存所有的主中断,指针类型,每一个成员都是struct s3c_irq_data类型;
    • 动态分配中断域istruct rq_domain,并进行初始化,实现硬件中断号到虚拟中断号的映射,其中domain->ops->map函数被用来初始化irq_desc的中断流控回调handle_irq以及irq_chip指针;

当中断发生时,将会执行中断处理程序irq_handler:

  • s3c24xx_handle_int函数首先获取硬件中断号,这里指的是主中断号;
  • 然后执行__handle_domain_irq:
    • 调用irq_find_mapping,根据硬件中断号以及主中断控制器对应的中断域irq_domain获取IRQ编号;
    • 调用generic_handle_irq:
      • 调用irq_to_desc根据IRQ编号获取中断描述符;
      • 调用中断描述符中的流控处理回调handle_irq;
      • handle_irq使用chip结构中的函数对当前中断进行屏蔽、取消屏蔽,还要调用用户在action链表中注册的primary handler,同时如果注册了中断线程化处理函数,还会唤醒中断线程,执行threaded interrupt handler;

需要注意的是:对于子中断,其所属的主中断描述符中的流控处理回调handle_irq被设置为了s3c_irq_demux,在该函数内部首先会获取到子中断控制器,然后读取中断挂起寄存器,根据寄存器请求位,找到子中断RQ编号,然后调用generic_handle_irq。

在SMP系统下,对于handle_level_irq而言,一次典型的情况是:

  • 中断控制器接收到中断信号,发送给一个或多个CPU,并执行中断处理函数;
  • 在中断处理函数中CPU会通知中断控制器屏蔽该中断(mask_ack_irq);
  • 之后当执行中断服务例程(handle_irq_event)时会设置该中断描述符的状态为IRQD_IRQ_INPROGRESS,表明其他CPU如果执行该中断就直接退出,因为本CPU已经在处理了。

因此可以推断出:

  • Linux中中断处理程序无需重入。当一个给定的中断处理程序正在执行时,相应的中断在所有处理器上都会被屏蔽,以防在同一个中断上接收另一个新的中断。也就是说,同一个中断,同一个中断处理程序,在执行完毕之前不可能同时在2个处理器上执行,加上中断处理程序不能休眠,因此无需考虑可重入性。
  • 中断处理程序可以被中断(不是同一个中断)也被称作嵌套中断(实际上在较新的linux版本中,在中断处理程序执行过程中硬件总中断是被屏蔽的,这样就不会在嵌套),但是同一个中断处理程序绝对不会被自己嵌套的。

7.2 中断处理程序中禁止睡眠

硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的 一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。

所谓的“ 中断上下文”,其实也可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被打断执行的进程环境)。

中断时,内核不代表任何进程运行,它一般只访问内核空间,而不会访问进程空间,由于中断处理并没有自独立的栈,而是使用了内核栈,其大小一般是有限制的,因此其必须短小精悍。

此外,由于中断服务打断了正常运行的程序流程,因此必须保证其快速运行,要求中断服务子程序执行时不能睡眠、阻塞。

简单来说,中断发生以后,CPU跳到内核设置好的中断处理代码中去,由这部分内核代码来处理中断。这个处理过程中的上下文就是中断上下文。

注意:中断上下文代码不允许睡眠,也不允许调用那些可能会引起睡眠的函数。

 在中断上下文中不允许睡眠的原因:

  • 如果在中断上下文中发生了睡眠,将会导致进程调度,当内核切换到其它的进程执行时,将无法再次唤醒该中断,因为所有的wake_up_xxx都是针对于进程而言的,而在中断上下文中,没有进程的概念,没有一个task_struct;
  • 进程调度会执行schedule方法,在该方法会保存当前的进程上下文(寄存器的值、进程状态等),以便以后恢复次进程运行。中断发生后,内核会先保存当前被中断的进程上下文,但在中断处理程序里,如果此时进行了进程调度,保存的将是中断上下文的内容了;
  • 内核中schedule方法进来时会进行判断当前是否处于中断上下文,如果是中断上下文,强行调用schedule的结果就是内核Bug;

7.3 具体中断的数据结构框图

这里以EINT0中断为例,当内核启动完后,我们绘制其中断描述符以及相关结构信息大致如下:

7.4 中断相关的目录结构

include/linux下目录,存放linux内核中断相关的头文件:

  • irq.h:放irq_chip、irq_data结构相关定义;
  • irqdesc.h:存放irq_desc 结构相关定义;
  • irqdomain.h:存放irq_domain结构相关定义;

kernel/irq下目录,存放linue内核中断相关的实现:

  • irqdesc.c:中断描述符分配等相关代码;
  • irqdomain.c:中断域初始化等相关代码;
  • chip.c:标准标准流控回调函数、中断使能、屏蔽等实现;
  • handle.c;

drivers/irqchip目录下:

  • irq-s3c24xx.c:SOC相关的中断初始化;

arch/arm目录下:

  • kernel/entry-armv.S:异常向量表相关代码;
  • kernel/irq.c:中断处理相关代码;
  • kernel/traps.c;
  • mach-s3c24xx/include/mach/irqs.h:SOC相关的IRQ编号定义;
  • mach-s3c24xx/mach-smdk2440.c:存放有SOC中断初始化的代码;

以上各个目录只是列出部分文件。

参考文章

[1]Linux中断(interrupt)子系统之二:arch相关的硬件封装层

[2]5.分析内核中断运行过程,以及中断3大结构体:irq_desc、irq_chip、irqaction(详解)

[3]linux3.10 中断处理过程(二)s3c2440中断控制器及处理函数的初始化

[4]linux驱动之中断

[5]【原创】Linux中断子系统(一)-中断控制器及驱动分析

[6]linux kernel的中断子系统之(三):IRQ number和中断描述符

[7]Linux中断(interrupt)子系统之三:中断流控处理层

[8]关于handle_level_irq、handle_edge_irq和中断嵌套问题

[9]linux中断源码分析 - 中断发生(三)

[10]基于设备树的中断实现 (24x0平台)

内容来源于网络如有侵权请私信删除

文章来源: 博客园

原文链接: https://www.cnblogs.com/zyly/p/15952962.html

你还没有登录,请先登录注册
  • 还没有人评论,欢迎说说您的想法!

相关课程