Linux 内核的中断处理是整个系统响应外部事件的基石。理解从中断信号到达 CPU 到用户态进程恢复执行的完整路径,是每一个系统级开发者的必修课。
1.1 中断的硬件视角
在现代 x86-64 架构中,中断控制器经历了 PIC → APIC → x2APIC 的演进。Local APIC 接收来自 IO-APIC 或 MSI/MSI-X 的中断消息,将中断向量号写入 ISR(In-Service Register),并通过 LAPIC 的 LVT(Local Vector Timer)监控本地中断源。当 CPU 执行 iret 指令后,APIC 通过 EOI(End of Interrupt)确认完成中断服务,优先级仲裁逻辑随即允许同级或低级中断投递。
ARM64 架构中,中断路由由 GIC(Generic Interrupt Controller)处理。GICv3 引入 SPI、PPI、LPI 三种中断类型,其中 LPI(Locality-specific Peripheral Interrupt)通过 ITS(Interrupt Translation Service)实现 MSI 的动态映射,支撑大规模 PCIe 设备中断分配。
1.2 中断入口汇编层
x86-664 的 IRQ 入口位于 arch/x86/entry/entry_64.S,宏 INTERRUPT_ENTRY 负责寄存器压栈(错误码、CS、EIP、EFLAGS、SS、RSP),然后通过 do_IRQ() 进入 C 代码层。对于无错误码的中断向量(如 0x20-0xFF),内核压入一个伪错误码保持栈帧统一。
do_IRQ() 的核心逻辑是:
- 获取中断向量号
vector和对应的irq_desc - 递增
desc->kstat_irqs中断计数器 - 调用
desc->handle_irq(&desc)(通常为handle_level_irq或handle_fasteoi_irq) - 在 handle_irq 回调中执行 handler 链表中的 action 函数
1.3 IDT 与中断栈
IDT(Interrupt Descriptor Table)在系统启动时由 idt_setup_traps() 初始化,后续通过 idt_setup_early_handler() 安装中断门。每个中断门包含目标代码段选择子和偏移地址。Linux 使用 IST(Interrupt Stack Table)机制,为 NMI、Double Fault 等关键中断分配独立栈,防止栈溢出时上下文损坏。
Per-CPU 中断栈(irq_stack_ptr)在 SMP 初始化时分配(alloc_irq_stack()),大小为 IRQ_STACK_SIZE(通常 16KB)。中断发生时 CPU 通过 TSS(Task State Segment)切换到中断栈。
二、Top Half 与 Bottom Half 机制
中断处理的核心约束是越快越好——在关中断或中断屏蔽期间,CPU 无法响应其他中断,导致系统延迟增加。Linux 将中断处理分为 Top Half(立即执行的关键操作)和 Bottom Half(延迟执行的非关键操作)。
2.1 Softirq
Softirq 是 Linux 最古老的 Bottom Half 机制,定义在 include/linux/interrupt.h 中的枚举 softirq_action:
| 编号 | 名称 | 触发场景 |
|---|---|---|
| HI_SOFTIRQ(0) | tasklet_hi_action | 高优先级 tasklet |
| TIMER_SOFTIRQ(1) | run_timer_list | 定时器到期回调 |
| NET_TX_SOFTIRQ(2) | net_tx_action | 网络数据包发送 |
| NET_RX_SOFTIRQ(3) | net_rx_action | 网络数据包接收(NAPI) |
| BLOCK_SOFTIRQ(4) | blk_done_softirq | 块设备完成通知 |
| IRQ_POLL_SOFTIRQ(5) | irq_poll_action | 高性能轮询 |
| TASKLET_SOFTIRQ(6) | tasklet_action | 普通 tasklet |
| SCHED_SOFTIRQ(7) | run_rebalance_domains | 调度负载均衡 |
| HRTIMER_SOFTIRQ(8) | run_hrtimer_list | 高精度定时器 |
| RCU_SOFTIRQ(9) | rcu_process_callbacks | RCU 回调处理 |
Softirq 的触发通过 raise_softirq() 设置 CPU 的 softirq_pending 位图,在以下三个时机检查并执行:
local_bh_enable()——重新开启 Bottom Half 时do_IRQ()返回后(中断退出路径irq_exit())- Ksoftirqd 内核线程(当 softirq 过于频繁时被唤醒消费积压)
值得注意的是,同一个 softirq 可以在不同 CPU 上并发运行(HI 和 NET_RX 除外通过对 softirq_veq 链表加锁实现串行化)。每个 CPU 的 softirq_pending 是 Per-CPU 变量。
2.2 Tasklet
Tasklet 是基于 softirq 构建的轻量级 Bottom Half 机制:
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 的调度由 tasklet_schedule() 实现,将 tasklet 加入 Per-CPU tasklet_vec 链表并触发 TASKLET_SOFTIRQ。关键特性:
- 串行保证:同一 tasklet 不会在多个 CPU 上并行执行
- 禁用/启用:
tasklet_disable()+tasklet_enable()通过引用计数实现 - 原子上下文:tasklet 执行在 softirq 上下文,不允许睡眠
Hi-priority tasklet(tasklet_hi_schedule())在 TASKLET_SOFTIRQ 处理时优先遍历 tasklet_hi_vec,适用于网络接收等高吞吐场景。
2.3 工作队列 (workqueue)
工作队列是唯一允许睡眠的 Bottom Half 实现:
struct workqueue_struct {
struct list_head pwqs; // 池工作队列链表
struct list_head list; // 工作链表
struct list_head flusher; // 冲刷等待队列
struct pool_workqueue **pwqs_tbl; // 多优先级 PWA 数组
...
};
Workqueue 使用 "Concurrency Managed"(CMWQ)模型,每个 CPU 维护 N 个 worker 线程(默认 256 个活跃 worker),具有自动扩容/收缩机制:
- SCHED_NORMAL:使用 CFS 调度器,worker 绑定 CPU
- SCHED_FIFO:用于
WQ_HIGHPRI高优先级工作队列 - UNBOUND:不绑定特定 CPU,跨 NUMA 节点调度
- POWERSAVING:空闲时回收 worker 线程
内核预定义了 system_wq 和 system_highpri_wq,用户通过 schedule_work() 或 queue_work() 提交工作。对于需要严格串行的场景,使用 system_ordered_wq(每个 CPU 只有一个 worker)。
2.4 irqthread 机制
Linux 3.x 引入的 IRQ thread 机制将中断处理移到进程上下文:
- Top Half 执行时仅
IRQF_THREADhandler 感知 - 内核启动 irq_thread,调用已注册的线程化处理函数
- 线程化处理可以睡眠(如访问 I2C/SPI 的慢速设备)
- 通过
IRQF_ONESHOT标志确保中断仅在线程完成后重新使能
该机制解决了传统 Top Half 中无法睡眠的约束,特别适用于 GPIO 按键、传感器等事件驱动外设。
三、中断亲和性与性能调优
3.1 SMP IRQ Affinity
通过 /proc/irq/<irq>/smp_affinity 可以指定中断路由到的 CPU 掩码:
# 将中断号 42 绑定到 CPU 2-3
echo 0x0c > /proc/irq/42/smp_affinity
# irqbalance 守护进程的动态调优
cat /proc/irq/42/smp_affinity_list
# 输出: 2-3
现代 NIC(如 Mellanox ConnectX)使用 RSS(Receive Side Scaling)将多队列中断分发到不同 CPU。结合 ethtool -X eth0 配置队列到 CPU 的映射,可使单端口 100Gbps 网卡线速接收。
3.2 NAPI 轮询模式
Linux 网络栈引入 NAPI(New API)解决高速网络中中断风暴问题:
- 中断到达后调用
napi_schedule()触发 NET_RX_SOFTIRQ - 核心禁用该设备的中断(
disable_napi()) - Poller 在 softirq 上下文轮询接收数据包直到
budget耗尽或队列为空 - 重新使能中断,退出软中断处理
NAPI 的 budget 默认值为 64,可通过 sysctl net.core.netdev_budget 调整。对于 10Gbps+ 网络环境,适度提高 budget 可以减少中断上下文切换开销。
3.3 中断延迟测量
ftrace 提供的 irqsoff tracer 是诊断中断延迟的利器:
# 启用 irqsoff tracer
echo irqsoff > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
# ... 运行基准测试 ...
cat /sys/kernel/debug/tracing/tracing_max_latency
# 输出: 235 us
此外 preemptoff tracer 记录关抢占时间,irqsoff + preemptoff 对比可以分离中断屏蔽与抢占屏蔽的延迟贡献。四、ftrace 框架架构
ftrace 是 Linux 内置的轻量级追踪框架,位于 kernel/trace/ 目录,核心组件包括:
4.1 ftrace 的核心数据结构
struct ftrace_func_command {
struct list_head list;
char *name;
func_func_t func;
};
struct trace_array {
struct list_head systems;
struct tracer *current_tracer; // 当前使用的 tracer
struct array_buffer buffer; // 环形缓冲区
unsigned long *start;
...
};Ring Buffer 是 ftrace 的数据存储层,使用无锁读者-写者模型,每个 CPU 独立缓冲区避免竞争。写入端(trace 调用)不可被阻断,读取端(trace_pipe)可能因缓冲区溢出丢失数据。
4.2 Tracer 类型
4.3 Mcount 动态插桩
ftrace 利用 GCC 的 -pg 编译选项在每个函数入口插入 mcount 调用。在启用 ftrace 后,mcount 被替换为 __ftrace_mcount,后者检查当前 tracer 状态决定是否记录函数。
- 静态追踪点:编译时确定的插桩位置(tracepoint)
- 动态追踪:运行时选择目标函数(通过
set_ftrace_filter) - kprobe:在任意指令位置插入断点触发回调
x86 平台使用 ENTRY(ftrace_caller) 汇编入口,ARC 平台和 RISC-V 平台有各自的实现,但都遵循统一接口。
五、Tracepoint 与静态探针
5.1 Tracepoint 定义与使用
Tracepoint 是内核中预定义的追踪点,使用宏 TRACE_EVENT 声明:
TRACE_EVENT(irq_handler_entry,
TP_PROTO(int irq, struct irqaction *action),
TP_ARGS(irq, action),
TP_STRUCT__entry(
__field(int, irq)
__string(name, action->name)
),
TP_fast_assign(
__entry->irq = irq;
__assign_str(name, action->name);
),
TP_printk("irq=%d name=%s", __entry->irq, __entry->name)
);
Tracepoint 的优势在于始终编译进内核(不依赖 kprobe),零开销开关时仅检查 static_key(br 预测命中时等效空转)。
5.2 子系统 Tracepoint 清单
Linux 在关键子系统部署了丰富的 tracepoint:
- 网络:
netif_receive_skb、napi_gro_receive_entry、tcp_probe - 块设备:
block_rq_issue、block_rq_complete、block_bio_queue - 调度器:
sched_switch、sched_wakeup、sched_process_exec - 内存:
mm_page_alloc、mm_vmscan_wakeup、kmalloc - 系统调用:
sys_enter_*、sys_exit_*

发表评论 取消回复