一、中断处理硬件模型与内核入口

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() 的核心逻辑是:

  1. 获取中断向量号 vector 和对应的 irq_desc
  2. 递增 desc->kstat_irqs 中断计数器
  3. 调用 desc->handle_irq(&desc)(通常为 handle_level_irq 或 handle_fasteoi_irq)
  4. 在 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_callbacksRCU 回调处理

Softirq 的触发通过 raise_softirq() 设置 CPU 的 softirq_pending 位图,在以下三个时机检查并执行:

  1. local_bh_enable()——重新开启 Bottom Half 时
  2. do_IRQ() 返回后(中断退出路径 irq_exit())
  3. 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 机制将中断处理移到进程上下文:

  1. Top Half 执行时仅 IRQF_THREAD handler 感知
  2. 内核启动 irq_thread,调用已注册的线程化处理函数
  3. 线程化处理可以睡眠(如访问 I2C/SPI 的慢速设备)
  4. 通过 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)解决高速网络中中断风暴问题:

  1. 中断到达后调用 napi_schedule() 触发 NET_RX_SOFTIRQ
  2. 核心禁用该设备的中断(disable_napi())
  3. Poller 在 softirq 上下文轮询接收数据包直到 budget 耗尽或队列为空
  4. 重新使能中断,退出软中断处理

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 类型

function追踪所有内核函数调用基于 mcount 动态插桩 function_graph追踪函数调用图与耗时记录调用栈与返回 nop无操作,仅保留基础设施默认状态

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_*
]]
点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部