引言:中断处理的灵魂问题

中断是操作系统与硬件交互的基石。当键盘按键、网卡收包、磁盘 IO 完成时,硬件通过中断通知 CPU。但一个核心矛盾摆在面前:中断处理既要快,又要做很多事。如果全程关中断执行大量代码,系统响应性将急剧下降;如果只做最少的工作,又无法完成复杂的数据处理。

Linux 内核的经典解决方案是顶半部/底半部(Top Half / Bottom Half)分离架构。本文将深入剖析内核中三种底半部机制——Softirq、Tasklet 和 Workqueue——的设计思想、源码实现和适用场景。

一、中断处理基础模型

1.1 顶半部与底半部的分工

当硬件中断触发时,CPU 跳转到中断向量表注册的 handler 执行。顶半部(Top Half)在中断上下文中运行,要求极简、快速、不阻塞,通常只完成:清除中断标志、拷贝关键数据、调度底半部。底半部(Bottom Half)则在相对宽松的环境中完成耗时操作。

// 注册中断处理函数的经典方式
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
            const char *name, void *dev);

// handler 即为顶半部,在其中仅做必要操作后触发底半部

1.2 中断上下文的约束

中断上下文(Interrupt Context)运行时有严格限制:不能睡眠/阻塞(没有进程上下文支撑 schedule())、不能访问用户空间内存、优先级高于所有普通进程。任何违反这些约束的行为都会导致内核崩溃或死锁。

1.3 中断嵌套与 IRQ 标志

request_irq() 中的 flags 参数控制中断行为:IRQF_SHARED 允许共享中断线;IRQF_SHARED 配合 IRQF_SHARED 使用;IRQF_SHARED 标识中断处理期间本地中断保持使能(高性能网络常用)。

二、Softirq:底半部机制的基石

2.1 静态注册的 Softirq 向量

Softirq 是内核在编译时静态注册的底半部机制,定义在 include/linux/interrupt.h 中:

enum {
    HI_SOFTIRQ = 0,        // 高优先级 Tasklet(HI_THREAD)
    TIMER_SOFTIRQ,         // 定时器
    NET_TX_SOFTIRQ,        // 网络发送
    NET_RX_SOFTIRQ,        // 网络接收(NAPI 轮询)
    BLOCK_SOFTIRQ,         // 块设备完成通知
    IRQ_POLL_SOFTIRQ,      // IO_POLL 模式
    TASKLET_SOFTIRQ,       // 普通 Tasklet
    SCHED_SOFTIRQ,         // 调度器时钟
    HRTIMER_SOFTIRQ,       // 高精度定时器
    RCU_SOFTIRQ,           // RCU 回调
    NR_SOFTIRQS            // 总数,当前为 10
};

这 10 个 Softirq 向量有严格优先级:下标越小优先级越高(HI_SOFTIRQ 最高)。网络收发包使用独立向量而非复用 Tasklet,正是为了防止高吞吐网络流量饿死其他底半部。

2.2 Softirq 的执行时机

Softirq 在以下三个时机被检查和执行:

  • 硬件中断返回后:irq_exit() 调用 invoke_softirq()
  • 底半部重新使能时:local_bh_enable() 调用 do_softirq()
  • Ksoftirqd 内核线程:当 Softirq 过于频繁时,不再在当前上下文执行,而是唤醒 ksoftirqd/N 内核线程处理

2.3 Softirq 的数据结构

每个 CPU 通过 irq_cpustat_t 中的 __softirq_pending 位图记录待处理的 Softirq。触发 Softirq 只需设置对应位:

open_softirq(NET_RX_SOFTIRQ, net_rx_action);   // 注册 handler
raise_softirq(NET_RX_SOFTIRQ);                  // 触发执行

三、Tasklet:基于 Softirq 的动态封装

3.1 Tasklet 的设计理念

Tasklet 建立在 Softirq 之上(使用 HI_SOFTIRQ 和 TASKLET_SOFTIRQ 两个向量),提供动态可注册的底半部接口。相比 Softirq,Tasklet 的关键优势:同一个 Tasklet 不会在多个 CPU 上并发运行(串行化保证),降低了驱动开发者的同步负担。

3.2 Tasklet 的核心结构

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;            // 传递给 func 的参数
};

3.3 Tasklet 的调度流程

驱动开发者通过 tasklet_init() 初始化、tasklet_schedule() 调度。raise_softirq_irqoff(TASKLET_SOFTIRQ) 将 tasklet 挂入当前 CPU 的 tasklet_vec 链表,随后由 tasklet_action() 处理链表中所有待执行 tasklet。

3.4 Tasklet 的串行化保证

TASKLET_STATE_RUN 标志确保同一 tasklet 在多个 CPU 上不并发。当 tasklet 在 CPU-A 上运行时,若 CPU-B 尝试调度同一 tasklet,test_and_set_bit(TASKLET_STATE_SCHED) 会失败而仅挂入链表,不会重复执行。

四、Workqueue:可睡眠的底半部

4.1 为什么需要 Workqueue?

Softirq 和 Tasklet 运行在中断上下文中,不能睡眠、不能阻塞。但实际驱动中经常需要:申请大块内存(可能触发直接回收)、执行阻塞 IO、持有互斥锁。这些操作只能使用 Workqueue——它运行在进程上下文中,由 Worker 内核线程执行。

4.2 Workqueue 的核心数据结构

struct work_struct {
    atomic_long_t data;
    struct list_head entry;
    work_func_t func;             // 工作处理函数
};

struct workqueue_struct {
    struct list_head pwqs;        // 挂载的 pwq 链表
    struct pool_workqueue *pwqs;  // per-cpu 工作池
    struct worker_pool *cpu_pools;
    // ...
};

4.3 默认工作队列 vs 自定义工作队列

系统提供两种默认工作队列:system_wq(普通优先级)和 system_highpri_wq(高优先级)。通用工作使用 schedule_work() 即可。但对延迟敏感或有特殊要求的场景,建议用 alloc_workqueue() 创建独立工作队列,避免其他业务的阻塞影响。

4.4 CANCEL 和 FLUSH 机制

Workqueue 支持取消未完成的工作(cancel_work_sync())和等待队列排空(flush_workqueue()),这对模块卸载和热插拔场景至关重要。

五、三种机制的选择策略

维度SoftirqTaskletWorkqueue
执行环境中断上下文中断上下文进程上下文(Worker 线程)
能否睡眠否否是
并发性多 CPU 可并发同类型串行化通过 pool lock 控制
注册方式编译时静态运行时动态运行时动态
典型用途网络收发包、块设备字符设备、驱动底半部热插拔、固件加载、磁盘维护
延迟极低低相对高(需调度)

5.1 选择原则

  • 需要阻塞/睡眠 → 必须用 Workqueue
  • 高性能且无需休眠 → 优先 Softirq 或 Tasklet
  • 驱动通用底半部 → Tasklet(开发最简单,安全有保障)
  • 高级网络/块设备优化 → 直接使用 Softirq

六、内核演进:Threaded IRQ

Linux 3.0+ 引入 request_threaded_irq(),允许驱动将中断处理分拆为两个函数:hard irq handler(快速 ACK+标记)和 threaded handler(在内核线程中执行,可阻塞)。这种设计将大部分中断处理移到进程上下文,避免长时间关中断。

request_threaded_irq(irq, hard_handler, threaded_handler,
                     IRQF_ONESHOT, name, dev);
// hard_handler 快速确认中断源(返回 IRQ_WAKE_THREAD 唤醒线程)
// threaded_handler 在内核线程中执行,可睡眠、可阻塞 IO

GPIO 驱动、I2C 设备驱动等广泛使用 Threaded IRQ,它在保留中断快速响应性的同时获得了进程上下文的灵活性。

七、实战案例:网卡中断优化

7.1 NAPI(New API)轮询与中断混合模式

Linux 网卡驱动采用经典的"中断+轮询"模式:数据包到达 → 硬件中断 → 顶半部关闭中断、调度 NAPI → 底半部用 netif_rx() 或 NAPI poll 函数轮询收包,直到没有数据或时间片耗尽。这种混合模式在高吞吐时避免中断风暴。

// 典型 NAPI 流程
// 1. 中断触发 -> 关闭 RX 中断,调度 NAPI
napi_schedule(&adapter->napi);

// 2. NAPI poll 函数在中断软中断(NET_RX_SOFTIRQ)中执行
int poll(struct napi_struct *napi, int budget) {
    // 预算控制:budget=64 表示最多处理 64 个包
    while (packets_processed < budget) {
        process_packet(rx_desc);
    }
    if (packets_processed < budget) {
        napi_complete(napi);    // 完成,重新使能 RX 中断
        enable_rx_interrupt();
    }
}

7.2 interrupt affinity 与多队列网卡

现代多队列网卡(如 Intel X710)将不同类型流量分到不同 RX/TX 队列,每个队列有独立中断线。通过 /proc/irq/IRQ_NUMBER/smp_affinity 绑定中断到指定 CPU 核,实现中断负载均衡和 NUMA 局部性优化。irqbalance 守护进程可自动管理中断亲和性。

7.3 busy_poll 零拷贝收包

Linux 4.x 引入 socket 级别的 SO_BUSY_POLL 选项,允许用户态线程在忙等中直接参与 NAPI poll,省去中断唤醒的开销。配合 XDP(eXpress Data Path)可实现微秒级网络处理延迟。

八、调试与观测工具链

8.1 /proc/softirqs

$ cat /proc/softirqs
                    CPU0       CPU1       CPU2       CPU3
          HI:         42         38         45         40
       TIMER:     12580      11320      10980      11560
      NET_TX:       234        198        210        220
      NET_RX:     52340      31200      19800      28700  -- 网络接收热点
       BLOCK:      1230      1180      1050      1320
    IRQ_POLL:         0          0          0          0
     TASKLET:        89         67         82         73
       SCHED:      8520       8310       8100       8670
   HRTIMER:         56         45         51         62
         RCU:     15230      14800      13900      15100

观察各 CPU 的 Softirq 分布可识别中断不均衡问题。NET_RX 在某 CPU 显著偏高时,需检查中断亲和性配置。

8.2 ftrace 中断延迟追踪

// 使能 irqsoff tracer
$ echo 0 > /sys/kernel/debug/tracing/tracing_on
$ echo irqsoff > /sys/kernel/debug/tracing/current_tracer
$ echo 1 > /sys/kernel/debug/tracing/tracing_on
// ... 运行测试场景 ...
$ echo 0 > /sys/kernel/debug/tracing/tracing_on
$ cat /sys/kernel/debug/tracing/trace

8.3 BPF 中断热点分析

// BPF 程序:统计中断处理延迟分布
SEC("tracepoint/irq/irq_handler_entry")
int trace_irq_handler_entry(struct trace_event_raw_irq_handler_entry *ctx) {
    u32 irq = ctx->irq;
    u64 ts = bpf_ktime_get_ns();
    bpf_map_update_elem(&start, &irq, &ts, BPF_ANY);
    return 0;
}

SEC("tracepoint/irq/irq_handler_exit")
int trace_irq_handler_exit(struct trace_event_raw_irq_handler_exit *ctx) {
    u32 irq = ctx->irq;
    u64 *start_ts = bpf_map_lookup_elem(&start, &irq);
    if (!start_ts) return 0;
    u64 delta = bpf_ktime_get_ns() - *start_ts;
    // 更新延迟直方图
    hist_key_t key = {.irq = irq};
    bpf_map_update_elem(&lat_hist, &key, &delta, BPF_ANY);
    return 0;
}

通过 eBPF 的 tracepoint 挂载,可精确统计各 IRQ 的中断处理耗时分布,识别硬件断瓶颈。结合 biolatency 和 runqlat 等 BPF 工具,可以建立从硬件中断到进程调度的全链路延迟画像。

九、性能调优实战

9.1 中断合并(Interrupt Coalescing)

现代网卡支持中断合并:在指定时间窗口内积累多个事件后发一次中断。通过 ethtool -C 配置:

# 延迟 100us 或积累 32 个包后发中断
$ ethtool -C eth0 rx-usecs 100 rx-frames 32

# 查看当前配置
$ ethtool -C eth0

降低中断频率可减少 CPU 开销,但增加单次中断处理量和延迟。需在吞吐和延迟之间寻找平衡点。

9.2 自适应中断调节(Adaptive Coalescing)

许多网卡驱动支持动态自适应调节:负载高时增大合并窗口(减少中断数),负载低时减小窗口(降低延迟)。ethtool 的 adaptive-rx 选项控制此行为。

9.3 NO_HZ_FULL 与 Tickless 内核

在低延迟场景(如高频交易),NO_HZ_FULL 模式让指定 CPU 完全免除定时器中断和 Softirq tick。但需要确保这些 CPU 上不会触发 TIMER_SOFTIRQ,否则内核会按模式降级处理。通过 nohz_full= 内核参数和 rcu_nocbs= 配置可启用此特性。

十、x86 APIC 中断控制器架构

10.1 本地 APIC 与 I/O APIC

现代 x86 系统通过 I/O APIC(通常集成在南桥/PCH中)收集中断源信号,通过 Local APIC(集成在每个 CPU 核心内)分发中断。Local APIC 支持中断优先级(TPR/PPR)、处理器间中断(IPI)和本地定时器。

10.2 MSI/MSI-X 消息信号中断

MSI(Message Signaled Interrupts)用内存写事务代替物理中断线,绕过 I/O APIC 瓶颈,支持 224 个独立中断向量(MSI-X 更多)。PCIe 设备默认使用 MSI/MSI-X,优势:无共享中断线冲突、独立亲和性、更低延迟。

十一、实时系统中的中断处理

11.1 PREEMPT_RT 补丁的核心思路

PREEMPT_RT(Real-Time)补丁将 Linux 内核转变为硬实时操作系统,关键改动包括:将 Softirq 转换为内核线程(ksoftirqd 模式)、将绝大多数 spinlock 替换为可睡眠的 rt_mutex。这些改动使 Softirq 和 Workqueue 同样在上下文中可抢占,极大改善了中断延迟。

11.2 中断线程化

PREEMPT_RT 强制所有硬件中断处理为线程模式(与 IRQF_ONESHOT 驱动),确保中断处理可被更高优先级任务抢占。cyclictest 是标准的中断延迟基准测试工具,PREEMPT_RT 内核通常能将延迟控制在 30us 以内。

十二、总结与最佳实践

Linux 中断底半部机制经过二十多年的演进,已形成层次分明的技术栈:

  • Softirq:内核底层基础设施,网络/块设备等核心子系统直接使用,追求极致性能
  • Tasklet:驱动开发者最常用的底半部接口,简单安全、无需关心并发
  • Workqueue:唯一允许睡眠的底半部,适用于需要阻塞 IO、内存分配等场景
  • Threaded IRQ:现代推荐的驱动开发模式,平衡延迟与灵活性

选择合适的底半部机制,本质上是在执行速度 vs 开发复杂度 vs 功能需求之间做权衡。理解其源码实现和调度行为,是优化系统性能、诊断中断瓶颈的必经之路。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部