2. Linux中断子系统架构:从硬件到软件的分层设计

好,咱们接着聊中断。上一章我讲了中断的基本概念,这一章咱们深入内核,看看Linux到底是怎么把中断这件事儿组织起来的。

说白了,中断子系统就是一条从硬件到软件的数据管道。硬件那边一有动静,软件这边就得响应。但问题是——硬件千奇百怪,软件又得统一处理,怎么办?

答案就是:分层。

2.1 三层架构:硬件、通用层、驱动层

我个人习惯把中断子系统分成三层来看:

  • 硬件层:CPU、中断控制器(GIC、APIC这些)、外设。它们负责产生和传递中断信号。
  • 通用中断层(Generic IRQ Layer):内核的核心代码,跟具体硬件无关。它管理中断号、中断描述符、中断流控。
  • 驱动层:每个设备驱动注册自己的中断处理函数。这一层最贴近业务。

你想想看,如果没有这个分层,每个驱动都得自己去操作中断控制器的寄存器,那代码得多乱?而且换个CPU架构,所有驱动都得重写。这显然不现实。

核心思想:通用中断层把硬件差异封装起来,驱动只需要调用 request_irq() 或 request_threaded_irq() 就能注册中断处理函数。至于中断是怎么路由过来的,驱动不用操心。

2.2 核心数据结构:irq_desc

说到分层设计,就绕不开一个核心结构体——irq_desc。它是整个中断子系统的“户口本”。每个中断号都对应一个 irq_desc 实例。

我在项目中遇到过一个问题:系统里注册了上百个中断,突然有一个外设不响应了。我第一反应就是去查这个中断号对应的 irq_desc,看看它的状态是不是被屏蔽了,或者处理函数是不是被覆盖了。

来看一下这个结构体的核心成员(我挑几个重点讲):

struct irq_desc {
    struct irq_common_data  irq_common_data;
    struct irq_data         irq_data;
    unsigned int __percpu   *kstat_irqs;
    irq_flow_handler_t      handle_irq;   // 中断流控处理函数
    struct irqaction        *action;      // 中断处理函数链表
    unsigned int            status_use_accessors;
    struct list_head        pending;      // 待处理中断链表
    struct irq_chip         *irq_chip;    // 硬件操作函数集
    int                     parent_irq;
    struct module           *owner;
    const char              *name;
    wait_queue_head_t       wait_for_threads;
    struct proc_dir_entry   *dir;
    int                     nr_actions;
    unsigned int            threads_handled;
    unsigned int            threads_active;
    struct cpumask          *percpu_enabled;
    struct cpumask          *percpu_affinity;
    unsigned long           threads_oneshot;
    atomic_t                threads_active_cnt;
    struct task_struct      *threads_affinity;
    struct irq_thread       *threads;
    unsigned long           irq_count;
    unsigned long           last_unhandled;
    unsigned int            irqs_unhandled;
    atomic_t                threads_handled_last;
    raw_spinlock_t          lock;
    struct cpumask          *irq_2_pin;
    struct irq_affinity_notify *affinity_notify;
    int                     node;
    unsigned int            core_internal_state__do_not_mess_with_it;
};

我的经验:调试中断问题时,我最常看的是 actionhandle_irq 这两个字段。action 是链表,因为多个设备可以共享同一个中断号。handle_irq 是流控函数,它决定了中断是边沿触发还是电平触发,以及怎么处理嵌套中断。

2.3 中断流控:handle_irq 的秘密

你可能会问:为什么需要 handle_irq?直接调用驱动的处理函数不就行了?

嗯,这里要注意。不同的中断控制器,行为不一样。有的支持电平触发,有的支持边沿触发。有的需要自动屏蔽,有的不需要。这些差异就靠 handle_irq 来抽象。

内核预定义了几种标准的流控函数:

流控函数 适用场景 特点
handle_simple_irq 简单中断控制器 不做任何硬件操作,直接调用处理函数
handle_level_irq 电平触发中断 进入处理函数前屏蔽中断,处理完再解除屏蔽
handle_edge_irq 边沿触发中断 记录挂起的中断,防止丢失
handle_fasteoi_irq 支持EOI的中断控制器 处理完中断后写EOI寄存器

我曾经踩过一个坑:一个网卡驱动用了电平触发,但流控函数选成了 handle_edge_irq。结果就是中断频繁丢失,网络时断时续。查了两天才发现是流控函数配错了。

避坑指南:流控函数的选择必须跟硬件的中断触发方式匹配。电平触发用 handle_level_irq,边沿触发用 handle_edge_irq。混用会导致中断丢失或重复触发。

2.4 从硬件到软件的完整路径

咱们来走一遍完整流程。假设一个GPIO按键按下:

  1. 硬件阶段:GPIO控制器检测到电平变化,向中断控制器(比如GIC)发送中断信号。
  2. 中断控制器阶段:GIC解析中断号,通过IRQ线通知CPU。
  3. CPU阶段:CPU保存现场,跳转到内核的异常向量表,进入 handle_arch_irqdo_IRQ
  4. 通用中断层:根据中断号找到对应的 irq_desc,调用 handle_irq 流控函数。
  5. 驱动层:流控函数调用 action 链表里的处理函数,执行具体的业务逻辑。

你看,从硬件到软件,每一层各司其职。这就是分层设计的魅力。

2.5 核心逻辑流程图

下面我用一张SVG图来展示这个流程,方便你理解:

Linux中断处理完整流程 硬件层 外设 → 中断控制器(GIC/APIC) → CPU IRQ引脚 异常向量表 / 汇编入口 保存现场 → 调用 do_IRQ() 或 handle_arch_irq() 通用中断层 (Generic IRQ Layer) 根据中断号查找 irq_desc → 调用 handle_irq 流控函数 处理中断屏蔽、EOI、嵌套等硬件差异 驱动层 (Device Driver) 执行 action 链表中的中断处理函数 核心: irq_desc request_irq() 注册

2.6 irq_desc 的管理机制

内核用一个全局数组来管理所有的 irq_desc

struct irq_desc *irq_to_desc(unsigned int irq)
{
    return (irq < NR_IRQS) ? irq_desc + irq : NULL;
}

这个数组的大小 NR_IRQS 在编译时确定。但实际系统中,中断号可能远小于这个值。所以内核也支持稀疏分配(SPARSE_IRQ),用基数树来管理,节省内存。

我记得有一次在嵌入式设备上,NR_IRQS 配得太大,导致内核映像膨胀了不少。后来改成 SPARSE_IRQ,内存占用降下来了。嗯,这里要注意,如果你的设备中断号不连续,建议开启 SPARSE_IRQ。

2.7 小结

这一章咱们把中断子系统的骨架搭起来了。核心就两点:

  • 分层设计:硬件层、通用层、驱动层,各司其职。
  • irq_desc:每个中断号的“户口本”,记录了中断的所有信息。

下一章咱们会深入 request_irq 的实现,看看驱动是怎么注册中断处理函数的。到时候你会更清楚 irq_desc 里的 action 链表是怎么工作的。

一句话总结:中断子系统就是靠 irq_desc 这个核心数据结构,把硬件差异封装起来,让驱动开发者只需要关心业务逻辑。


专注资料整理