一、中断的本质: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 软中断的执行时机
软中断在三个时机被检查和执行:
- 硬件中断返回时(irq_exit 中)
- 系统调用返回用户空间前
- 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() 时:
- 检查 tasklet 是否已在执行(TASKLET_STATE_RUN)或已调度(TASKLET_STATE_SCHED)
- 若未调度,将 tasklet 加入 per-cpu
tasklet_vec链表 - 设置 TASKLET_STATE_SCHED 标志
- 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 调优构成了中断性能优化的四维空间。掌握这些机制,才能在高并发网络、存储等场景下释放硬件的全部性能。

发表评论 取消回复