4、中断子系统回顾:Linux中断处理流程、上半部与下半部机制、中断描述符与irq_chip结构体

在深入实时性补丁的中断线程化改造之前,必须彻底理解标准Linux内核的中断子系统。中断是内核响应硬件事件的核心机制,其设计直接决定了系统的实时响应能力。本章将系统回顾中断处理的全流程、上半部(Top Half)与下半部(Bottom Half)的协作模式,以及中断描述符与irq_chip结构体的底层抽象。

4.1 Linux中断处理流程全景

当硬件设备产生中断信号时,CPU会暂停当前执行流,跳转到预先设置的中断向量表(IDT)对应的入口函数。整个处理流程可以分为以下几个关键阶段:

  1. 硬件中断信号到达:外设通过中断请求线(IRQ)向中断控制器(如APIC、GIC)发送信号。
  2. 中断控制器仲裁:中断控制器根据优先级、屏蔽状态决定是否向CPU核心发送中断。
  3. CPU响应与现场保存:CPU保存当前上下文(寄存器、程序计数器),关闭本地中断,跳转到通用中断入口entry_irq。
  4. 中断描述符查找:内核根据中断号查找对应的irq_desc结构体。
  5. 执行中断控制器回调:调用irq_chip中的irq_ack、irq_mask等硬件操作。
  6. 执行上半部处理函数:调用注册在action链表中的handler,通常只做最紧急的硬件操作。
  7. 触发下半部:上半部通过tasklet、workqueue或softirq调度下半部处理。
  8. 恢复现场与中断返回:恢复CPU上下文,开启中断,返回被中断的进程。
关键理解:中断处理的核心矛盾在于“快速响应硬件”与“避免阻塞高优先级任务”之间的平衡。上半部在关中断或屏蔽中断线的情况下执行,必须极短;下半部在开中断环境下执行,可以完成大部分数据处理。
Linux中断处理流程全景图 Linux中断处理流程全景图 硬件中断信号 中断控制器仲裁 CPU响应与现场保存 查找中断描述符 irq_chip硬件操作 执行上半部handler 触发下半部处理 ⚠ 关键约束 上半部在关中断/屏蔽中断线 下执行,必须极短且不可睡眠。 下半部在开中断下执行。 💡 实时性痛点 上半部关中断时间过长会 导致高优先级任务错过 截止时间(Deadline)。

4.2 上半部(Top Half)与下半部(Bottom Half)机制

Linux内核将中断处理拆分为两个阶段,以平衡实时性与吞吐量。上半部是“硬中断上下文”,下半部是“软中断上下文”或“进程上下文”。

4.2.1 上半部(Top Half)

  • 执行环境:在中断上下文中运行,当前中断线被屏蔽(或CPU本地中断关闭)。
  • 职责:确认中断、清除硬件中断标志、拷贝硬件寄存器数据到内存、调度下半部。
  • 限制:不能睡眠、不能调用可能阻塞的函数、不能持有互斥锁。
  • 注册方式:通过request_irq()或request_threaded_irq()注册handler。

4.2.2 下半部(Bottom Half)

下半部机制经历了多次演进,目前主流实现包括:

机制 执行上下文 可睡眠 并发度 典型使用场景
Softirq 软中断上下文 否 同类型可多CPU并行 网络收发包、定时器
Tasklet 软中断上下文 否 同类型串行 块设备、简单驱动
Workqueue 进程上下文 是 可并行、可阻塞 复杂数据处理、I/O操作
Threaded IRQ 内核线程 是 按中断线隔离 实时性补丁强制使用
实时性视角:标准内核中,Softirq和Tasklet仍然在“软中断上下文”中执行,优先级高于普通进程,且不可被抢占。这会导致高优先级实时任务被延迟。实时性补丁(PREEMPT_RT)的核心改造之一就是将Softirq和Tasklet全部迁移到内核线程中执行,使其可被抢占。
上半部与下半部协作关系图 上半部与下半部协作关系图 上半部 (Top Half) 硬中断上下文 · 关中断 · 不可睡眠 调度下半部 Softirq 软中断上下文 · 可并行 Tasklet 软中断上下文 · 串行 Workqueue 进程上下文 · 可睡眠 🔧 PREEMPT_RT改造方向 将Softirq和Tasklet全部迁移到内核线程中执行,使其可被高优先级实时任务抢占。

4.3 中断描述符(irq_desc)结构体

每个中断号在Linux内核中对应一个irq_desc结构体,它是中断子系统的核心管理单元。该结构体包含了中断的所有状态信息、处理函数链表、硬件操作回调以及统计信息。

// include/linux/irqdesc.h (简化结构)
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 cpumask          *percpu_enabled;
#ifdef CONFIG_PREEMPT_RT
    struct task_struct      *thread;        // 中断线程化后的内核线程
#endif
    struct proc_dir_entry   *dir;
    const char              *name;
} ____cacheline_internodealigned_in_smp;

关键字段说明:

  • handle_irq:中断流控处理函数,如handle_level_irq、handle_edge_irq,由中断控制器类型决定。
  • action:链表头,挂载所有注册到该中断号的irqaction结构体(支持共享中断)。
  • thread:在PREEMPT_RT内核中,每个中断描述符可以关联一个内核线程,用于执行中断线程化处理。
  • kstat_irqs:每个CPU的中断发生次数统计。
注意:在标准内核中,irq_desc的action链表中的handler直接在上半部执行。而在PREEMPT_RT内核中,如果中断被线程化,handler会在关联的内核线程上下文中执行,从而避免关中断时间过长。

4.4 irq_chip结构体:硬件操作抽象层

irq_chip是内核与中断控制器硬件之间的接口抽象。每个中断控制器(如IO-APIC、GIC、ARMv7M NVIC)都需要实现一组回调函数,封装在irq_chip结构体中。

// include/linux/irq.h (简化结构)
struct irq_chip {
    const char      *name;           // 芯片名称
    unsigned int    (*irq_startup)(struct irq_data *data);
    void            (*irq_shutdown)(struct irq_data *data);
    void            (*irq_enable)(struct irq_data *data);
    void            (*irq_disable)(struct irq_data *data);
    void            (*irq_ack)(struct irq_data *data);       // 应答中断
    void            (*irq_mask)(struct irq_data *data);      // 屏蔽中断线
    void            (*irq_unmask)(struct irq_data *data);    // 取消屏蔽
    void            (*irq_eoi)(struct irq_data *data);       // 中断结束
    int             (*irq_set_affinity)(struct irq_data *data,
                                        const struct cpumask *dest,
                                        bool force);
    int             (*irq_retrigger)(struct irq_data *data);
    int             (*irq_set_type)(struct irq_data *data,
                                    unsigned int flow_type);
    void            (*irq_ack)(struct irq_data *data);
    // ... 其他可选回调
};

核心回调函数作用:

回调函数 触发时机 典型操作
irq_ack 中断处理开始时 向中断控制器发送应答信号,允许继续接收同类型中断
irq_mask 需要屏蔽中断线时 设置中断控制器屏蔽寄存器,禁止该中断线传递信号
irq_unmask 中断处理结束或重新使能时 清除屏蔽寄存器,允许中断线再次传递信号
irq_eoi 中断处理完成时(EOI) 通知中断控制器中断处理完毕,通常用于电平触发中断
irq_set_affinity 设置中断亲和性时 将中断路由到指定CPU核心
irq_set_type 配置中断触发方式时 设置电平触发、边沿触发、高电平有效等
irq_desc与irq_chip结构关系图 irq_desc 与 irq_chip 结构关系图 struct irq_desc handle_irq: 流控处理函数指针 action: struct irqaction 链表头 irq_data: 包含 irq_chip 指针 thread: 中断线程 (PREEMPT_RT) kstat_irqs: 中断统计 指向 struct irq_chip irq_ack: 应答中断 irq_mask: 屏蔽中断线 irq_unmask: 取消屏蔽 irq_eoi: 中断结束 irq_set_affinity: 设置CPU亲和性 irq_set_type: 设置触发方式 💡 irq_chip 是硬件抽象层,驱动开发者只需实现这些回调,内核中断子系统即可统一管理不同中断控制器。

4.5 中断处理流程中的实时性瓶颈

标准Linux内核的中断处理机制在实时性方面存在以下关键瓶颈:

  1. 上半部关中断时间不可控:复杂驱动的上半部可能执行较长时间,导致高优先级任务无法被调度。
  2. Softirq/Tasklet优先级过高:软中断上下文优先级高于所有进程,即使实时任务也无法抢占。
  3. 中断嵌套导致优先级反转:低优先级中断的上半部可能屏蔽高优先级中断。
  4. 中断亲和性配置不当:中断集中在某个CPU核心,导致该核心负载过高。
PREEMPT_RT的解决思路:将中断处理上半部中非紧急部分、以及所有下半部(Softirq/Tasklet)迁移到可被抢占的内核线程中执行。中断线程化后,实时任务可以通过优先级调度机制抢占中断处理线程,从而保证实时性。