引言

中断是硬件与操作系统之间最基础、最关键的通信机制。从键盘敲击到网卡收到数据包,从定时器节拍到 NVMe 完成队列通知,所有异步事件最终都通过中断(或 MSI/MSI-X)通知 CPU。然而,在中断从硬件引脚传递到 CPU 执行处理函数的漫长旅程中,Linux 内核构建了一套极其复杂而又精巧的抽象体系:irq_domain 框架。

irq_domain 诞生于 ARM 多 SoC 时代。在 x86 平台上,中断控制器架构相对统一(8259 PIC → APIC → IOAPIC),中断号与硬件引脚的对应关系相对固定。但在 ARM 世界中,每个 SoC 都有独特的 GIC(Generic Interrupt Controller)版本,还有 GPIO 中断控制器、级联中断控制器、平台级中断控制器等多种硬件拓扑。irq_domain 框架正是为了管理这种多样性而设计的——它将硬件中断号映射为 Linux 的虚拟 IRQ 号(virq),并提供统一的注册、查找、释放接口。

本文将从中断控制器硬件演进开始,深入到 irq_domain 的数据结构、irq_chip 的回调机制、中断描述符(irqdesc)的生命周期管理,再到 GICv3 的内部细节、Device Tree 中断映射解析,最后以一个真实的 ARM 服务器中断拓扑为例,展示如何调试和调优中断分配。

一、中断控制器硬件演进史

要理解 irq_domain,首先需要了解它解决了什么问题。让我们回顾中断控制器硬件从 8259 到 GICv3 的演进过程。

1.1 可编程中断控制器(PIC)时代

早期 x86 系统使用 Intel 8259 PIC,支持 8 个中断级(IRQ0-IRQ7),通过级联可以获得 15 个中断(IRQ0-IRQ15,其中一个用于级联)。8259 是一个纯硬件设备,通过 I/O 端口编程来控制中断屏蔽、优先级和 EOI(End of Interrupt)发送。

设备 ──→ 8259 PIC ──→ CPU INTR引脚
              (级联)
         8259 PIC#1 ←→ 8259 PIC#2

1.2 APIC 时代

随着 SMP(对称多处理)和多核架构的出现,8259 PIC 无法满足需求。Intel 引入了 APIC(Advanced Programmable Interrupt Controller)架构,包含:

  • Local APIC:每个 CPU 核心内置一个,接收本地中断(定时器、温度传感器、性能监控)和 IPI(处理器间中断)
  • IOAPIC:外部芯片,连接外部设备中断线到 Local APIC
  • Message Signaled Interrupts (MSI):PCI 设备通过写特定内存地址来触发中断,无需物理引脚
设备 ──→ IOAPIC ──→ LAPIC (CPU0)
                  ├──→ LAPIC (CPU1)
                  └──→ LAPIC (CPU2)  [通过 APIC Bus 或现代中的 x2APIC]

1.3 ARM GIC 家族

ARM 阵营的中断控制器演进更加丰富:

  • VIC(Vector Interrupt Controller):早期 ARM(如 PL190),支持 32 个中断源,IRQ/FIQ 两种类型
  • GICv1:首个标准化的 ARM 中断控制器,支持有限数量的 SPI(Shared Peripheral Interrupt)
  • GICv2:广泛使用的版本,最多 480 个中断源,支持 CPU interface 和 Distributor 分离架构
  • GICv3:革命性架构变更,引入了 ITS(Interrupt Translation Service)、LPI(Locality-specific Peripheral Interrupt)、affinity 路由(取代 target CPU list),最大中断号扩展到 1020+ LPI
  • GICv4:在 v3 基础上增加 vLPI 支持,实现直接注入虚拟中断(无需 Hypervisor 介入),加速虚拟机中断处理
GICv3 拓扑:
硬件中断源 (SPI/PPI/SGI/LPI)
    │
    ▼
Distributor (GICD) ←── 配置使能、优先级分组、目标 CPU
    │
    ▼
Redistributor (GICR) ←── 每个 CPU 一个,处理 PPI/SGI/LPI
    │
    ▼
CPU Interface (ICC_* 寄存器) ←── 信号给 CPU (IRQ/FIQ)

ITS (GICv4) ←── 将设备事件ID转换为 LPI 编号
    │
    ↑
PCI MSI 消息或平台级设备

二、irq_domain 核心数据结构

irq_domain 是 Linux 内核管理硬件中断号到 Linux IRQ 号映射的核心框架。它实现了多级级联映射,支持线性表、基数树、层级映射等多种映射策略。

2.1 irq_domain 结构体

struct irq_domain {
    struct list_head        link;           // 全局链表节点
    const char              *name;          // 域名称(如 "GICv3")
    const struct irq_domain_ops *ops;       // 操作函数表
    void                    *host_data;     // irq_chip 或私有数据
    unsigned int            flags;          // 标志位(如 IRQ_BUS_PCI_MSI)
    unsigned int            mapcount;       // 已映射中断数量
    
    /* 映射相关 */
    struct irq_fwspec      *fwnode;         // 固件节点(DT/ACPI)
    enum irq_domain_bus_token bus_token;    // 总线类型(DOMAIN_BUS_WIRED 等)
    
    union {
        struct irq_domain_hwirq_data linear;  // 线性映射:hwirq → virq 数组
        struct irq_domain_xlate *xlate;       // 层级映射:通过 ops->translate() 动态计算
        unsigned int            radix_max;    // 基数树最大值
    };
    
    struct radix_tree_root  revmap_data;    // 反向映射:virq → hwirq
    struct mutex            bus_token_mutex;  
};

2.2 irq_domain_ops 操作函数

struct irq_domain_ops {
    int  (*match)(struct irq_domain *d, struct device_node *node,
                  enum irq_domain_bus_token bus_token);
    int  (*select)(struct irq_domain *d, struct irq_fwspec *_fwspec,
                   enum irq_domain_bus_token bus_token);
    int  (*map)(struct irq_domain *d, unsigned int virq, irq_hw_irq_t hw);
    void (*unmap)(struct irq_domain *d, unsigned int virq);
    int  (*translate)(struct irq_domain *d, struct irq_fwspec *fwspec,
                      unsigned int *out_hwirq, unsigned int *out_type);
    int  (*alloc)(struct irq_domain *d, unsigned int virq,
                  unsigned int nr_irqs, void *arg);
    void (*free)(struct irq_domain *d, unsigned int virq, unsigned int nr_irqs);
};

各回调的含义:

  • match/select:判断该 domain 是否适用于某设备/固件节点(在 irq_find_domain() 中使用)
  • translate:将固件中断描述符(intspec/intspec_size)翻译为 hwirq + trigger type,用于级联域
  • map:在 domain 中实际建立映射,分配 irqdesc、调用 irq_chip 设置硬件
  • unmap:撤销映射,释放 irqdesc
  • alloc/free(可选):替代默认的 irq_domain_alloc_irqs,支持支持断言/释放的自定义行为

2.3 三种映射策略

irq_domain 根据硬件拓扑和性能需求,支持三种映射存储方式:

映射方式数据结构适用场景查找复杂度
线性表 (Linear)预先分配的 virq 数组硬件中断号固定、数量少(如 GPIO 控制器)O(1) 直接索引
基数树 (Radix Tree)radix_tree_root 动态插入中断号稀疏、范围大(如 GICv3 SPI)O(log n)
层级映射 (Hierarchical)无固定表,通过 ops->translate() 链式计算级联拓扑(GPIO 作为 GIC 的下游)O(级数)

2.4 irq_fwspec — 固件中断描述符

struct irq_fwspec {
    struct fwnode_handle    *fwnode;   // 指向 DT 节点或 ACPI obj
    int                     param_count;  // 参数个数(通常1-3个)
    u32                     param[IRQ_DOMAIN_IRQ_SPEC_PARAMS];  // 硬件中断号数组
};
// Device Tree 典型规格:// <0x0 intspec_0 intspec_1 type>
// GICv3 示例:<0 IRQ_TYPE_LEVEL_HIGH> → SPI 0
// GPIO 控制器示例:<0 17 IRQ_TYPE_EDGE_BOTH> → bank0, hwirq=17

三、irq_chip 回调与中断硬件抽象

irq_chip 是对中断控制器硬件操作的抽象层。一个 irq_chip 实例封装了对特定硬件中断控制器的操作函数,这些操作在 CPU 响应中断、中断控制器配置时被调用。

3.1 irq_chip 核心字段

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);        // 读 IAR 应答中断
    void            (*irq_mask)(struct irq_data *data);       // 写 ISPENDR/ICDICERn 屏蔽
    void            (*irq_unmask)(struct irq_data *data);     // 写 ISPENDR/ICDISPRn 取消屏蔽
    void            (*irq_eoi)(struct irq_data *data);        // 写 ICCEOIRn 发送 EOI
    int             (*irq_set_affinity)(struct irq_data *data,
                                        const struct cpumask *dest,
                                        bool force);          // 设置目标 CPU
    int             (*irq_retrigger)(struct irq_data *data);  // 重新触发 IPI
    int             (*irq_set_type)(struct irq_data *data,
                                    unsigned int flow_type);  // 设置边沿/电平触发
    int             (*irq_set_wake)(struct irq_data *data,
                                    unsigned int on);         // 唤醒源配置(PM)
    // ... 
};

3.2 GICv3 irq_chip 实现片段

// drivers/irqchip/irq-gic-v3.c
static struct irq_chip gic_chip = {
    .name           = "GICv3",
    .irq_mask       = gic_mask_irq,
    .irq_unmask     = gic_unmask_irq,
    .irq_eoi        = gic_eoi_irq,
    .irq_set_affinity = gic_set_affinity,
    .irq_set_type   = gic_set_type,
    .irq_set_wake   = gic_set_wake,
    .ipi_send_mask  = gic_ipi_send_mask,
    .irq_nmi_setup  = gic_irq_nmi_setup,
    .irq_nmi_teardown = gic_irq_nmi_teardown,
    // flags: IRQCHIP_SET_TYPE_MASKED | IRQCHIP_MASK_ON_SUSPEND | ...
};

static int __init gic_init_bases(void)
{
    // ... 初始化 Distributor 和 Redistributor
    
    gic_domain = irq_domain_create_hierarchy(parent_domain, 0, gic_irqs,
                                              gic_node, &gic_irq_domain_ops,
                                              NULL);
    irq_domain_update_bus_token(gic_domain, DOMAIN_BUS_WIRED);
    
    // 为每个 SPI 预映射 IRQ descriptor
    for (i = 0; i < SPI_RANGE; i++) {
        virq = irq_create_fwspec_mapping(&fwspec);
    }
}

3.3 中断处理流程与时序

了解 irq_chip 回调的调用时机,有助于理解硬件与内核的交互:

[中断触发]
设备产生中断信号 ──→ GIC 拉高 IRQ 引脚 ──→ CPU 进入 IRQ 模式
                                │
[硬件自动]
R14_irq 保存返回地址
SPSR_irq 保存 CPSR
切换到 IRQ 栈(内核栈 per-CPU IRQ stack)
                                │
[软件入口: arch/arm64/kernel/irqs.S]
__irq_svc → handle_arch_irq(gic_handle_irq)
    │
    ▼
read  ICC_IAR1_EL1 → 获取 hwirq
    │
    ▼
generic_handle_irq(desc[hwirq])
    │
    ├──→ irq_enter() [RCS 通知 in_interrupt()]
    │
    ├──→ 调用 action->handler(dev) [上半部: NAPI、tasklet 调度]
    │
    ├──→ irq_eoi() [→ gic_eoi_irq → write ICC_EOIR1_EL1]
    │
    └──→ irq_exit() [调度 softirq、检查 PREEMPT]

四、中断描述符(irqdesc)生命周期

irqdesc 是中断在内核中的完整表示,包含状态、irq_chip 指针、处理函数列表、等待者队列等。

4.1 irq_desc 结构(简化)

struct irq_desc {
    struct irq_common_data  irq_common_data;  // irq_chip, handler_data, msk
    struct irq_data         irq_data;        // hwirq, domain, chip 指针
    unsigned int            *kstat_irqs;     // /proc/interrupts 统计
    irq_flow_handler_t      handle_irq;      // 流控处理函数(handle_level_irq 等)
    struct irqaction        *action;         // 已注册的 irqaction 链表
    unsigned int            status_use_accessors;  // IRQ_LEVEL, IRQ_NOPROBE 等
    unsigned int            depth;           // 闭锁深度(disable depth)
    unsigned int            wake_depth;      // 唤醒闭锁深度
    struct module           *owner;          // request_irq 时传入的 THIS_MODULE
    const char              *name;           // 中断名称(来自 request_irq 的 devname)
};

4.2 中断流程控函数

Linux 内置两种中断流控处理函数,分别对应电平触发和边沿触发中断:

  • handle_level_irq():电平触发中断的默认处理。入口时 mask 中断 + ack(防止同一中断反复触发);出口时 unmask
  • handle_edge_irq():边沿触发中断的默认处理。入口 ack 中断;在 handler 执行期间如果再次检测到该中断 pending,则调用 handle_irq_event() 直到 pending 清除
  • handle_fasteoi_irq():GIC 等现代控制器使用。入口 EOI(无需 mask),依赖硬件自动防重入。中断完成后,通过 irq_chip->irq_eoi() 发送 EOI。这是 GIC 系列中断控制器最常用的处理函数
  • handle_percpu_irq():PPI(Private Peripheral Interrupt)/ SGI 专用。每个 CPU 独立状态,无需自旋锁保护链表

4.3 request_irq 与内部流程

int request_threaded_irq(unsigned int irq, irq_handler_t handler,
                         irq_handler_t thread_fn, unsigned long flags,
                         const char *name, void *dev)
{
    // 1. 分配 irqaction
    // 2. 检查是否可共享 (IRQF_SHARED)
    // 3. 将 action 挂入 desc->action 链表
    // 4. 如果是第一个 action → irq_activate → irq_chip->irq_startup()
    // 5. 如果 thread_fn 存在 → 创建 irq_thread (kthread)
    //    上半部 handler 返回 IRQ_WAKE_THREAD 时触发下半部
}

五、irq_domain 创建与映射实战

这里我们展示一个 GPIO 控制器作为中断控制器创建 irq_domain 的典型流程。

5.1 GPIO 中断控制器 irq_domain 创建

static int gpioctl_probe(struct platform_device *pdev)
{
    struct gpio_controller *ctl = devm_kzalloc(&pdev->dev, sizeof(*ctl), GFP_KERNEL);
    
    // 1. 创建 irq_domain
    ctl->domain = irq_domain_add_linear(pdev->dev.of_node, NUM_GPIOS,
                                         &gpio_irq_domain_ops, ctl);
    ctl->irq_chip = &gpio_irq_chip;  // 该 GPIO 控制器的 irq_chip
    
    // 2. 注册父级域(级联处理)
    irq_set_chained_irqchip(parent_irq, &parent_chip, gpioctl_handler);
    
    return 0;
}

static int gpio_irq_domain_map(struct irq_domain *d, unsigned int virq,
                               irq_hw_irq_t hw)
{
    struct gpio_controller *ctl = d->host_data;
    
    // 设置 irq_desc 的处理函数(电平触发用 handle_level_irq)
    irq_set_chip_handler_name_locked(&ctl->irq_chip, virq, 
                                     handle_level_irq, NULL, "level");
    
    // 绑定 irq_chip 指针到 irq_data
    irq_set_chip_data(virq, ctl);
    
    // 映射硬件寄存器(配置 GPIO 中断检测条件)
    gpioctl_enable_hwirq(ctl, hw);
    return 0;
}

5.2 Device Tree 中断映射解析

当设备驱动在 probe 中调用 platform_get_irq() 时,内核通过 interrupt-parent 属性链进行映射:

内核解析流程:platform_get_irq() → of_irq_get() → irq_create_of_mapping() → domain->ops->translate() → 转换为 hwirq + type → irq_find_mapping() 或 irq_create_fwspec_mapping() → 如果不存在则分配新的 irqdesc。

5.3 irq_find_mapping 与 irq_create_fwspec_mapping


// 已映射时的快速路径
unsigned int irq_find_mapping(struct irq_domain *domain, irq_hw_irq_t hwirq)
{
    // 线性表: 直接索引 table[hwirq]
    // 基数树: radix_tree_lookup(&domain->revmap_data, hwirq)
    // 层级映射: 返回 -ENODEV(要求先调用 translate)
    return virq;
}

// 首次映射时分配 irqdesc
unsigned int irq_create_fwspec_mapping(struct irq_fwspec *fwspec)
{
    // 1. irq_find_domain() 按 firmware node 找到 domain
    // 2. domain->ops->translate() → 得到 hwirq + type
    // 3. irq_find_mapping() 检查是否已存在(可能在固件解析时预注册)
    // 4. 如果不存在 → irq_domain_alloc_irqs() → domain->ops->map()
    //    map 内部: 
    //      a) internal_desc = alloc_irq_desc(virq)
    //      b) irq_domain_set_mapping(),插入映射表
    //      c) irq_set_chip_and_handler_name() + irq_set_chip_data()
    return virq;
}

六、GICv3 内部实现深度解析

GICv3 是高性能 ARM 服务器的主流中断控制器,理解其内部结构对系统调优至关重要。

6.1 SPI/PPI/SGI/LPI 分类

类型编号范围使用场景CPU 可见性
SGI (Software Generated Interrupt)0-15核间 IPI(调度、RCU、内存屏障)One-to-many
PPI (Private Peripheral Interrupt)16-31Local timer, PMU, 安全中断One-to-one
SPI (Shared Peripheral Interrupt)32-1019PCI NIC, Platform devices, UARTOne-to-many
LPI (Locality-specific Peripheral Interrupt)8192+PCI MSI/MSI-X, ITS 转换One-to-one (affinity-based)

6.2 Redistributor 与 LPI 表


Redistributor (GICR) 每个 CPU core 一个:
├── LPI Pending Table (物理内存中)
│   └── BITMAP of 8192+ LPI (status: pending/active)
│
├── LPI Configuration Table (物理内存中)
│   └── 每个 LPI 的 enable/priority/residency 配置
│
├── LPI 翻译逻辑
│   └── 设备写 ITS Doorbell → ITS 查表 → 设置 LPI pending bit
│       → CPU 读到 ICC_IAR1_EL1 时返回 LPI hwirq
│
└── vLPI (GICv4, 虚拟机使用)
    └── vLPI Pending Table 由 ITS 直接更新 → VM exit 减少

6.3 ICC_* 系统寄存器

GICv3 的 CPU 接口通过 ARM64 系统寄存器访问(无需 MMIO):

// 读取当前 IAR (Interrupt Acknowledge Register)
u32 hwirq = read_sysreg(ICC_IAR1_EL1);  // 获取 hwirq 号

// 写入 EOI (Priority Drop / Deactivate)
write_sysreg(hwirq, ICC_EOIR1_EL1);      // 仅 EOI
write_sysreg(hwirq, ICC_DIR_EL1);       // Deactivate (PPIs/LPI 不需要 EOI)

// 配置优先级 threshold
write_sysreg(priority_mask, ICC_PMR_EL1);  // 高于 threshold 的中断才投递

// 设置目标处理器 (IPI)
u64 target_list = ...;
write_sysreg(target_list + (affinity << 32), ICC_SGI1R_EL1);

七、级联中断拓扑——以 Jetson AGX Orin 为例

让我们看一个真实 SoC 的多级中断控制器拓扑,理解级联 irq_domain 的运作方式:


Jetson AGX Orin 中断拓扑:
┌─────────────┐     ┌──────────────┐     ┌─────────┐
│  PCIe NIC   │────→│   GICv3      │────→│  CPU×12 │
│  (MSI-X)    │     │  (Root)      │     │  (A78AE)│
└─────────────┘     └──────┬───────┘     └─────────┘
                           │
          ┌────────────────┼────────────────┐
          ▼                ▼                ▼
   ┌────────────┐  ┌──────────────┐  ┌─────────────┐
   │ GPIO ctrl  │  │ PMIC 中断    │  │ Thermal zone│
   │ (Tegra AON)│  │ (I2C/SPI)    │  │ 传感器中断  │
   └─────┬──────┘  └──────────────┘  └─────────────┘
         │
    ┌────┴────┐
    ▼         ▼
 设备1   设备2

在 Device Tree 中,GPIO 控制器的 interrupt-parent 指向 GICv3,同时声明自己也是一个 interrupt-controller。当一个 GPIO 设备的 interrupts 引用 GPIO 控制器,而 interrupt-parent 又是 GICv3 时,内核会自动建立级联:GPIO 控制器的 hwirq 被映射到 GICv3 的 hwirq 上,形成 irq_domain 链式结构。

八、中断亲和性与性能调优

在多核系统中,合理分配中断到不同 CPU 对吞吐量和延迟有决定性影响。

8.1 irq_set_affinity 机制

// 内核空间: 设置中断亲和性
int irq_set_affinity(unsigned int irq, const struct cpumask *cpumask)
{
    struct irq_desc *desc = irq_to_desc(irq);
    struct irq_chip *chip = desc->irq_data.chip;
    
    return chip->irq_set_affinity(&desc->irq_data, cpumask, false);
}

// GICv3 实现: 将目标 CPU 写入 ICC_* 寄存器
// 对于 SPI: 通过 ICDIPTRn (Interrupt Processor Targets Register)
// 对于 LPI: 通过 ITS device table + translation table

8.2 irqbalance 与手动分配

用户态通过 /proc/irq/<N>/smp_affinity 文件调整中断绑定:

# 将中断 149 绑定到 CPU 0-3
echo f > /proc/irq/149/smp_affinity  # 位掩码 0b1111

# 查看当前中断分布
cat /proc/interrupts | grep -i eth0

# 监控 irqbalance 服务决策
journalctl -u irqbalance --since "10 min ago"

8.3 RPS/XPS (Receive/Transmit Packet Steering)

对于网卡等高性能设备,中断亲和性是第一步。第二步是通过 RPS 和 XPS 充分利用多队列网卡的能力:

# 启用 RPS: 将 NAPI 调度负载分散到多个 CPU
echo ff > /sys/class/net/eth0/queues/rx-0/rps_cpus

# 启用 XPS: 发送队列绑定到特定 CPU
echo 4 > /sys/class/net/eth0/queues/tx-0/xps_cpus

# 中断合并 (Interrupt Coalescing)
ethtool -C eth0 rx-usecs 50 tx-usecs 50

8.4 NAPI (New API) 与自适应中断聚合

在现代高速网卡驱动中,NAPI 与中断配合形成"轮询+中断"混合模式:


[正常模式] 中断到达 → 关闭中断 → 调度 NAPI poll()
                                  → 处理 RX/TX 直到收完或预算耗尽
                                  → 若收完: 退出 NAPI + 使能中断
                                  → 有包仍在 RX FIFO: 继续 poll

[高负载] 中断被有效替代 → CPU 在 poll() 循环中轮询
                          → 延迟更低、吞吐更高
                          → 代价: 用户态 CPU 占用略升
                          
[irq_moderation] 硬件在 FIFO 达到阈值或超时后才发中断
                → 权衡:减少 CPU 中断开销 vs 增加包处理延迟

九、中断调试与排错

9.1 /proc/interrupts 输出深度解读

$ cat /proc/interrupts
           CPU0       CPU1       CPU2       CPU3
  149:    23456      0          0          0     GICv3 149  eth0-rx0
  150:    0          18732      0          0     GICv3 150  eth0-rx1
  327:    45         52         48         51     GICv3 327  arm_arch_timer
  400:    0          0          0          123    Tegra GPIO  button

输出含义(从左到右):virq 号 → 各 CPU 中断计数 → 控制器名称+hwirq → 中断名称。

9.2 trace_printk / ftrace 中断追踪

# 使用 ftrace 追踪中断处理时间
echo 1 > /sys/kernel/debug/tracing/events/irq/enable
echo 1 > /sys/kernel/debug/tracing/events/irq_handler_entry/enable
echo 1 > /sys/kernel/debug/tracing/tracing_on

# 观察中断延迟
cat /sys/kernel/debug/tracing/trace | grep -E "irq|handle"

# 使用 latencytop 工具查看中断延迟直方图
perf trace -e irq:* --duration 10000

9.3 irqdomain 调试命令

# 查看所有 irq_domain
cat /sys/kernel/debug/irq_domain_mapping

# 查看单个中断详细信息
cat /proc/irq/149/spurious  # 误触发计数
cat /proc/irq/149/affinity_hint

# 检查中断是否均衡分布
watch -n 1 "cat /proc/interrupts | grep eth0"

十、虚拟化环境下的中断处理

在 KVM/QEMU 虚拟机中,中断路径更加复杂,涉及虚拟中断注入和 VM exit 优化。

10.1 虚拟中断注入流程


[宿主机]
物理设备中断 → GICv3 hwirq → qemu-kvm ioctl(KVM_IRQFD)
                                │
                                ▼
[KVM kernel]
irqfd 收到信号 → kvm_set_irq()
                 │
                 ├── vGIC emulation: kvm_vgic_inject_irq()
                 │   → 设置 vGIC LR (List Register) pending bit
                 │   → 若 CPU 被调度 → 入口时 unblock VC
                 │
                 └── GICv4 直通 (无 VM exit):
                     ITS Doorbell → GICR 设置 vLPI pending bit → 直接投递
                                ▼
[虚拟机 (Guest)]
虚拟中断注入 → VMCS update → 入口时 cpu_has_vhe() ? 无 exit : VM_EXIT_ACK_IRQ
                                │
                                ▼
Guest OS 读 GICH_LR → 获取虚拟 hwirq → 处理

10.2 VFIO 直通中断(MSI/MSI-X)

在 DPDK 等高性能场景中,网卡通过 VFIO 直通给虚拟机:

// VFIO 中断注册流程
struct vfio_irq_set *irq_set;
ioctl(device_fd, VFIO_DEVICE_SET_IRQS, &irq_set);
// → MSI-X vector 通过 VFIO → VM KVM → ioeventfd → 用户态 DPDK poll

// MSI-X 在中断路径上的优势:
// 1. 每个 RX/TX 队列有独立的 MSI-X 中断,可在不同 CPU 均匀分布
// 2. 无需物理中断线,避免级联延迟
// 3. 通过 IRQFD 或 VFIO 直通实现零中断 VM exit (GICv4)

十一、常见中断问题排错指南

以下是开发和运维中常见的中断问题及其排查方法:

11.1 "nobody cared" 中断误触发

当 GIC 收到一个中断但没有任何 action 的 handler 可以处理时,内核会打印 "IRQ N: nobody cared"。常见原因:

  • 设备在未使能时产生了边沿中断(GPIO 上下电时的 glitch)
  • request_irq 时 flags 中缺少 IRQF_SHARED 但与其他设备共享中断
  • 共享中断中某个 handler 返回 IRQ_HANDLED 导致后续 handler 不再调用

11.2 中断风暴 (Interrupt Storm)

某个中断源源不断触发,导致 CPU 被完全占用:

# 临时 irq_disable 该中断
echo 1 > /proc/irq/149/disable

# 检查设备状态,是否有 fatal error 导致反复触发
dmesg | tail -100 | grep -i error

# 最终手段: 屏蔽 GIC Distributor 级别的中断
# echo hwirq > /sys/kernel/debug/gic-mask  (需内核 debugfs 支持)

11.3 IRQ handler 延迟异常

中断 handler 执行时间过长有两种常见原因:handler 中错误地做了重操作(如内存分配、互斥锁),或中断被 disable 期间新的中断导致丢失:

# 用 ftrace 查看 handler 执行耗时
echo function > /sys/kernel/debug/tracing/current_tracer
echo "handle_irq*" > /sys/kernel/debug/tracing/set_ftrace_filter

# 长时间中断堵死 RCU stall
dmesg | grep "RCU detected"

11.4 SoftIRQ 瓶颈

当中断产生的 softirq 被阻塞在同一个 CPU 上无法及时运行时,会导致网络吞吐下降或延迟抖动:

# 查看 softirq 处理统计
cat /proc/softirqs

# NET_RX 增长异常快但中断不多 → single-queue 网卡瓶颈
# 解决: 启用多队列 + RPS + irqbalance

十二、irq_domain API 速查表

场景API说明
创建级联域(如 GPIO ctrl)irq_domain_add_linear() / irq_domain_create_hierarchy()需要传入父 domain
创建顶层域 (如 GIC)irq_domain_add_hierarchy(parent=0)parent 为 NULL 表示顶层
DT/ACPI 映射irq_create_of_mapping() / irq_create_fwspec_mapping()驱动 probe 时按需映射
手动映射(预注册)irq_create_mapping()在 domain 创建时批量预映射 SPI 范围的 virq
查找映射irq_find_mapping()hwirq → virq
释放映射irq_dispose_mapping()设备移除时撤销
获取 irq_datairq_get_irq_data(virq)内部使用,获取 chip/hwirq
设置中断处理irq_set_chip_and_handler_name()指定 handle_level_irq 等流控函数
级联中断irq_set_chained_irqchip()在父中断 handler 级联处理

十三、Linux 6.x 新特性:irqchip 的演进

Linux 6.x 内核在 irq subsystem 方面引入了若干重要改进:

  • irqchip 的 core irqdomain 改进:更统一的 GICv3 ITS 初始化流程、更好的 GICv4 直通中断支持
  • irq_poll 机制增强:减少低优先级中断的 softirq 排队延迟
  • RISC-V AIA(Advanced Interrupt Architecture):RISC-V 引入的新中断控制器架构,通过 IMSIC(Incoming MSI Controller)实现与 MSI 兼容的中断投递
  • irq_resend 优化:减少了错误路径下的中断重发延迟
  • 中断 per-cpu 变量访问优化:对频繁读取中断统计的场景做了无锁化改进

十四、总结

irq_domain 和 irq_chip 是 Linux 内核中断子系统的核心抽象。这套框架将千差万别的中断控制器硬件统一到同一套 virq 映射、composure 和流控体系下,使得设备驱动程序可以完全独立于底层硬件进行中断注册和处理。

理解这套框架对系统级开发者至关重要:当你需要为新的 SoC 移植 Linux、调试中断丢失问题、优化网卡多队列性能、或者在 DPDK/VFIO 路径上追求零中断延迟时,irq_domain 的层级映射、GICv3 的 LPI 表亲和性路由、以及中断与 softirq/NAPI 的协作机制都是必须深入理解的核心概念。

随着 RISC-V AIA、CXL 中断虚拟化等新技术的发展,Linux 中断子系统仍在持续演进。但其核心设计哲学——硬件抽象层 + 统一 virq 号空间 + 层级域映射——已经证明是一个经久耐用且极具扩展性的架构。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部