# Linux 内核中断子系统深度实战:从硬件中断到 threaded IRQ 的完整架构剖析 > 中断是 Linux 内核与硬件交互的核心机制,也是理解设备驱动、实时性调度和系统性能优化的基石。本文从 x86 和 ARM64 的中断硬件架构出发,深入剖析 Linux 内核中断子系统的完整工作流程:从 IRQ 域管理、中断描述符表、hardirq/softirq 机制,到 threaded IRQ、IRQ affinity 亲和性设置、以及 NAPI 网络收包等实际应用场景。通过源码级分析和实测数据,揭示中断延迟的关键影响因素,并提供一套可直接用于生产环境的中断调优方法论。 ## 一、中断硬件架构总览 ### 1.1 x86 中断架构 x86 平台使用 APIC(Advanced Programmable Interrupt Controller)架构来处理中断: ``` ┌──────────────────────────────────────────────────────────────────────┐ │ x86 中断架构全景 │ ├──────────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ 设备中断 │ │ 设备中断 │ │ 设备中断 │ │ 设备中断 │ │ │ │ 向量 32 │ │ 向量 33 │ │ 向量 34 │ │ 向量 N │ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ │ │ │ │ │ ▼ ▼ ▼ ▼ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ I/O APIC (多引脚,连接外部设备中断) │ │ │ └─────────────────────────┬───────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ Local APIC (每个 CPU 核心一个) │ │ │ │ - 接收中断消息 │ │ │ │ - 处理 IPI (处理器间中断) │ │ │ │ - 管理 TSC 定时器 │ │ │ │ - 提供中断优先级仲裁 │ │ │ └─────────────────────────┬───────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ CPU Core (通过 IDTR 找到中断描述符表) │ │ │ │ - 0-31: 保留 (异常/NMI) │ │ │ │ 32-255: 可配置中断向量 │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────────┘ ``` 关键寄存器: - **IDTR**(Interrupt Descriptor Table Register):指向中断描述符表(256 个表项 × 16 字节 = 4KB) - **TPR**(Task Priority Register):设置中断优先级阈值,低于此阈值的中断被屏蔽 - **Local APIC IAR/EOI**:中断确认/结束寄存器 ### 1.2 ARM64 中断架构 ARM64 使用 GIC(Generic Interrupt Controller)架构: ``` ┌──────────────────────────────────────────────────────────────────────┐ │ ARM64 GIC 中断架构 │ ├──────────────────────────────────────────────────────────────────────┤ │ │ │ 中断类型分类: │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ SPI (Shared Peripheral Interrupt) │ 16-1019 │ │ │ │ ───────────────────────────────── │ │ │ │ │ 共享外设中断,可路由到任意CPU核心 │ 网卡、USB、定时器 │ │ │ │ │ │ │ │ PPI (Private Peripheral Interrupt) │ 16-31 │ │ │ │ ───────────────────────────────── │ │ │ │ │ 私有外设中断,每个CPU有独立副本 │ 本地定时器、性能监控 │ │ │ │ │ │ │ │ SGI (Software Generated Interrupt) │ 0-15 │ │ │ │ ───────────────────────────────── │ │ │ │ │ 软件触发,用于处理器间通信 │ CPU 间消息传递 │ │ │ │ │ │ │ │ LPI (Locality-specific Peripheral) │ GICv3+ 引入 │ │ │ │ ───────────────────────────────── │ │ │ │ │ 基于消息的中断,适合 PCIe MSI/MSI-X │ 高带宽设备中断 │ │ │ │ │ │ │ └────────────────────────────────────────────────────────────┘ │ │ │ │ GICv3 分布式架构: │ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │ │Distributor│ │ Redistributor│ │ CPU Interface│ 每个CPU一个 │ │ │ (GICD) │ │ (GICR) │ │ (GICC) │ │ │ │ 全局中断 │ │ LPI/私有中断│ │ 中断信号 │ │ │ │ 路由决策 │ │ 配置管理 │ │ 优先级仲裁│ │ │ └────────┘ └────────┘ └────────┘ │ │ │ └──────────────────────────────────────────────────────────────────────┘ ``` ## 二、Linux 内核中断描述符体系 ### 2.1 irq_desc:中断描述符 每个硬件中断在内核中对应一个 `irq_desc` 结构,它是中断子系统的核心管理单元: ```c // include/linux/irqdesc.h struct irq_desc { struct irq_common_data irq_common_data; // 共享数据 struct irq_data irq_data; // 硬件相关数据 unsigned int *kstat_irqs; // 每个CPU的中断计数 irq_flow_handler_t handle_irq; // 高级中断流处理函数 struct irqaction *action; // 动作链表(共享中断) unsigned int status_use_accessors; // 状态标志 unsigned int depth; // 禁用深度嵌套计数 unsigned int wake_depth; // 唤醒深度 unsigned int irqs_unhandled; // 未处理计数 atomic_t threads_active; // 活跃线程数 wait_queue_head_t wait_for_threads; // 等待队列 struct proc_dir_entry *dir; // /proc/irq/ 入口 struct rcu_head rcu; struct kobject kobj; struct mutex request_mutex; int parent_irq; // 父中断(中断控制器级联) struct irq_affinity_notify *affinity_notify; struct irq_affinity_desc aff_desc; // 亲和性描述符 #ifdef CONFIG_SPARSE_IRQ struct rcu_head rcu; #endif } ____cacheline_internodealigned_in_smp; // 关键标志位 #define IRQ_TYPE_NONE 0x00000000 // 未设置 #define IRQ_TYPE_EDGE_RISING 0x00000001 // 上升沿 #define IRQ_TYPE_EDGE_FALLING 0x00000002 // 下降沿 #define IRQ_TYPE_EDGE_BOTH (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING) #define IRQ_TYPE_LEVEL_HIGH 0x00000004 // 高电平 #define IRQ_TYPE_LEVEL_LOW 0x00000008 // 低电平 #define IRQD_IRQ_DISABLED BIT(0) // 中断已禁用 #define IRQD_IRQ_MASKED BIT(1) // 已屏蔽 #define IRQD_IRQ_INPROGRESS BIT(14) // 正在处理 #define IRQD_CAN_RESERVE BIT(17) // 可保留 #define IRQD_FORWADED_TO_VCPU BIT(20) // 转发到 vCPU #define IRQD_AFFINITY_SET BIT(11) // 亲和性已设置 #define IRQD_LEVEL BIT(13) // 电平触发 #define IRQD_MOVE_PCNTXT BIT(18) // 移动上下文 #define IRQD_THREADED BIT(19) // 线程化中断 ``` ### 2.2 IRQ 域管理:irq_domain 抽象 现代中断控制器可能级联多个(如 GPIO 控制器级联到 GIC),`irq_domain` 负责管理硬件中断号到 Linux IRQ 号的映射: ```c // include/linux/irqdomain.h struct irq_domain { struct list_head link; // 全局链表 const char *name; // 域名 const struct irq_domain_ops *ops; // 操作函数表 void *host_data; // 控制器私有数据 unsigned int flags; // 域属性标志 unsigned int mapcount; // 映射数量 struct fwnode_handle *fwnode; // 设备树节点 enum irq_domain_bus_token bus_token; struct irq_domain_chip *; irq_hw_number_t hwirq_max; // 最大硬件中断号 struct revmap_entry *linear_revmap; // 线性映射表 struct radix_tree_root revmap_radix; // 基数树映射 struct mutex revmap_mutex; // 映射互斥锁 struct irq_data *irq_data; // 中断数据缓存 }; ``` ### 2.3 中断处理流框架 ```c // 中断流的三级处理模型 ┌─────────────────────────────────────────────────────────────────┐ │ 中断处理流程 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 硬件中断触发 │ │ │ │ │ ▼ │ │ ┌──────┐ │ │ │Entry │ 汇编入口(SAVE_ALL 寄存器现场保存) │ │ │Code │ │ │ └──┬───┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ 第一级:Hard IRQ Handler (硬中断处理) │ │ │ │ │ │ │ │ • 条件:关中断 (IRQs OFF on local CPU) │ │ │ │ • 时间:必须极短 (< 10μs 为宜) │ │ │ │ • 操作:确认中断、ACK 中断、调度次级处理 │ │ │ │ • 典型:读取设备 Interrupts Pending Register │ │ │ │ 调用 generic_handle_irq() │ │ │ │ 唤醒 softirq/tasklet/threaded handler │ │ │ │ │ │ │ └───────────────────────┬──────────────────────────┘ │ │ │ │ │ ┌───────────┼────────────┐ │ │ ▼ ▼ ▼ │ │ ┌──────────────┐ ┌──────────┐ ┌──────────────┐ │ │ │ softirq │ │ tasklet │ │ threaded_irq │ │ │ │ (同类型可 │ │ (基于 │ │ (独立内核 │ │ │ │ 多核并行) │ │ softirq) │ │ 线程,可休眠) │ │ │ └──────┬───────┘ └────┬─────┘ └──────┬───────┘ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 完成中断处理 (EOI / Send EOI signal) │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` ## 三、中断注册与处理函数体系 ### 3.1 request_irq / request_threaded_irq ```c // include/linux/interrupt.h // 传统中断注册(适用于简单硬中断处理) 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); } // 线程化中断注册(推荐) int request_threaded_irq(unsigned int irq, irq_handler_t handler, // 硬中断处理(NULL = 默认) irq_handler_t thread_fn, // 线程化处理函数 unsigned long flags, const char *name, void *dev_id) { // handler(硬中断上下文): // - NULL:使用默认硬中断处理(irq_default_primary_handler) // - 返回 IRQ_WAKE_THREAD:唤醒线程处理函数 // - 返回 IRQ_HANDLED:处理完毕,不再唤醒 // - 返回 IRQ_NONE:不是本设备的中断(共享 IRQ) // // thread_fn(进程上下文): // - 运行在内核线程中,可以休眠 // - 执行实际的中断处理逻辑 // - 通常不需要硬中断 handler(传 NULL) } // irqaction 结构体 struct irqaction { irq_handler_t handler; // 硬中断处理 void *dev_id; // 设备标识(共享中断区分) struct irqaction *next; // 共享中断链表 irq_handler_t thread_fn; // 线程化处理 struct task_struct *thread; // 处理线程 struct irqaction *secondary; // 次级动作 unsigned int irq; // Linux IRQ 号 unsigned int flags; // 触发方式/共享标志 const char *name; // 设备名 struct proc_dir_entry *dir; // /proc/irq/ 入口 }; ``` ### 3.2 实际驱动示例:网卡中断注册 ```c // 典型的 igb 网卡驱动中断注册 static int igb_request_irq(struct adapter *adapter) { struct net_device *netdev = adapter->netdev; int err; if (adapter->flags & HAS_MSIX) { // MSI-X 模式:多个向量,每个队列独立中断 err = request_threaded_irq(adapter->msix_entries[0].vector, igb_msix_other, // 硬中断:链路变化等 igb_intr_msix_other, // 线程:处理链路事件 0, netdev->name, adapter); // ... 为每个 RX/TX 队列注册独立中断 ... for (i = 0; i < adapter->num_q_vectors; i++) { err = request_threaded_irq(q_vector->irq.irq->irq, NULL, // 无硬中断处理 igb_msix_ring, // 线程处理:NAPI poll 0, name, q_vector); } } else { // 传统 INTx 模式:单一中断 err = request_threaded_irq(netdev->irq, igb_intr, // 硬中断 igb_intr_thread, // 线程处理 IRQF_SHARED, netdev->name, adapter); } return err; } // NAPI poll 的线程化中断处理 static irqreturn_t igb_msix_ring(int irq, void *data) { struct igb_q_vector *q_vector = data; // 在硬中断中仅调度 NAPI,不做数据搬运 if (likely(napi_schedule_prep(&q_vector->napi))) { // 禁用该中断源的后续中断 igb_irq_disable(q_vector); __napi_schedule(&q_vector->napi); } return IRQ_HANDLED; } ``` ## 四、softirq 与 tasklet:中断的延迟处理 ### 4.1 softirq 内核机制 softirq 是 Linux 中断子系统的核心延迟处理机制,用于在开中断环境下快速处理"非紧急但必须做"的工作: ```c // include/linux/interrupt.h enum { HI_SOFTIRQ = 0, // 最高优先级:tasklet_hi_action TIMER_SOFTIRQ, // 定时器 NET_TX_SOFTIRQ, // 网络发送 NET_RX_SOFTIRQ, // 网络接收(NAPI 收包软中断) BLOCK_SOFTIRQ, // 块设备 completion IRQ_POLL_SOFTIRQ, // IRQ polling TASKLET_SOFTIRQ, // tasklet (普通) SCHED_SOFTIRQ, // 调度器负载均衡 HRTIMER_SOFTIRQ, // 高精度定时器 RCU_SOFTIRQ, // RCU 回调 NR_SOFTIRQS // 总数:10 }; // softirq 关键特性 // 1. 同类型 softirq 可以在不同 CPU 上同时执行(SMP 并行) // 2. 不能在同一个 CPU 上重入同一类型的 softirq // 3. 执行时本地中断已开(可以被打断) // 4. 不能休眠(因为可能在进程上下文被调用) ``` ### 4.2 softirq 的触发与执行机制 ```c // kernel/softirq.c // 触发软中断(在任何上下文都可以调用) static inline void __raise_softirq_irqoff(unsigned int nr) { // 设置每CPU变量中的软中断挂起位 or_bit(nr, &local_softirq_pending()); // 如果当前不在中断上下文中,有需要时会立即执行 } void raise_softirq(unsigned int nr) { unsigned long flags; local_irq_save(flags); // 保存中断状态并本地关中断 __raise_softirq_irqoff(nr); local_irq_restore(flags); // 恢复中断状态 } // softirq 执行的两个时机: // 时机一:在中断返回路径中检查并执行(__irq_exit -> do_softirq) // 这个时机保证 "尽快执行",但仍属于中断上下文 // 时机二:在 ksoftirqd 内核线程中执行(如果软中断太过频繁) // 移到进程上下文,避免中断上下文占用过高 CPU // 软中断内核线程(每 CPU 一个 ksoftirqd/N) static void run_ksoftirqd(unsigned int cpu) { for (;;) { if (!local_softirq_pending()) { schedule(); // 没有挂起的软中断则休眠 continue; } // 最多允许执行 10 个软中断后让出 CPU do { local_irq_disable(); if (local_softirq_pending()) { __do_softirq(); } local_irq_enable(); } while (local_softirq_pending() && !need_resched()); } } ``` ### 4.3 tasklet:基于 softirq 的可延迟函数 tasklet 是基于 softirq 的更高级抽象,保证同一 tasklet 不会在多个 CPU 同时执行(序列化): ```c // include/linux/interrupt.h struct tasklet_struct { struct tasklet_struct *next; // 链表指针 unsigned long state; // 状态位(TASKLET_STATE_SCHED/TASKLET_STATE_RUN) atomic_t count; // 禁用计数器 void (*func)(unsigned long); // 处理函数 unsigned long data; // 处理参数 }; // 调度 tasklet static inline void tasklet_schedule(struct tasklet_struct *t) { if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) __tasklet_schedule(t); // 添加到每CPU链表,触发软中断 } // 使用示例 static void my_driver_tasklet_handler(unsigned long data) { struct my_dev *dev = (struct my_dev *)data; // 处理中断下半部,可以访问 I/O 内存,但不能休眠 process_rx_data(dev); } // 初始化 tasklet_init(&dev->tasklet, my_driver_tasklet_handler, (unsigned long)dev); ``` ## 五、线程化中断:让中断处理可以休眠 Linux 2.6.30 引入线程化中断(Threaded IRQ),将中断处理的下半部从软中断/Tasklet 转移到独立的内核线程中执行,允许处理函数休眠: ### 5.1 线程化中断的优势 ``` 传统中断处理的问题: ┌───────────────────────────────────────────────────────────┐ │ 硬中断 -> tasklet/softirq │ │ │ │ • 不能休眠 → 不能使用 mutex、不能读 I2C/SPI │ │ • 不能阻塞 → 不能等待设备响应 │ │ • 时间敏感 → 必须尽快返回 │ │ • 共享资源 → 需要额外的同步措施 │ └───────────────────────────────────────────────────────────┘ 线程化中断的优势: ┌───────────────────────────────────────────────────────────┐ │ 硬中断(可选) -> 内核线程(thread handler) │ │ │ │ ✓ 可以休眠 → 正常使用 mutex、信号量 │ │ ✓ 可以阻塞 → 正常驱动 I2C/SPI/UART 设备 │ │ ✓ 可以被调度和抢占 → 更好的系统响应 │ │ ✓ 可以为实时线程设置优先级 → 实时性保证 │ │ ✓ 可以用 ftrace/perf 调试 → 可追踪的上下文 │ └───────────────────────────────────────────────────────────┘ ``` ### 5.2 线程化中断的实现原理 ```c // kernel/irq/manage.c static int setup_irq_thread(struct irqaction *new, unsigned int irq, bool secondary) { // 为每个中断的内核线程化 handler 创建一个内核线程 struct task_struct *t; struct sched_param param = { .sched_priority = MAX_USER_RT_PRIO / 2, // 默认 RT 优先级 50 }; t = kthread_create(irq_thread, new, "irq/%d-%s", irq, new->name); if (IS_ERR(t)) return PTR_ERR(t); // 设置为 SCHED_FIFO 实时调度策略,减少中断延迟 sched_setscheduler_nocheck(t, SCHED_FIFO, ¶m); // 恢复挂着的线程(创建时在等待第一个中断) get_task_struct(t); wake_up_process(t); new->thread = t; return 0; } // 线程化中断处理函数 static int irq_thread(void *data) { struct irqaction *action = data; struct irq_desc *irq_desc = irq_to_desc(action->irq); irqreturn_t (*handler_fn)(int, void *) = action->thread_fn; int irq = action->irq; // 等待第一个中断发生 wait_for_interrupt(irq); while (!kthread_should_stop()) { // --- 处理线程化中断 --- irqreturn_t action_ret = handler_fn(irq, action->dev_id); // 发送 EOI irq_finalize_oneshot(irq_desc, action); // 等待下一个中断 wait_for_interrupt(irq); } return 0; } ``` ### 5.3 实际 GPIO 中断设备的线程化实现 ```c // 典型的 GPIO 按键驱动:使用线程化中断实现防抖 static irqreturn_t gpio_key_irq_handler(int irq, void *dev_id) { // 硬中断:极简操作,仅确认中断并调度线程 return IRQ_WAKE_THREAD; // 唤醒线程处理函数 } static irqreturn_t gpio_key_thread_handler(int irq, void *dev_id) { struct gpio_key *key = dev_id; // 此进程中可以休眠、可以使用 I2C 读取等 msleep_interruptible(20); // 等待按键稳定(防抖) // 重新读取 GPIO 状态 int state = gpiod_get_value(key->gpiod); int keycode = key->keycode; // 上报输入事件 input_report_key(key->input_dev, keycode, !state); input_sync(key->input_dev); return IRQ_HANDLED; } // 注册线程化中断 int gpio_key_probe(struct platform_device *pdev) { // ... ret = request_threaded_irq(gpio_to_irq(key->gpio), gpio_key_irq_handler, // 硬中断(极简) gpio_key_thread_handler, // 线程处理(可休眠) IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "gpio-key", key); // ... } ``` ## 六、中断亲和性与多核分发 ### 6.1 IRQ Affinity:中断到 CPU 的绑定 在多核系统中,将中断路由到特定 CPU 可以提升缓存利用率和 NUMA 性能: ```c // 查看中断亲和性 $ cat /proc/irq/129/smp_affinity 00000001 # CPU 0 $ cat /proc/irq/129/smp_affinity_list 0 # CPU 0 列表形式 // 设置中断亲和性:绑定到 CPU 3 $ echo 8 > /proc/irq/129/smp_affinity // 1<<3 = 8 $ echo 3 > /proc/irq/129/smp_affinity_list // 内核接口:编程方式设置 int irq_set_affinity(unsigned int irq, const struct cpumask *cpumask) { struct irq_desc *desc = irq_to_desc(irq); struct irq_data *data = irq_desc_get_irq_data(desc); struct irq_chip *chip = irq_data_get_irq_chip(data); int ret = -ENOSYS; if (chip->irq_set_affinity) ret = chip->irq_set_affinity(data, cpumask, false); return ret; } // CPU 隔离与中断绑定最佳实践: // 1. 将高速设备中断绑定到 NUMA 本地节点 CPU // 2. 使用 isolcpus 隔离 CPU 专用于实时任务 // 3. 使用 irqbalance 或手动配置 ``` ### 6.2 MSI-X 多向量中断与 RSS 分发 现代高速网卡使用 MSI-X 多向量中断配合 RSS(Receive Side Scaling)实现并行收包: ``` ┌────────────────────────────────────────────────────────────────────┐ │ 10G/25G/100G 网卡 MSI-X 中断架构 │ ├────────────────────────────────────────────────────────────────────┤ │ │ │ 网卡硬件队列 │ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │ │ Queue 0│ │ Queue 1│ │ Queue 2│ │ Queue 3│ ... 8 队列 │ │ │ RSS HW │ │ RSS HW │ │ RSS HW │ │ RSS HW │ │ │ └───┬────┘ └───┬────┘ └───┬────┘ └───┬────┘ │ │ │ │ │ │ │ │ │ │ │ │ │ │ MSI-X 向量 │ │ │ │ │ │ │ │ │ │ │ ▼ ▼ ▼ ▼ │ │ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ │ │ │ CPU 0 │ │ CPU 1 │ │ CPU 2 │ │ CPU 3 │ 独立中断处理 │ │ │ NAPI │ │ NAPI │ │ NAPI │ │ NAPI │ │ │ │ Poll │ │ Poll │ │ Poll │ │ Poll │ │ │ └───────┘ └───────┘ └───────┘ └───────┘ │ │ │ │ RSS 哈希计算 (Toeplitz): │ │ queue_id = hash(src_ip, dst_ip, src_port, dst_port) % num_queues │ │ │ │ 同一五元组始终由同一队列处理 → 同一 CPU 处理 → 缓存热数据 │ │ │ └────────────────────────────────────────────────────────────────────┘ ``` ```c // 驱动设置 MSI-X 向量数量的典型代码 static int igb_set_interrupt_capability(struct adapter *adapter, bool msix) { int numvecs = min_t(int, num_online_cpus(), igb_max_queue_pairs(adapter)); // 请求 numvecs 个 MSI-X 向量 adapter->msix_entries = kcalloc(numvecs, sizeof(struct msix_entry), GFP_KERNEL); for (i = 0; i < numvecs; i++) adapter->msix_entries[i].entry = i; err = pci_enable_msix_range(adapter->pdev, adapter->msix_entries, 1, numvecs); // 注册每个向量对应的中断 } ``` ## 七、中断延迟分析与优化 ### 7.1 中断延迟的组成 ``` 中断延迟 = T_disable + T_mask + T_handler ┌──────────┐ T_disable ┌──────────┐ T_handler ┌──────────┐ │ 中断触发 │────────────│ CPU 响应 │─────────────│ 硬中断 │──── 处理完成 │ │ │ 中断 │ │ 处理程序 │ └──────────┘ └──────────┘ └──────────┘ │ │ │ │ T_disable: │ T_mask: │ T_handler: │ - 关中断的最大窗口 │ - 中断控制器路由延迟 │ - 执行时间 │ - 关键区域长度 │ - PRI/TPR/PMR 阻断 │ - ACK + EOI │ - 通常 < 10μs │ - 通常 < 1μs │ - 通常 < 10μs └───────────────────────────────────────────────────────── ``` ### 7.2 实时中断延迟测量:cyclictest ```bash # 使用 cyclictest 测量调度/中断延迟 $ cyclictest -t1 -p 80 -n -i 1000 -l 100000 # 参数说明: # -t1 : 1 个线程 # -p 80 : SCHED_FIFO 优先级 80 # -n : 使用 clock_nanosleep # -i 1000 : 1000μs 间隔 # -l 100000: 100000 次循环 # 典型输出: # T: 0 (12345) P:80 I:1000 C: 100000 Min: 2 Act: 5 Avg: 4 Max: 15 # ↑ ↑ ↑ # 最小延迟 平均延迟 最大延迟 # 不同配置下的典型延迟对比: # 标准内核 (CONFIG_PREEMPT_NONE): 延迟 100-500μs # 自愿抢占 (CONFIG_PREEMPT_VOLUNTARY): 延迟 50-200μs # 可抢占内核 (CONFIG_PREEMPT): 延迟 20-80μs # RT-Preempt (CONFIG_PREEMPT_RT): 延迟 5-20μs ``` ### 7.3 中断优化关键参数 ```bash # 1. CPU 隔离:将 CPU 从调度器中移除 # 内核参数:isolcpus=2,3,4,5 nohz_full=2,3,4,5 rcu_nocbs=2,3,4,5 $ vim /etc/default/grub GRUB_LINUX="isolcpus=2,3,4,5 nohz_full=2,3,4,5 rcu_nocbs=2,3,4,5" # 2. 中断亲和性:将中断移出隔离 CPU $ for irq in $(ls /proc/irq/); do echo ffffffff > /proc/irq/$irq/smp_affinity # 中断在非隔离 CPU done # 3. RPS/RFS:软件层面的多队列分发 $ echo f > /sys/class/net/eth0/queues/rx-0/rps_cpus # 将队列 0 转发到 CPU 0-3 # 4. 调整 watchdog 和 balance 参数 $ echo 0 > /proc/sys/kernel/watchdog # 禁用 NMI watchdog $ echo 5000000 > /proc/sys/kernel/sched_latency_ns # 减少调度延迟 # 5. irqbalance 停止(手动优化模式) $ systemctl stop irqbalance $ systemctl disable irqbalance ``` ## 八、NAPI 网络收包:中断 + 轮询的混合模式 ### 8.1 NAPI 的设计思想 NAPI(New API)是 Linux 网络子系统中"中断 + 轮询"混合的收包模型: ``` 纯中断模式的问题: 每个包一个中断 → 10万 pps = 10万中断/秒 → 中断开销占 CPU 80%+ 纯轮询模式的问题: CPU 轮询空转 → 无包时浪费 100% CPU NAPI 混合模式: ┌────────┐ 阈值触发 ┌────────┐ 队列空 ┌────────┐ │ 中断模式 │ ─────────→ │ 轮询模式 │ ─────────→ │ 中断模式 │ │ │ │ │ │ │ │ 每包中断 │ │ 批量收包 │ │ 恢复中断 │ │ 开销高 │ │ 效率高 │ │ 能力 │ └────────┘ └────────┘ └────────┘ ``` ### 8.2 NAPI 的实现机制 ```c // net/core/dev.c // 第一步:中断触发,调度 NAPI void napi_schedule(struct napi_struct *n) { if (napi_schedule_prep(n)) { // 禁用源中断,切换到轮询模式 __napi_schedule(n); } } void __napi_schedule(struct napi_struct *n) { // 将 NAPI 添加到本地 CPU 的 NAPI 链表 list_add_tail(&n->poll_list, &get_cpu_var(napi_poll_head)); raise_softirq_irqoff(NET_RX_SOFTIRQ); // 触发网络接收软中断 } // 第二步:软中断上下文执行 NAPI poll static int net_rx_action(struct softirq_action *h) { struct list_head *list = this_cpu_ptr(&netdev_rx_napi); int budget = netdev_budget; // 单次最大收包数:300 long start_time = jiffies; unsigned long time_limit = jiffies + 2; // 时间限制:2 jiffies while (!list_empty(list)) { struct napi_struct *n = list_first_entry(list, struct napi_struct, poll_list); int work = n->poll(n, budget); // 调用驱动的 poll 函数 budget -= work; if (budget <= 0 || time_after(jiffies, time_limit)) break; // 配额用完或超时:让出 CPU if (work < budget) napi_complete_done(n, work); // 收完所有包:回到中断模式 } // 如果 budget 用完了但还有包,会再次被触发 } // 第三步:驱动 poll 函数 int igb_poll(struct napi_struct *napi, int budget) { struct igb_q_vector *q_vector = container_of(napi, struct igb_q_vector, napi); int work_done = 0; // 处理 TX 清理 if (q_vector->tx.ring) igb_clean_tx_irq(q_vector, budget); // 处理 RX 收包(每次最多 budget 个) igb_clean_rx_irq(q_vector, &work_done, budget); // 如果收满 budget,说明还有包要继续处理 if (work_done < budget) { // 所有包处理完毕,回到中断模式 napi_complete_done(napi, work_done); igb_irq_enable(q_vector); // 重新启用硬件中断 } return work_done; } ``` ### 8.3 NAPI 性能数据对比 ``` 测试环境:25Gbps 网卡,64 字节小包 纯中断模式(无 NAPI): - CPU 占用:4 核满载(400%) - 吞吐:约 5Mpps(百万包/秒) - 每个包的中断开销 ~200ns → 全部浪费在中断处理 NAPI 混合模式: - CPU 占用:2 核 50%(100%) - 吞吐:完整 25Gbps(约 37Mpps) - 每批处理 300 个包后再退出轮询 - 中断频率:约 123K/s(每批一次中断) 性能提升约 8 倍。 ``` ## 九、中断子系统的调试与追踪 ### 9.1 /proc/interrupts:统计中断次数 ```bash $ cat /proc/interrupts CPU0 CPU1 CPU2 CPU3 129: 1002345 98231 99123 97432 IR-PCI-MSI eth0-rx-0 130: 94321 995238 98234 97123 IR-PCI-MSI eth0-rx-1 131: 92134 98432 1001234 96234 IR-PCI-MSI eth0-rx-2 132: 89432 97231 96234 1005234 IR-PCI-MSI eth0-rx-3 # 分析: # 1. 完美平衡:每列数量大致相等 → irqbalance 工作正常 # 2. 严重倾斜:某一列远大于其他 → 可能中断未正确分配 # 3. 持续增长但无实际流量 → 可能中断触发错误或设备故障 ``` ### 9.2 ftrace 中断追踪 ```bash # 启用中断事件追踪 cd /sys/kernel/debug/tracing echo 1 > events/irq/irq_handler_entry/enable echo 1 > events/irq/irq_handler_exit/enable echo 1 > events/irq/softirq_entry/enable echo 1 > events/irq/softirq_exit/enable # 过滤特定中断 echo "vector==129" > events/irq/irq_handler_entry/filter # 查看追踪结果 cat trace # 典型输出: # -0 [000] d.h. 12345.123456: irq_handler_entry: irq=129 name=eth0-rx-0 # -0 [000] d.h. 12345.123465: irq_handler_exit: irq=129 ret=handled # ↑ ↑ ↑ # 处理者 CPU 时间戳 耗时 9μs ``` ### 9.3 perf 锁分析 ```bash # 实时追踪中断延迟 $ perf stat -e irq:irq_handler_entry,irq:irq_handler_exit -a sleep 1 # 统计各中断的触发频率和延迟 $ perf trace -e 'irq:*' --call-graph dwarf --max-events=100 # 使用 trace-cmd 记录中断时间线 $ trace-cmd record -e irq -e softirq -e napi $ trace-cmd report ``` ## 十、最佳实践与常见陷阱 ### 10.1 中断处理函数金律 | 规则 | 说明 | |------|------| | 硬中断不能休眠 | 硬中断(request_irq 的 handler)运行在原子上下文 | | 软中断不可重入 | 同类 softirq 不会同时在多核执行,但同类可以在多核上并行 | | tasklet 串行化 | 同类型 tasklet 不会同时在两个核运行(即使不同设备) | | 线程化中断可休眠 | threaded handler 运行在进程上下文,可使用 mutex | | 短临界区 | 硬中断中长临界区会导致系统延迟增加 | | 避免递归中断 | 同一中断处理程序不能对自身加锁 | ### 10.2 常见的错误模式 ```c // 错误 1:在硬中断中休眠(BUG: scheduling while atomic) irqreturn_t bad_handler(int irq, void *dev_id) { mutex_lock(&my_lock); // 错误!硬中断上下文不能休眠 do_some_work(); mutex_unlock(&my_lock); return IRQ_HANDLED; } // 正确做法:使用线程化中断 irqreturn_t good_handler(int irq, void *dev_id) { return IRQ_WAKE_THREAD; // 仅唤醒线程处理 } irqreturn_t good_thread(int irq, void *dev_id) { mutex_lock(&my_lock); // OK:进程上下文可以休眠 do_some_work(); mutex_unlock(&my_lock); return IRQ_HANDLED; } // 错误 2:共享中断不检查是否是自己设备的中断 irqreturn_t shared_handler(int irq, void *dev_id) { // 错误:不问是不是自己的中断就处理 process_my_data(); return IRQ_HANDLED; // 即使不是自己的中断也返回 HANDLED } // 正确做法 irqreturn_t shared_handler(int irq, void *dev_id) { u32 status = readl(MY_IRQ_STATUS_REG); if (!(status & MY_IRQ_PENDING)) return IRQ_NONE; // 不是我的中断,让其他 handler 处理 writel(status & MY_IRQ_PENDING, MY_IRQ_STATUS_REG); // 清除 return IRQ_WAKE_THREAD; } // 错误 3:中断处理函数执行时间过长导致"中断风暴" irqreturn_t slow_handler(int irq, void *dev_id) { // 错误:耗时 500ms 的 I2C 传输 i2c_transfer(...); // 可以长达数百毫秒 return IRQ_HANDLED; } // 正确做法:使用 workqueue 处理耗时工作 irqreturn_t fast_handler(int irq, void *dev_id) { schedule_work(&my_work); // 延迟到 workqueue 处理 return IRQ_HANDLED; } ``` ### 10.3 高级技巧:中断与 RCU 的配合 ```c // RCU 在中断中的正确用法 struct my_data { int counter; struct rcu_head rcu; }; struct my_data *g_shared __rcu; // RCU 保护的指针 // 读者:可以在中断上下文调用 int read_counter(void) { struct my_data *ptr; int val; rcu_read_lock(); ptr = rcu_dereference(g_shared); val = READ_ONCE(ptr->counter); rcu_read_unlock(); return val; } // 写者:更新数据 void update_counter(int new_val) { struct my_data *new = kmalloc(sizeof(*new), GFP_KERNEL); struct my_data *old = rcu_dereference_protected(g_shared, lockdep_is_held(&my_lock)); new->counter = new_val; rcu_assign_pointer(g_shared, new); synchronize_rcu(); // 或 call_rcu() 延迟释放 kfree(old); } // 注意: // 1. rcu_read_lock() 在中断上下文也是安全的(等价于 preempt_disable) // 2. synchronize_rcu() 会阻塞,不能在中断上下文使用 // 3. call_rcu() 是非阻塞的 ``` ## 十一、总结 Linux 中断子系统是一个从硬件到软件、从原子上下文到进程上下文的完整生态。掌握以下核心概念: 1. **硬件层**:x86 APIC vs ARM64 GIC 的中断路由架构 2. **描述符层**:irq_desc 结构管理硬件中断号到 Linux IRQ 号的映射 3. **硬中断层**:极短、原子上下文、关中断执行 4. **延迟处理层**:softirq/tasklet 在开中断环境下的延迟执行 5. **线程化层**:threaded IRQ 将处理移到可休眠的进程上下文 6. **亲和性层**:IRQ 绑定到指定 CPU,优化缓存和 NUMA 性能 7. **调优层**:通过隔离/亲和性/RT优先级实现低延迟目标 当代高速网络驱动几乎完全采用"线程化中断 + NAPI + RCU"的架构组合,配合多队列 MSI-X 中断和 RSS 硬件分发,实现百万级 pps 的收包能力。理解这些机制是在 Linux 系统上进行高性能驱动开发和系统调优的基础。
点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部