linux中断处理的流程

linux-5.10.4版本:interrupt.h

deepseek_mermaid_20250903_446fba

linux系统启动的流程

irq_domain

irq_domain 是一个中间翻译层,它负责将硬件的中断号(Hardware Interrupt Number)映射到 Linux 内核的虚拟中断号(Virtual IRQ Number),并管理中断控制器的相关操作。

你可以把它想象成一个翻译官接线总机

  • 硬件中断号 (HW IRQ):是中断控制器自己识别的编号。例如,对于 ARM GIC,SGIs 是 0-15,PPIs 是 16-31,SPIs 从 32 开始。这是硬件固定的,不同控制器完全不同。
  • 虚拟中断号 (VIRQ):是 Linux 内核内部使用的、统一的软件中断编号。驱动开发者请求和使用的就是这个号。

它的作用:

  • 抽象硬件差异

    世界上有无数种中断控制器(GIC, APIC, IO-APIC, GPIO 控制器等),每种都有自己的编号方案和编程模型。内核无法为每种控制器都写一套独立的中断处理流程。irq_domain 提供了一个统一的抽象接口,让内核核心代码只需与 irq_domain 交互,而由各中断控制器驱动去实现具体的操作。

  • 管理中断号映射

    这是最核心的功能。当设备树(Device Tree)中描述一个设备的中断时,它只写硬件信息,例如:

    1
    interrupts = <0 58 4>; // 代表这个设备连接到GIC的SPI,硬件中断号是58

    内核需要为这个“硬件中断号 58”分配一个“虚拟中断号”(比如 126)。irq_domain 就负责创建并维护这个映射关系。当驱动调用 request_irq(126, ...) 时,内核通过 irq_domain 知道,最终要去配置 GIC 控制器上编号为 58 的中断源。

  • 提供控制器特定的操作函数集

    每个 irq_domain 都关联着一个 irq_domain_ops 结构体,里面包含了一系列函数指针,用于执行该控制器特有的操作:

    • .map(): 中断映射。在设备树解析到设备中断时,调用此函数来建立硬件中断号到虚拟中断号的映射,并配置该中断线的处理方式(设置 ->handle_irq 函数)。gic_irq_domain_map 函数,就是这个操作的具体实现!
    • .alloc(): 另一种分配中断号的方式。
    • .translate(): 从设备树信息中翻译出硬件中断号。
    • .xclate(): 转换中断说明符。
  • 支持中断控制器的级联

    一个系统可能有多个中断控制器(例如,一个主 GIC 和一个作为中断源的 GPIO 控制器级联到 GIC 上)。每个控制器都需要自己的 irq_domain。子控制器的 irq_domain 会将它的虚拟中断号映射到父控制器的硬件中断号上,从而形成一个层次结构,内核可以无缝地处理这种级联关系。

系统启动的流程分析

  1. start_kernel函数

    内核的启动函数,通过调用众多的子函数来完成 Linux 启动之前的一些初始化工作。

    1
    2
    3
    4
    5
    6
    7
    8
    start_kernel()
    -> local_irq_disable()//关闭当前 CPU 的中断响应。通常通过设置 ARM CPSR 寄存器的 I //bit (对于 IRQ) 和 F bit (对于 FIQ) 来实现。
    -> sort_main_extable()//对内核的异常表(Exception Table)进行排序
    -> trap_init()//初始化系统陷阱(Trap)或异常向量表
    -> ...
    -> init_IRQ()//架构相关的中断初始化。
    -> softirq_init()//初始化软中断(SoftIRQ)机制。
    -> local_irq_enable()//开启当前 CPU 的中断响应
    函数名 调用阶段 核心作用 详细解释 与中断的关系
    local_irq_disable() 最早阶段 关闭CPU中断响应 在一切初始化之前,创造一个安全的、不被中断打断的原子环境,防止未初始化时中断导致系统崩溃。 奠基与防护:为后续初始化提供安全环境,是中断系统启动的前提
    sort_main_extable() 早期准备 排序异常表 对内核的异常修复表(__ex_table)进行排序,以便在发生页面错误等异常时能快速查找并使用二分法定位修复地址。 增强鲁棒性:不直接处理中断,但为异常处理(中断的一种)提供错误恢复机制,提升稳定性。
    trap_init() 基础设施搭建 初始化异常向量表 设置CPU的异常向量基地址寄存器(如ARM的VBAR),填充异常/中断的通用处理入口,是CPU响应异常和中断的硬件跳转表 硬件基石:建立了中断/异常发生后CPU的第一跳转目的地,是中断能够被处理的绝对基础
    init_IRQ() 软件框架搭建 架构相关中断初始化 初始化架构相关的中断通用代码,通常负责分配和初始化核心数据结构 irq_desc 数组(中断描述符),为每个中断号建立管理单元。 软件框架:搭建了内核管理所有中断的核心框架irq_desc),是连接通用中断子系统与具体硬件的架构桥梁。
    softirq_init() 下半部机制初始化 初始化软中断机制 初始化 tasklet 和软中断(SoftIRQ)机制,为中断的下半部(Bottom Half)处理提供延迟执行的框架。 完善处理流程:完成了中断处理“上半部+下半部”的完整框架,允许中断处理函数将非紧急任务推迟执行。
    local_irq_enable() 最终开启 开启CPU中断响应 在所有中断基础设施(向量表、控制器、框架、下半部)都已初始化完毕后,正式打开CPU的中断响应开关,系统开始正常处理中断。 正式启用:标志着中断系统全面启动完成,内核从静态初始化阶段进入动态运行阶段。
  2. 中断初始化init_IRQ

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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: %d\n", ret);
    }

    uniphier_cache_init();
    }
    函数名 调用阶段 核心作用 详细解释 与中断的关系
    irqchip_init() 硬件控制器初始化 初始化中断控制器 解析设备树,识别系统中的应用中断控制器(如GIC),映射其寄存器,禁用所有中断源,设置默认路由,并向内核注册控制器(创建irq_domain)。 驱动硬件:是驱动中断控制器硬件的关键步骤,使内核能够具体地配置、使能和接收来自硬件设备的中断信号。
  3. irqchip_init函数

    1
    2
    3
    4
    5
    void __init irqchip_init(void)
    {
    of_irq_init(__irqchip_of_table);
    acpi_probe_device_table(irqchip);
    }

    of_irq_init(__irqchip_of_table); of_irq_init遍历设备树,找到所有已注册的中断控制器节点,并调用它们各自的初始化函数。

    1. 入参:接收一个列表 __irqchip_of_table,这个列表包含了所有在内核编译时注册的、支持的中断控制器信息(兼容字符串和初始化函数指针)。当中断控制器驱动(如 GIC 驱动)使用 IRQCHIP_DECLARE 宏声明时,它的兼容性字符串和初始化函数指针就会被添加到这个段中。

      例如:IRQCHIP_DECLARE

      1
      IRQCHIP_DECLARE(gic_400, "arm,gic-400", gic_of_init);
    2. 扫描设备树:遍历整个设备树(Device Tree),查找所有 interrupt-controller 属性,并获取它们的 compatible 属性。

    3. 匹配驱动:将设备树中每个中断控制器节点的 compatible 属性与 __irqchip_of_table 列表中的项进行匹配。

    4. 初始化调用:一旦匹配成功,就调用该中断控制器在 __irqchip_of_table 中注册的初始化函数(例如,对于 "arm,gic-400",就会调用 gic_of_init)。

    5. 处理级联:它会正确处理中断控制器的级联关系(例如,一个主 GIC 连接一个 GPIO 中断控制器),确保父控制器在子控制器之前被初始化。

  4. gic_of_init函数会

    • 映射硬件寄存器。
    • 初始化私有数据 struct gic_chip_data
    • 调用 gic_init_bases()
  5. gic_init_bases函数中注册**irq_domain**

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    if (handle) {		/* DT/ACPI *///这是一个指针,通常指向设备树(DT)或ACPI中描述这个GIC控制器的节点。如果它存在,说明内核是通过现代的设备树或ACPI机制来获取硬件信息的。
    gic->domain = irq_domain_create_linear(handle, gic_irqs,
    &gic_irq_domain_hierarchy_ops,
    gic);
    }
    else { /* Legacy support *///为了兼容那些没有使用设备树的旧平台。
    /*
    * For primary GICs, skip over SGIs.
    * No secondary GIC support whatsoever.
    */
    int irq_base;

    gic_irqs -= 16; /* calculate # of irqs to allocate */
    irq_base = irq_alloc_descs(16, 16, gic_irqs, numa_node_id());
    if (irq_base < 0) {
    WARN(1, "Cannot allocate irq_descs @ IRQ16, assuming pre-allocated\n");
    irq_base = 16;
    }
    gic->domain = irq_domain_add_legacy(NULL, gic_irqs, irq_base,
    16, &gic_irq_domain_ops, gic);
    }
  6. &gic_irq_domain_hierarchy_ops参数 irq_domain_ops

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    static const struct irq_domain_ops gic_irq_domain_hierarchy_ops = {
    .translate = gic_irq_domain_translate,
    .alloc = gic_irq_domain_alloc,
    .free = irq_domain_free_irqs_top,
    };

    static int gic_irq_domain_alloc(struct irq_domain *domain, unsigned int virq,
    unsigned int nr_irqs, void *arg)
    {
    int i, ret;
    irq_hw_number_t hwirq;
    unsigned int type = IRQ_TYPE_NONE;
    struct irq_fwspec *fwspec = arg;

    ret = gic_irq_domain_translate(domain, fwspec, &hwirq, &type);
    if (ret)
    return ret;

    for (i = 0; i < nr_irqs; i++) {
    ret = gic_irq_domain_map(domain, virq + i, hwirq + i);//映射
    if (ret)
    return ret;
    }

    return 0;
    }

  7. 6中代码的gic_irq_domain_map函数为中断域的映射,发生在系统启动时,中断控制器被初始化之后,任何驱动申请中断之前。作用为负责为每个硬件中断号配置对应的Linux虚拟中断号的处理方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    static int gic_irq_domain_map(struct irq_domain *d, unsigned int irq,
    irq_hw_number_t hw)
    {
    struct gic_chip_data *gic = d->host_data;
    struct irq_data *irqd = irq_desc_get_irq_data(irq_to_desc(irq));

    switch (hw) {
    case 0 ... 15:
    irq_set_percpu_devid(irq);
    irq_domain_set_info(d, irq, hw, &gic->chip, d->host_data,
    handle_percpu_devid_fasteoi_ipi,
    NULL, NULL);
    break;
    case 16 ... 31:
    irq_set_percpu_devid(irq);
    irq_domain_set_info(d, irq, hw, &gic->chip, d->host_data,
    handle_percpu_devid_irq, NULL, NULL);
    break;
    default:
    irq_domain_set_info(d, irq, hw, &gic->chip, d->host_data,
    handle_fasteoi_irq, NULL, NULL);
    irq_set_probe(irq);
    irqd_set_single_target(irqd);
    break;
    }

    /* Prevents SW retriggers which mess up the ACK/EOI ordering */
    irqd_set_handle_enforce_irqctx(irqd);//
    return 0;
    }
    • irq_domain:是内核用于管理中断控制器和硬件中断号的一个抽象层。每个中断控制器都有一个自己的 irq_domain
    • irq:Linux 内核的虚拟中断号。这是一个软件概念,驱动开发者请求和使用的就是这个号。
    • hw硬件中断号。这是 GIC 控制器自己识别的中断编号(如 SPI 从 32 开始,PPI 从 16 开始)

    switch (hw) {...} 根据硬件中断号进行配置

    这是整个函数的核心,根据硬件中断号的不同范围,将中断分为三类进行处理:

    • 1. Case 0 … 15: Software Generated Interrupt (SGIs) 核间中断,一个CPU核心可以用它来中断另一个CPU核心。

    • 2. Case 16 … 31: Private Peripheral Interrupt (PPIs) 每个CPU核心私有的外设中断

    • 3. Default (通常为 32+): Shared Peripheral Interrupt (SPIs) 共享外设中断,所有CPU核心都可以看到的中断

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      irq_domain_set_info(d, irq, hw, &gic->chip, d->host_data,handle_fasteoi_irq, NULL, NULL);

      /**************** 1 *****************/
      //kernel/irq/irqdomain.c
      void irq_domain_set_info(struct irq_domain *domain, unsigned int virq,
      irq_hw_number_t hwirq, struct irq_chip *chip,
      void *chip_data, irq_flow_handler_t handler,
      void *handler_data, const char *handler_name)
      {
      irq_set_chip_and_handler_name(virq, chip, handler, handler_name);
      irq_set_chip_data(virq, chip_data);
      irq_set_handler_data(virq, handler_data);
      }

      /**************** 2 *****************/
      ///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);
      }
      /**************** 3 *****************/
      //kernel/irq/chip.c
      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);
      }
      • d: 所属的 irq_domain

      • irq: Linux 虚拟中断号

      • hw: 硬件中断号

      • &gic->chip: 指向中断控制器芯片的底层操作集(struct irq_chip *),包含像 .irq_mask, .irq_unmask, .irq_eoi 这样的硬件操作函数。

      • handle_percpu_devid_fasteoi_ipi 等:这是最关键的参数,它设置了 irq_desc->handle_irq 字段,即中断的流处理函数

  8. __irq_do_set_handler - 设置流处理函数__irq_do_set_handler

    这个函数是第一部分中 irq_domain_set_info 等函数在底层最终会调用的函数。它的工作非常简单直接:

    1. 安全地将流处理函数安装到中断描述符中
      • desc->handle_irq = handle;
      • 它将中断描述符(struct irq_desc *desc)的 handle_irq 成员设置为指定的流处理函数(如 handle_fasteoi_irq,这个函数在内核编译时就已确定并存放在内核镜像中)。
    2. 处理各种边界情况,特别是层级中断域和链式中断
    3. 确保中断状态的正确性
    4. 处理中断的卸载情况

    这个函数完成了从“配置”到“执行”的最后一步挂钩操作。

CPU部分流程

特点:

  1. 硬件自动发起:最初的跳转和状态保存由CPU硬件逻辑完成,无法干预。
  2. 汇编语言处理:后续的现场保存、模式切换、分发都由高度优化的汇编代码完成。
  3. 关键目标
    • 保护现场:完整保存被中断任务的执行状态(寄存器)。
    • 模式切换:从IRQ模式切换到更通用的SVC模式。
    • 获取中断源:获取硬件中断号(hwirq)。
    • 传递上下文:将硬件中断号和保存的寄存器结构(pt_regs)传递给C处理函数。
  4. 关中断:在整个CPU处理阶段,IRQ中断始终是关闭的(CPSR.I=1),直到C代码中的某些阶段才可能被打开。

当外设触发中断,中断控制器(如GIC)仲裁后会将中断信号发送给CPU核心。CPU核心会自动执行以下操作:

deepseek_mermaid_20250903_b988d0

硬件部分自动完成

如同我们在之前CPSR小节学习到的知识,当外设触发中断,中断控制器(如GIC)仲裁后会将中断信号发送给CPU核心。CPU核心会自动执行以下操作:

  • 保存状态:将当前程序的状态寄存器 CPSR 保存到 SPSR_irq(IRQ模式下的备份程序状态寄存器)。
  • 模式切换:修改 CPSR 的低5位,让CPU立即切换到IRQ模式
  • 关闭中断自动将 CPSR 中的 I bit 置1禁用新的IRQ中断,防止中断嵌套。这是关键一步。
  • 保存返回地址:将下一条即将要执行的指令地址(即返回地址)保存到 LR_irq 寄存器。注意:对于ARM架构,这个地址需要矫正(LR - 4)才能正确返回,流程图中 vector_stub irq, IRQ_MODE, 44 就是这个矫正值。
  • 跳转到异常向量表:CPU自动跳转到异常向量表的固定地址 0x00000018(对于IRQ)执行。

小结:至此,CPU在完全由硬件控制的情况下,保存了现场,并跳转到了软件指定的入口。这是任何中断处理的起点。

伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
; === CPU 硬件自动完成的操作(非实际指令,而是硬件行为) ===

; 1. 保存状态:将当前 CPSR 保存到 SPSR_irq
SPSR_irq = CPSR

; 2. 模式切换:修改 CPSR 的低5位,切换到 IRQ 模式
CPSR[4:0] = 0b10010 ; 设置模式为 IRQ

; 3. 关闭中断:禁用 IRQ
CPSR[7] = 1 ; 设置 I-bit,禁用 IRQ 中断

; 4. 保存返回地址:将下一条指令的地址保存到 LR_irq
; 注意:由于 ARM 流水线架构,PC 值通常比当前指令超前 2 条指令(8 字节)
; 因此需要矫正,返回地址实际应为 PC - 4(即下一条指令)
LR_irq = PC - 4 ; 对于 ARM 状态,返回地址是当前指令 + 4

; 5. 跳转到异常向量表:强制将 PC 设置为 IRQ 异常向量的地址
PC = 0x00000018 ; 跳转到 IRQ 异常向量入口

CPU部分其余流程

  1. 异常向量表 (软件定义的第一跳)

    异常向量表是软件预先设置好的一块内存区域,通常由汇编代码定义(如 arch/arm/kernel/entry-armv.S)。CPU只是机械地跳转到这里。

    1
    2
    3
    4
    5
    6
    .L__vectors_start:
    W(b) vector_rst @ 0x00 Reset
    W(b) vector_und @ 0x04 Undefined Instruction
    ...
    W(b) vector_irq @ 0x18 IRQ Interrupt <-- CPU跳到这里
    W(b) vector_fiq @ 0x1C FIQ Interrupt

    vector_irq 是一条跳转指令,跳转到 vector_stub 宏展开的代码。这是软件处理中断的开始。

  2. 汇编入口处理 (vector_stub 宏)

    vector_stub 是一个宏,它负责进行更精细的现场保存和模式准备。它的主要工作是:

    • 地址矫正:修正 LR 寄存器中的返回地址。

    • 保存寄存器:将 r0lr(返回地址)暂时压入IRQ模式的栈。

    • 保存原始状态:将 SPSR_irq(即中断发生时的 CPSR)也保存到栈上。

    • 准备切换到SVC模式:修改 SPSR 的值,为接下来切换到SVC模式做准备。之所以要切换到SVC模式,是因为SVC模式有更丰富的资源(如栈),方便执行更复杂的C代码。

    • 根据中断发生前的模式进行分发:根据保存的原始 CPSR,判断中断发生前CPU是处于用户模式(USR)还是内核模式(SVC),从而跳转到不同的处理路径(__irq_usr__irq_svc)。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      ; === arch/arm/kernel/entry-armv.S 中的 vector_stub 宏 ===

      .macro vector_stub, name, mode, correction=0
      .align 5
      vector_\name:
      .if \correction
      sub lr, lr, #\correction @ 矫正返回地址!这里 correction=4
      .endif

      @ 保存 r0 和 lr (用户模式的 PC) 到 IRQ 栈
      stmia sp, {r0, lr}
      mrs lr, spsr @ 获取保存的 CPSR (SPSR_irq)
      str lr, [sp, #8] @ 将 SPSR 也保存到栈上

      @ 准备切换到 SVC 模式,但 IRQ 仍然保持禁用
      mrs r0, cpsr
      eor r0, r0, #(\mode ^ SVC_MODE | PSR_ISETSTATE)
      msr spsr_cxsf, r0 @ 设置 SPSR 为 SVC 模式

      @ 根据中断发生前的模式(保存在 SPSR 中),跳转到不同的处理程序
      and lr, lr, #0x0f @ 获取之前的模式
      mov r0, sp
      ldr lr, [pc, lr, lsl #2] @ 根据模式加载跳转地址
      movs pc, lr @ 跳转到处理程序,并恢复 CPSR
      .endm

      ; 使用宏创建 IRQ 向量 stub,并指定矫正值为 4
      vector_stub irq, IRQ_MODE, 4

      .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) - 从内核模式中断
      @ ... 其他模式
  3. 中断处理分发 (__irq_usr / __irq_svc)

    __irq_usr(在用户空间被中断)为例:IRQ_USR

    1
    2
    3
    4
    5
    6
    7
    8
    9
    __irq_usr:
    usr_entry
    kuser_cmpxchg_check
    irq_handler
    get_thread_info tsk
    mov why, #0
    b ret_to_user_from_irq
    UNWIND(.fnend )
    ENDPROC(__irq_usr)
    • usr_entry:继续保存被中断的用户进程的上下文(所有通用寄存器),形成一个完整的 pt_regs 结构体,保存在栈上。这是后续能够恢复现场的关键。

    • irq_handler:这是最关键的一步,它负责获取中断号并跳转到高级别的C语言中断处理函数。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      	.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
      • 它最终会调用一个架构相关的函数(如 arch_irq_handler_default)来获取硬件中断号(hwirq)
      • 然后调用 asm_do_IRQ(hwirq, pt_regs)__handle_domain_irq(...)
  4. arch_irq_handler_default

    1
    2
    3
    4
    5
    6
    7
    8
    9
    	.macro	arch_irq_handler_default
    get_irqnr_preamble r6, lr
    1: get_irqnr_and_base r0, r2, r6, lr@r0= 中断编号(如果待处理)。r2= 中断状态寄存器。
    movne r1, sp
    @
    @ routine called with r0 = irq number, r1 = struct pt_regs *
    @
    badrne lr, 1b
    bne asm_do_IRQ@如果中断挂起,则分支到 (C 处理程序)。
  5. 通用中断处理入口!!!!!asm_do_IRQ在arch/arm/kernel/irq.c文件中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    /*********************    1    *********************************/
    //arch/arm/kernel/irq.c
    asmlinkage void __exception_irq_entry
    asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
    {
    handle_IRQ(irq, regs);
    }
    /********************* 2 *********************************/
    //arch/arm/kernel/irq.c
    void handle_IRQ(unsigned int irq, struct pt_regs *regs)
    {
    __handle_domain_irq(NULL, irq, false, regs);
    }
    /********************* 3 *********************************/
    //kernel/irq/irqdesc.c
    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);
    #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;
    }

    /********************* 4 *********************************/
    //kernel/irq/irqdesc.c
    int generic_handle_irq(unsigned int irq)
    {
    struct irq_desc *desc = irq_to_desc(irq);//irq变成结构体desc
    struct irq_data *data;

    if (!desc)
    return -EINVAL;

    data = irq_desc_get_irq_data(desc);
    if (WARN_ON_ONCE(!in_irq() && handle_enforce_irqctx(data)))
    return -EPERM;

    generic_handle_irq_desc(desc);
    return 0;
    }
    /********************* 5 *********************************/
    //include/linux/irqdesc.h
    static inline void generic_handle_irq_desc(struct irq_desc *desc)
    {
    desc->handle_irq(desc);
    }

同过上述的流程我们可以发现,CPU部分最终linux调用的desc->handle_irq(desc);,它的作用是执行与该中断类型相匹配的、预设的通用流处理(flow handling)逻辑。它调用的不是一个具体设备驱动注册的函数,而是一个架构或中断控制器相关的通用处理函数。这个函数负责处理特定类型中断的通用流程,例如:

  • 处理中断控制器交互:如确认(ACK)、屏蔽(mask)、结束中断(EOI)等操作。
  • 管理中断状态:处理边缘触发(edge-triggered)或电平触发(level-triggered)中断的特定逻辑。
  • 调用设备驱动:在适当的时机,调用由设备驱动通过 request_irq() 注册的具体的中断服务程序。

常见的 handle_irq 函数指针指向的处理函数有:

  • handle_fasteoi_irq: 用于支持“End Of Interrupt”的中断控制器(如 ARM GIC),先处理中断,最后再发送 EOI。
  • handle_level_irq: 用于电平触发中断的处理。在处理前先屏蔽中断,处理完后再打开,防止中断嵌套导致的电平持续触发问题。
  • handle_edge_irq: 用于边缘触发中断的处理。
  • handle_percpu_irq: 用于每个 CPU 独有的中断。
  • handle_simple_irq: 最简单的处理,没有硬件交互,假设所有硬件操作都由驱动自己完成。

这个函数指针是在linux系统启动过程中的中断映射阶段(例如,在 gic_irq_domain_map 函数中通过 irq_domain_set_info 设置)被初始化的,它根据中断的硬件特性(是电平触发还是边缘触发,是共享的还是 per-CPU 的等)来选择最合适的处理函数。

小结:至此,CPU已经完成了从硬件自动响应到汇编保存现场,再到获取中断号并准备调用C处理函数的所有准备工作。中断处理的重心从汇编代码彻底移交给了C代码。

具体的设备驱动注册过程

irq_desc结构体

  • 在系统启动早期,内核会调用 early_irq_init() 函数(位于 kernel/irq/irqdesc.c)。这个函数的主要职责就是预分配(或静态初始化)整个 irq_desc 数组

Linux 内核为每一个虚拟中断号(VIRQ) 都维护了一个核心数据结构 struct irq_desc。你可以把它想象成一个中断的“档案袋”或“管理员”,里面记录了这个中断的所有信息。最关键的两个成员是:

  1. ->handle_irq:指向一个流处理函数(例如 handle_fasteoi_irq, handle_level_irq)。这个函数是中断处理框架的“舞台经理”,负责处理中断的通用流程(如确认、屏蔽、EOI等)。
  2. ->action:指向一个 struct irqaction 的链表。你通过 request_irq 注册的信息就封装在这里面,包括你的中断服务函数 name1irqreturn

驱动注册流程分析

  1. 当我们使用**request_irq(unsigned int irq, ...)**注册调用函数。传入虚拟中断号 irq 和中断处理函数(例如 name1_irq_return_handle)。request_irq

    1
    2
    3
    4
    5
    6
    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);
    }
  2. request_threaded_irq(): request_irq 会调用这个函数。它创建一个 struct irqaction 结构体,并将 name1_irq_return_handle 函数指针赋值给 action->handlerrequest_threaded_irq()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    int request_threaded_irq(unsigned int irq, irq_handler_t handler,
    irq_handler_t thread_fn, unsigned long irqflags,
    const char *devname, void *dev_id)
    {
    struct irqaction *action;
    struct irq_desc *desc;
    int retval;

    desc = irq_to_desc(irq)//获取中断描述符 (struct irq_desc)
    action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);//中断动作结构体初始化
    if (!action)
    return -ENOMEM;

    action->handler = handler;
    action->thread_fn = thread_fn;
    action->flags = irqflags;
    action->name = devname;
    action->dev_id = dev_id;

    retval = __setup_irq(irq, desc, action);//核心中断设置
    }
  3. __setup_irq()setup_irq: 这是中断设置的最终函数。它会将上一步创建好的 irqaction 结构体,添加到 irq_desc[irq]->action 链表的末尾

    如果这个中断号被多个设备驱动共享(即多个驱动都对同一个 virq 调用了 request_irq(irq, handler, IRQF_SHARED, ...)),那么这个 ->action 链表上就会有多个 struct irqaction 节点。每个节点都保存着一个设备驱动的中断服务函数。

    中断触发时:当一个特定的硬件中断发生,经过 irq_domain 翻译后,得到对应的一个虚拟中断号 virqX

    • 内核会找到 irq_desc[virqX]
    • 然后遍历 irq_desc[virqX]->action 这个链表。
    • 调用这个链表上每一个 action->handler