一、中断的本质:CPU 是如何被打断的

中断(Interrupt)是计算机系统中最为底层的异步事件处理机制。当外部设备需要 CPU 处理时,它会通过物理信号线触发 CPU 的中断引脚,强制 CPU 暂停当前执行流,转去执行预定义的处理程序。

1.1 中断的物理路径

从按下键盘到字符出现在终端,这条路径跨越了多个硬件层次:

键盘按键 → 键盘控制器(8042) → APIC → CPU核心 → IDT查找 → do_IRQ → 驱动handle

现代 x86 系统中,中断控制器经历了 PIC(8259A) → APIC → x2APIC 的演进。Local APIC 集成在 CPU 核心内部,支持中断优先级、IPI(处理器间中断) 和 256 个中断向量。

1.2 中断向量表 IDT

IDT(Interrupt Descriptor Table) 是 x86 架构中断机制的核心数据结构,每个条目包含段选择子、偏移地址和特权级:

// arch/x86/include/asm/idtentry.h
struct idt_gate {
    u16 offset_low;      // 处理函数地址低16位
    u16 segment;         // 代码段选择子
    u8  ist;             // IST栈索引
    u8  type_attributes; // 类型与DPL
    u16 offset_mid;      // 处理函数地址中16位
    u32 offset_high;     // 处理函数地址高32位
} __attribute__((packed));

Linux 将 0-31 分配给异常,32-127 分配给外部硬件中断,128 保留给系统调用(int 0x80/syscall),225-255 分配给 IPI。

二、上半部与下半部:中断处理的黄金分割

2.1 为什么需要拆分

硬件中断的处理必须在关中断或屏蔽当前中断线的状态下执行,这带来了两个矛盾的需求:中断处理要快(减少关中断时间),但实际工作往往复杂耗时。Linux 的解决方案是将中断处理分为两半:

  • 上半部(Top Half):硬件相关操作(读状态、ACK),在关中断上下文执行
  • 下半部(Bottom Half):耗时操作(数据处理),在开中断上下文执行

2.2 request_irq 与中断注册

// 现代接口:request_threaded_irq
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
                         irq_handler_t thread_fn, unsigned long flags,
                         const char *name, void *dev);

参数说明:

  • handler:硬中断处理函数(上半部),返回 IRQ_WAKE_THREAD 唤醒线程处理函数
  • thread_fn:线程化处理函数(下半部),在进程上下文执行,可被调度
  • flags:IRQF_SHARED(共享中断线)、IRQF_TRIGGER_RISING(上升沿触发)等

三、软中断 softirq:最底层的下半部机制

3.1 软中断的类型与优先级

Linux 定义了 10 种软中断,按优先级从高到低:

// include/linux/interrupt.h
enum {
    HI_SOFTIRQ = 0,        // 高优先级 tasklet
    TIMER_SOFTIRQ,         // 定时器
    NET_TX_SOFTIRQ,        // 网络发送
    NET_RX_SOFTIRQ,        // 网络接收(最关键)
    BLOCK_SOFTIRQ,         // 块设备
    IRQ_POLL_SOFTIRQ,      // IRQ 轮询
    TASKLET_SOFTIRQ,       // 普通 tasklet
    SCHED_SOFTIRQ,         // 调度器
    HRTIMER_SOFTIRQ,       // 高精度定时器
    RCU_SOFTIRQ,           // RCU 回调
};

3.2 软中断的执行时机

软中断在三个时机被检查和执行:

  1. 硬件中断返回时(irq_exit 中)
  2. 系统调用返回用户空间前
  3. ksoftirqd 内核线程(当软中断频率过高时,避免饥饿用户进程)

3.3 网络接收的 NAPI+软中断

高性能网络驱动使用 NAPI(New API) 机制混合中断和轮询:

// 驱动收到数据包触发硬中断
static irqreturn_t mynic_intr(int irq, void *dev)
{
    // 第一步:关闭后续中断
    disable_irq_nosync(irq);
    // 第二步:调度 NAPI 轮询
    napi_schedule(&priv->napi);
    return IRQ_HANDLED;
}

// 轮询函数在软中断上下文执行
static int mynic_poll(struct napi_struct *napi, int budget)
{
    int work_done = 0;
    while (work_done < budget xss=removed>irq);
    }
    return work_done;
}

四、Tasklet:基于软中断的串行化下半部

4.1 Tasklet 的实现原理

Tasklet 是基于 HI_SOFTIRQ 和 TASKLET_SOFTIRQ 两种软中断构建的更高层 API,保证同类型 tasklet 不会在多个 CPU 上并行执行:

// 定义并初始化 tasklet
struct tasklet_struct my_tasklet;

void my_tasklet_handler(unsigned long data)
{
    // 在软中断上下文执行,不可睡眠
    process_device_data((void *)data);
}

// 初始化
tasklet_init(&my_tasklet, my_tasklet_handler, (unsigned long)priv);

// 在硬中断上半部调度 tasklet
tasklet_schedule(&my_tasklet);

4.2 Tasklet 的调度机制

当调用 tasklet_schedule() 时:

  1. 检查 tasklet 是否已在执行(TASKLET_STATE_RUN)或已调度(TASKLET_STATE_SCHED)
  2. 若未调度,将 tasklet 加入 per-cpu tasklet_vec 链表
  3. 设置 TASKLET_STATE_SCHED 标志
  4. raise_softirq_irqoff(TASKLET_SOFTIRQ) 触发软中断

五、工作队列 workqueue:可睡眠的下半部

5.1 工作队列与前面机制的本质区别

  • 软中断/tasklet:在中断上下文执行,不可被抢占/睡眠,同一个 handler 不会并行
  • 工作队列:在内核线程(进程上下文)执行,可以睡眠、被调度、被抢占

5.2 workqueue 的实现架构

用户提交 work 后通过 worker-pool 选择 worker 线程取出执行。Linux 实现了多种工作队列:system_wq(默认)、system_highpri_wq(高优先级)、system_unbound_wq(不绑定CPU)、system_long_wq(长时间任务)、system_power_efficient_wq(节能)。

5.3 使用示例

// 静态声明
DECLARE_WORK(my_work, my_work_handler);
DECLARE_DELAYED_WORK(my_dwork, my_work_handler);  // 支持延时执行

// 动态初始化
INIT_WORK(&my_work, my_work_handler);
INIT_DELAYED_WORK(&my_dwork, my_work_handler);

// 提交到系统工作队列
schedule_work(&my_work);
// 提交到自定义工作队列
queue_work(my_wq, &my_work);
// 延时10ms后执行
schedule_delayed_work(&my_dwork, msecs_to_jiffies(10));

六、Threaded IRQ:现代驱动的标准范式

6.1 为什么需要 Threaded IRQ

传统驱动模型需要在上半部快速判断中断来源并 ACK,然后通过 tasklet/workqueue 处理耗时逻辑。Threaded IRQ 将这一模式标准化:将硬中断处理简化为最小化操作,主要逻辑移到可调度、可睡眠的内核线程中。

6.2 request_threaded_irq 完整示例

#include 
#include 

struct btn_data {
    int gpio;
    int irq;
    struct device *dev;
};

/* 上半部:返回 IRQ_WAKE_THREAD 唤醒线程 */
static irqreturn_t button_hard_isr(int irq, void *dev_id)
{
    struct btn_data *data = dev_id;
    if (!gpio_get_value(data->gpio))
        return IRQ_NONE;  // 不是我们的中断
    return IRQ_WAKE_THREAD;
}

/* 下半部线程函数,可以睡眠 */
static irqreturn_t button_thread_fn(int irq, void *dev_id)
{
    struct btn_data *data = dev_id;
    msleep(20);  // 去抖动,可以睡眠!
    int state = gpio_get_value(data->gpio);
    dev_info(data->dev, "Button %s", state ? "released" : "pressed");
    return IRQ_HANDLED;
}

static int button_probe(struct platform_device *pdev)
{
    struct btn_data *data;
    int ret;
    data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
    data->gpio = 3;
    data->irq = gpio_to_irq(data->gpio);
    ret = request_threaded_irq(data->irq, button_hard_isr, button_thread_fn,
                               IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
                               "my-button", data);
    if (ret) {
        dev_err(&pdev->dev, "Cannot request IRQ %d", data->irq);
        return ret;
    }
    return 0;
}

七、中断亲和性与多队列分发

7.1 SMP 负载均衡

现代多核系统需要将中断均匀分配到各个 CPU。/proc/irq/IRQ/smp_affinity 控制中断的 CPU 亲和性。通过写入十六进制掩码来指定 CPU 集合。

7.2 irqbalance 守护进程

irqbalance 是用户态服务,通过监控中断分布模式动态调整亲和性。它识别 NUMA 本地模式(绑定到设备所属 NUMA 节点)、轮询模式(均匀分配)和电源模式(集中到少数 CPU 以让其他核心深度睡眠)。

7.3 MSI-X 多队列网卡

MSI-X 允许设备拥有多个独立中断向量,驱动利用此特性实现多队列分发。每个 RX/TX 队列拥有独立的 MSI-X 中断,数据到达时只触发对应队列的中断,由 NAPI 轮询处理完后重新开启中断。

八、中断上下文与睡眠禁令

8.1 为什么中断上下文不能睡眠

中断上下文没有关联的 task_struct(尽管 current 指向被中断进程),没有独立内核栈,不可被调度器选择执行。睡眠意味着让调度器选择一个没有 task_struct 的上下文执行,会导致系统崩溃。

8.2 in_interrupt() 与 in_atomic()

// in_interrupt(): 是否在中断上下文(硬中断 + 软中断 + NMI)
// in_atomic(): 是否在原子上下文(抢占关闭 + 中断上下文 + 持有锁)

if (in_interrupt()) {
    ptr = kmalloc(size, GFP_ATOMIC);  // 原子分配,不睡眠
} else {
    ptr = kmalloc(size, GFP_KERNEL);  // 可能睡眠等待页分配
}

8.3 常见的睡眠陷阱

在中断上下文中禁止调用的函数包括:copy_from_user()/copy_to_user()(可能触发缺页)、mutex_lock()(可能睡眠等待)、kmalloc(GFP_KERNEL)、schedule() 直接调用等。正确区分中断上下文和进程上下文是驱动开发的关键技能。

九、生产级中断调优实战

9.1 /proc/interrupts 解读

通过 /proc/interrupts 可以查看每个中断在各 CPU 上的计数分布。如果某队列明显高于其他队列,说明哈希分发不均,需要检查 RSS 哈希策略(ethtool -X)。

9.2 软中断排查:softirq 风暴

使用 /proc/softirqs 监控每秒中断计数。若 NET_RX_SOFTIRQ 某核异常高且伴随丢包,需要检查 NAPI budget(net.core.netdev_budget)和网卡队列分配。

9.3 中断合并(Coalescing)

高速网卡默认启用中断合并,在包数量和延迟之间权衡。通过 ethtool -C 可以配置 rx-usecs(微秒级延迟)或 rx-frames(帧数阈值)。现代网卡还支持硬件自适应模式,根据流量自动调整。

十、总结:Linux 中断处理全景

Linux 内核中断子系统经历了从简单到复杂、从粗粒度到精细化的演进历程。理解其内核机制对于编写高性能驱动至关重要:

  • hardirq:最小化执行,只做 ACK 和状态保存
  • softirq:高频低延迟,固定 10 种类型,不睡眠
  • tasklet:基于 softirq,串行化保证
  • workqueue:可睡眠,适合长时间任务
  • threaded irq:现代驱动标准范式,清晰的分层结构

生产环境中,GHz 级别的中断频率、NUMA 亲和性配置、MSI-X 队列分发和 coalescing 调优构成了中断性能优化的四维空间。掌握这些机制,才能在高并发网络、存储等场景下释放硬件的全部性能。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部
0.418298s