# 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 系统上进行高性能驱动开发和系统调优的基础。

发表评论 取消回复