在第 6 章:物理内存管理中,我们了解了内核如何管理物理页帧。但有一个关键问题始终悬而未决:当网卡收到一个数据包、当键盘被按下、当定时器到期——这些异步事件是如何通知内核的?内核又如何在保证系统响应速度的同时,不因中断处理而拖慢整体吞吐量?
答案就在本章的主题——中断与软中断。
中断是硬件与内核沟通的桥梁,是操作系统”响应性”的根本保障。没有中断,内核只能不断轮询设备状态,CPU 将被白白浪费在空转上。但中断处理本身也存在深刻的矛盾:中断必须快,否则会阻塞其他中断;中断又必须做足够多的事,否则数据会丢失、协议栈来不及处理。Linux 用一套精巧的分层机制——Top Half / Bottom Half 分离、Softirq、Tasklet、Workqueue——来化解这一矛盾。
本章将从硬件中断的本质出发,逐层剖析 Linux 中断子系统的设计与实现。
一、中断的本质:从硬件信号到软件处理
1.1 什么是中断
中断(Interrupt)是 CPU 在执行程序的过程中,由于内部或外部的异步事件,暂时中止当前程序,转去处理该事件,处理完毕后再返回原程序断点继续执行的机制。
从硬件角度看,中断是一条电信号——设备通过中断控制器(如 x86 的 APIC)向 CPU 发出中断请求,CPU 在每条指令执行结束时检测是否有未处理的中断。一旦检测到,CPU 硬件自动完成以下操作:
- 保存当前执行上下文(至少包括 EFLAGS/RFLAGS、CS、RIP)
- 根据中断向量号查表(IDT),找到对应的中断处理程序入口
- 切换到内核栈,跳转到处理程序执行
中断与系统调用有本质区别:系统调用是同步的、由软件主动发起的(int 0x80 或 syscall 指令);中断是异步的、由硬件在任意时刻触发的。这意味着中断可以在用户态代码执行的任何一条指令之后发生,内核必须为此做好准备。
1.2 中断的分类
Linux 将中断分为两大类:
| 类别 | 来源 | 特点 | 示例 |
|---|---|---|---|
| 外部中断(硬件中断) | 外部设备 | 异步、可屏蔽 | 网卡收包、键盘中断、定时器 |
| 异常(内部中断) | CPU 内部 | 同步、不可屏蔽 | 缺页异常、除零错误、断点 |
外部中断由 IRQ(Interrupt Request)编号标识,通过可编程中断控制器(PIC/APIC)管理。异常则由 CPU 在执行指令时检测到错误条件自动触发。
x86 架构将中断向量空间(0-255)划分为:0-31 为 CPU 保留的异常和 NMI,32-255 为用户可用的外部中断。Linux 将 32-255 号向量分配给各种硬件设备和软件用途。
1.3 中断控制器:从 8259A 到 APIC
在单核时代,PC 使用两片级联的 8259A PIC(可编程中断控制器),提供 15 个 IRQ 线。进入多核时代后,8259A 的单控制器设计无法满足 SMP 需求,被 APIC(Advanced Programmable Interrupt Controller)取代。
APIC 体系由两部分组成:
- Local APIC:每个 CPU 核心一个,负责接收中断、管理中断优先级、向 CPU 递交中断
- I/O APIC:系统级,负责接收外部设备的中断信号,根据配置将中断路由到特定 CPU 的 Local APIC
在更现代的系统中,MSI(Message Signaled Interrupt)绕过了 I/O APIC,设备直接通过 PCIe 写操作向 Local APIC 发送中断消息,减少了共享中断引脚的竞争。
二、IDT:中断描述符表
2.1 IDT 的结构与门描述符
IDT(Interrupt Descriptor Table)是 x86 架构中 CPU 查找中断处理程序入口的核心数据结构。它是一个最多 256 项的数组,每项是一个门描述符(Gate Descriptor),描述了中断处理程序的段选择子、偏移地址和权限信息。
x86_64 上的门描述符有三种类型:
| 类型 | 用途 | 特权级切换 | 典型场景 |
|---|---|---|---|
| 中断门(Interrupt Gate) | 硬件中断处理 | 自动切换 | 外设中断 |
| 陷阱门(Trap Gate) | 异常处理 | 不切换 | 缺页、断点 |
| 任务门(Task Gate) | 硬件任务切换 | 硬件切换 | Double Fault(现代内核很少使用) |
中断门与陷阱门的关键区别:中断门在进入处理程序时自动清除 IF 标志位(禁止中断),陷阱门不清除。这意味着通过中断门进入的处理程序在执行期间不会被其他中断打断,而陷阱门允许嵌套中断。
2.2 IDT 的初始化
Linux 在启动早期通过 idt_setup_early_handler() 建立基本的 IDT,随后在 idt_setup_traps() 和 idt_setup_ist_traps() 中完善异常处理入口,最后通过 idt_setup_from_table() 注册硬件中断处理程序。
// arch/x86/kernel/idt.c — IDT 表项定义(简化)static const __initconst struct idt_data early_idts[] = { INTG(X86_TRAP_DB, asm_exc_debug), INTG(X86_TRAP_BP, asm_exc_int3),};
static const __initconst struct idt_data def_idts[] = { INTG(X86_TRAP_DE, asm_exc_divide_error), INTG(X86_TRAP_NMI, asm_exc_nmi), INTG(X86_TRAP_PF, asm_exc_page_fault), // ... 更多异常入口};每个 CPU 核心都有自己的 IDT(通过 idt_descr 描述),通过 lidt 指令加载到 CPU 的 IDTR 寄存器中。当 CPU 收到中断向量号 N 时,硬件自动从 IDT[N] 中取出门描述符,跳转到对应的处理程序。
2.3 IST:中断栈表
x86_64 引入了 IST(Interrupt Stack Table)机制,为某些关键异常提供独立的内核栈。普通中断使用当前 CPU 的内核栈,但 Double Fault、NMI、Machine Check 等严重异常可能发生在内核栈已损坏的情况下,必须切换到专用栈才能安全处理。
// arch/x86/kernel/idt.c — 使用 IST 的异常static const __initconst struct idt_data ist_idts[] = { ISTG(X86_TRAP_DB, asm_exc_debug, IST_INDEX_DB), ISTG(X86_TRAP_NMI, asm_exc_nmi, IST_INDEX_NMI), ISTG(X86_TRAP_DF, asm_exc_double_fault, IST_INDEX_DF), ISTG(X86_TRAP_MC, asm_exc_machine_check, IST_INDEX_MCE),};IST 是 x86_64 硬件特性,Linux 为每个 CPU 预分配了多个 IST 栈(每个 4KB),确保即使主内核栈溢出,关键异常仍能被安全处理。这是操作系统防御性设计的经典案例。
三、硬件中断处理流程
3.1 完整的中断处理路径
当外部设备发出中断信号后,从硬件到软件的完整处理路径如下:
3.2 中断入口:从汇编到 C
中断入口代码是高度优化的汇编代码,位于 arch/x86/entry/entry_64.S。它的工作是:
- 切换栈:如果中断发生在用户态,切换到当前 CPU 的内核栈
- 保存上下文:将所有通用寄存器压栈,形成
pt_regs结构 - 调用 C 函数:跳转到
generic_handle_irq_desc()开始高级处理
// arch/x86/include/asm/ptrace.h — pt_regs 结构(简化)struct pt_regs { unsigned long r15, r14, r13, r12; unsigned long bp, bx; unsigned long r11, r10, r9, r8; unsigned long ax, cx, dx, si, di; unsigned long orig_ax; // 中断向量号 / 系统调用号 unsigned long ip; // 被中断指令的地址 unsigned long cs; // 代码段寄存器 unsigned long flags; // RFLAGS unsigned long sp; // 栈指针 unsigned long ss; // 栈段寄存器};3.3 IRQ 描述符与中断处理链
Linux 用 struct irq_desc 描述每一个 IRQ 线,它构成了中断子系统的核心数据结构:
// include/linux/irqdesc.h — 简化版struct irq_desc { struct irq_data irq_data; // 底层硬件信息 struct irqaction *action; // 中断处理程序链表 unsigned int depth; // 禁用嵌套深度 unsigned int status_use_accessors; // 状态标志 raw_spinlock_t lock; // 保护本结构的自旋锁 struct cpumask *percpu_enabled; // 每 CPU 启用掩码 unsigned int irqs_unhandled; // 未处理中断计数 // ...};当多个设备共享同一个 IRQ 线时(如 PCI 设备的共享中断),action 字段指向一个 irqaction 链表,内核依次调用链表上的每个处理程序,由处理程序自行判断中断是否属于自己:
struct irqaction { irq_handler_t handler; // 中断服务程序 void *dev_id; // 设备标识(区分共享中断) struct irqaction *next; // 链表下一个 unsigned int irq; // IRQ 编号 unsigned int flags; // 标志(SA_SHIRQ 等) const char *name; // /proc/interrupts 中显示的名称};3.4 中断上下文的限制
中断处理程序运行在中断上下文中,而非进程上下文。这意味着:
- 没有进程上下文:没有
current指向的task_struct(准确地说,current指向被中断的进程,但中断处理不属于该进程) - 不可睡眠:不能调用可能阻塞的函数(
kmalloc(GFP_KERNEL)、mutex_lock()、schedule()等),因为没有进程上下文来承载睡眠和唤醒 - 不可访问用户空间:中断可能在内核态发生,此时用户空间地址可能无效
- 执行时间必须极短:中断处理期间,当前 CPU 通常禁止中断,长时间处理会阻塞其他中断
在中断上下文中调用睡眠函数是 Linux 内核编程中最常见的致命错误之一。它会导致”BUG: scheduling while atomic”的内核告警,甚至系统死锁。这也是 Linux 将中断处理分为 Top Half 和 Bottom Half 的根本原因。
四、Top Half 与 Bottom Half 分离
4.1 为什么要分离
中断处理面临一个根本矛盾:
- 中断必须快:中断处理期间,本地 CPU 的中断被禁止(或部分禁止),长时间处理会延迟其他中断的响应,导致数据丢失或系统卡顿
- 中断必须做足够多的事:网卡收到数据包后,需要将数据从网卡 FIFO 拷贝到内核缓冲区、初始化 sk_buff、通知网络协议栈处理……这些工作不可能在几微秒内完成
Linux 的解决方案是 Top Half / Bottom Half 分离:
- Top Half(上半部):在中断上下文中执行,关中断状态下完成最紧急的工作——应答硬件、拷贝数据、标记 Bottom Half 待处理
- Bottom Half(下半部):在开中断状态下执行,完成可延迟的工作——协议栈处理、数据解析、唤醒等待进程
这种分离确保了中断响应的极低延迟,同时不丢失需要完成的后续工作。
4.2 Bottom Half 的演进
Linux 的 Bottom Half 机制经历了漫长的演进:
| 机制 | 执行上下文 | 可睡眠 | 并行性 | 适用场景 |
|---|---|---|---|---|
| Softirq | 中断上下文 | 同类型可多核并行 | 网络收发、块设备、定时器 | |
| Tasklet | 中断上下文 | 同类型串行执行 | 驱动中的轻量延迟工作 | |
| Workqueue | 进程上下文 | 可多核并行 | 需要睡眠或长时间的工作 | |
| Threaded IRQ | 进程上下文 | 每 IRQ 一个线程 | 将整个中断处理线程化 |
早期的 BH(Bottom Half)机制使用一个全局锁,同一时刻只能有一个 CPU 执行 Bottom Half,在 SMP 系统上成为严重的性能瓶颈。Softirq 的引入正是为了解决这一问题——同类型的 Softirq 可以在不同 CPU 上并行执行。
五、Softirq:高性能的软中断机制
5.1 Softirq 的 9 种类型
Softirq 是 Linux 中最高效的 Bottom Half 机制,它在编译时静态注册,执行于中断上下文,同类型的 Softirq 可以在多个 CPU 上并行执行。内核当前定义了 9 种 Softirq:
enum { HI_SOFTIRQ = 0, // 高优先级 tasklet TIMER_SOFTIRQ, // 定时器 NET_TX_SOFTIRQ, // 网络发送 NET_RX_SOFTIRQ, // 网络接收 BLOCK_SOFTIRQ, // 块设备 IRQ_POLL_SOFTIRQ, // 中断轮询(IO 完成优化) TASKLET_SOFTIRQ, // 普通 tasklet SCHED_SOFTIRQ, // 调度负载均衡 HRTIMER_SOFTIRQ, // 高精度定时器 RCU_SOFTIRQ, // RCU 回调};这些 Softirq 按优先级从高到低排列。HI_SOFTIRQ 优先级最高,RCU_SOFTIRQ 最低。在 __do_softirq() 中,内核按优先级顺序依次检查并执行待处理的 Softirq。
5.2 per-CPU pending 位图
每个 CPU 维护一个 __softirq_pending 位图,记录哪些 Softirq 有待处理的工作:
static DEFINE_PER_CPU(__u32, __softirq_pending);当 Top Half 需要触发某个 Softirq 时,调用 raise_softirq() 设置对应位:
// kernel/softirq.c — 简化void raise_softirq(unsigned int nr){ unsigned long flags; local_irq_save(flags); raise_softirq_irqoff(nr); // 设置 __softirq_pending 的第 nr 位 local_irq_restore(flags);}这种 per-CPU 设计消除了多核之间的锁竞争——每个 CPU 只操作自己的 pending 位图,不需要任何同步。
5.3 __do_softirq 的执行逻辑
__do_softirq() 是 Softirq 的核心执行引擎。它在以下时机被调用:
- 中断返回时:
irq_exit()→invoke_softirq()→__do_softirq() - 本地开中断时:
local_bh_enable()检测到 pending 后调用 - ksoftirqd 内核线程:当 Softirq 过多时由守护线程处理
// kernel/softirq.c — __do_softirq 核心逻辑(简化)asmlinkage __visible void __softirq_entry __do_softirq(void){ unsigned long end = jiffies + MAX_SOFTIRQ_TIME; // 最多执行 2ms int max_restart = MAX_SOFTIRQ_RESTART; // 最多轮询 10 次 struct softirq_action *h; __u32 pending;
pending = local_softirq_pending(); // 读取本 CPU 的 pending 位图 __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
restart: while ((softirq_bit = ffs(pending))) { // 找到最低位的待处理 softirq unsigned int vec_nr = softirq_bit - 1; h = softirq_vec + vec_nr; pending >>= softirq_bit;
h->action(h); // 执行 softirq 处理函数 }
// 检查是否有新的 softirq 被触发 pending = local_softirq_pending(); if (pending && --max_restart && time_before(jiffies, end)) goto restart;
// 如果仍有未处理的 softirq,唤醒 ksoftirqd if (pending) wakeup_softirqd();}__do_softirq() 有两个重要的限制机制:
- 时间限制:最多执行
MAX_SOFTIRQ_TIME(2ms),防止 Softirq 长时间霸占 CPU - 轮询限制:最多轮询
MAX_SOFTIRQ_RESTART(10 次),防止新触发的 Softirq 导致无限循环
超过限制后,剩余的 Softirq 交给 ksoftirqd 内核线程在进程上下文中处理,确保不会饿死用户进程。
5.4 ksoftirqd 守护线程
每个 CPU 有一个 ksoftirqd/%u 内核线程,当 Softirq 负载过重时被唤醒:
static int ksoftirqd_should_run(unsigned int cpu){ return local_softirq_pending();}
static void run_ksoftirqd(unsigned int cpu){ local_irq_disable(); if (local_softirq_pending()) { __do_softirq(); // ... } local_irq_enable();}ksoftirqd 的调度优先级为 SCHED_OTHER,与普通进程公平竞争 CPU。这确保了:当 Softirq 持续大量触发时(如网络风暴),不会无限抢占 CPU,用户进程仍有机会运行。
你可以通过 ps -ef | grep ksoftirqd 看到每个 CPU 核心对应的 ksoftirqd 线程。当网络负载很高时,这些线程的 CPU 占用率会明显上升。
六、Tasklet:基于 Softirq 的动态延迟机制
6.1 Tasklet 的设计
Tasklet 构建在 Softirq 之上——HI_SOFTIRQ 和 TASKLET_SOFTIRQ 分别对应高优先级和普通优先级的 Tasklet。与 Softirq 不同,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; // 传递给处理函数的参数};6.2 Tasklet 的执行保证
Tasklet 的一个关键特性是同类型 Tasklet 的串行化:
- 同一个 Tasklet 不会在多个 CPU 上同时执行(通过
TASKLET_STATE_RUN标志保证) - 不同 Tasklet 可以在不同 CPU 上并行执行
这意味着驱动开发者不需要在 Tasklet 处理函数中使用自旋锁来保护 Tasklet 自身的数据——内核已经保证了串行化。但如果有其他上下文(如进程上下文或中断处理程序)也访问同一数据,仍需加锁。
6.3 Tasklet 的注册与调度
// 定义并初始化 taskletDECLARE_TASKLET(my_tasklet, my_tasklet_func, data);
// 在中断处理程序中调度tasklet_schedule(&my_tasklet); // 普通 tasklet(TASKLET_SOFTIRQ)tasklet_hi_schedule(&my_tasklet); // 高优先级 tasklet(HI_SOFTIRQ)
// 禁用 / 启用tasklet_disable(&my_tasklet);tasklet_enable(&my_tasklet);
// 销毁tasklet_kill(&my_tasklet); // 等待执行完毕后销毁Tasklet 在新代码中已不推荐使用。内核文档明确建议:新代码应优先使用 Softirq(对性能要求极高的场景)或 Workqueue(需要睡眠或更灵活的场景)。Tasklet 的串行化语义虽然简化了编程,但限制了多核扩展性。
七、Workqueue:可睡眠的延迟工作队列
7.1 为什么需要 Workqueue
Softirq 和 Tasklet 都运行在中断上下文中,不能睡眠。但很多延迟工作需要调用可能阻塞的函数——比如 kmalloc(GFP_KERNEL) 分配内存、mutex_lock() 获取互斥锁、msleep() 等待一段时间。Workqueue 正是为了满足这类需求而设计的。
Workqueue 的核心思想:将延迟工作提交给内核线程执行。内核线程运行在进程上下文中,可以自由地睡眠、调度、访问用户空间。
7.2 Workqueue 的核心数据结构
struct work_struct { atomic_long_t data; // 标志 + 入参 struct list_head entry; // 链表节点 work_func_t func; // 处理函数};
// 延迟工作struct delayed_work { struct work_struct work; struct timer_list timer; // 内部定时器,到期后才提交};7.3 系统 Workqueue 与自定义 Workqueue
Linux 提供了预定义的系统 Workqueue,驱动可以直接使用:
// 提交到系统默认 workqueue(events)schedule_work(&my_work);schedule_delayed_work(&dwork, delay);
// 提交到特定 CPU 的 workqueueschedule_work_on(cpu, &my_work);对于有特殊需求(如并发控制、CPU 亲和性、优先级)的场景,可以创建自定义 Workqueue:
// 创建自定义 workqueuestruct workqueue_struct *wq;wq = alloc_workqueue("my-wq", WQ_UNBOUND | WQ_HIGHPRI, 0);
// 提交工作queue_work(wq, &my_work);queue_delayed_work(wq, &dwork, delay);
// 销毁destroy_workqueue(wq);7.4 CMWQ:并发管理工作队列
Linux 2.6.36 引入了 CMWQ(Concurrency Managed Workqueue),彻底重构了 Workqueue 的实现。CMWQ 的核心改进:
- 动态工作线程池:不再为每个 Workqueue 分配固定线程,而是维护 per-CPU 的共享工作线程池
- 并发管理:根据当前执行中的工作数量,自动调整活跃工作线程数
- 前向推进保证:即使某些工作阻塞,也不会阻止同一线程池中的其他工作执行
CMWQ 的关键标志位:
| 标志 | 含义 |
|---|---|
WQ_UNBOUND | 工作线程不绑定特定 CPU,可迁移 |
WQ_HIGHPRI | 高优先级工作队列 |
WQ_CPU_INTENSIVE | CPU 密集型工作,不参与并发管理 |
WQ_MEM_RECLAIM | 内存回收时仍可执行(用于内存路径) |
八、Threaded IRQ:中断线程化
8.1 从硬中断到线程化
Linux 2.6.39 引入了 Threaded IRQ 机制,允许将中断处理程序放入内核线程中执行。注册中断时设置 IRQF_ONESHOT 标志,request_threaded_irq() 会创建一个专用的内核线程来执行处理函数:
// 注册线程化中断int request_threaded_irq(unsigned int irq, irq_handler_t handler, // 硬中断处理(Top Half) irq_handler_t thread_fn, // 线程化处理(Bottom Half) unsigned long flags, const char *devname, void *dev_id);handler:在硬中断上下文中执行,返回IRQ_WAKE_THREAD时唤醒线程thread_fn:在内核线程中执行,可以睡眠
Threaded IRQ 的优势:
- 简化驱动开发:驱动开发者不再需要区分 Top Half 和 Bottom Half,只需将紧急操作放在
handler中,其余放在thread_fn中 - 改善实时性:中断线程化后,中断处理程序与普通进程一样参与调度,不会无限制地抢占进程
- 更好的隔离:每个设备的中断处理有独立的线程,一个设备的中断处理阻塞不会影响其他设备
在 PREEMPT_RT(实时内核)补丁中,所有中断都被强制线程化,这是实现确定性延迟的关键机制。你可以通过 ps -ef | grep irq 看到系统中的中断线程。
九、IRQ Affinity 与多核中断分发
9.1 IRQ Affinity
在多核系统中,中断默认可能被路由到任意 CPU。IRQ Affinity 允许管理员指定某个中断只由特定 CPU 处理,这在以下场景中非常有用:
- NUMA 优化:将网卡中断绑定到与网卡同 NUMA 节点的 CPU,减少跨节点内存访问延迟
- CPU 隔离:将中断从实时任务的 CPU 上移走,避免中断干扰
- 负载均衡:将高频中断分散到多个 CPU,避免单 CPU 过载
# 查看某个 IRQ 的亲和性掩码cat /proc/irq/32/smp_affinity
# 设置 IRQ 亲和性(十六进制掩码)echo 0f > /proc/irq/32/smp_affinity # 只允许 CPU 0-3 处理
# 查看当前由哪个 CPU 处理cat /proc/irq/32/smp_affinity_list # 十进制范围格式,如 0-39.2 IRQ Balance
irqbalance 守护进程自动在多个 CPU 之间分发中断,优化缓存局部性和负载均衡。它根据中断频率、NUMA 拓扑和缓存层次结构,定期调整 IRQ Affinity。
# 安装 irqbalancesudo apt install irqbalance
# 查看状态systemctl status irqbalance
# 手动运行(调试模式)irqbalance --debug对于高性能网络场景(如 10Gbps+ 网卡),通常需要手动配置 RSS(Receive Side Scaling),将网卡的多队列分别绑定到不同 CPU,实现中断的并行处理:
# 查看网卡队列数ethtool -l eth0
# 设置队列数ethtool -L eth0 combined 8
# 将每个队列的中断绑定到不同 CPUfor i in $(seq 0 7); do echo $((1 << i)) > /proc/irq/$(cat /proc/interrupts | grep "eth0-TxRx-$i" | awk -F: '{print $1}' | tr -d ' ')/smp_affinitydone十、中断统计与观测
10.1 /proc/interrupts
/proc/interrupts 提供了系统中每个 IRQ 的详细统计:
cat /proc/interrupts输出示例:
CPU0 CPU1 CPU2 CPU3 0: 50 0 0 0 IO-APIC 2-edge timer 8: 1 0 0 0 IO-APIC 8-edge rtc0 24: 1234567 987654 567890 345678 PCI-MSI 524288-edge eth0-TxRx-0 25: 987654 1234567 345678 567890 PCI-MSI 524289-edge eth0-TxRx-1NMI: 123 456 789 012 Non-maskable interruptsLOC: 98765432 87654321 76543210 65432109 Local timer interrupts每一列的含义:
- IRQ 编号:中断向量号
- CPUn 列:每个 CPU 上该中断的触发次数
- 中断控制器类型:IO-APIC、PCI-MSI 等
- 设备名称:触发中断的设备
10.2 /proc/softirqs
/proc/softirqs 提供了每个 CPU 上各类型 Softirq 的执行统计:
cat /proc/softirqs输出示例:
CPU0 CPU1 CPU2 CPU3 HI: 3 0 0 2 TIMER: 1234567 1123456 1098765 1054321 NET_TX: 12345 11234 10987 10543 NET_RX: 9876543 8765432 7654321 6543210 BLOCK: 543210 432109 321098 210987 IRQ_POLL: 0 0 0 0 TASKLET: 234 123 345 456 SCHED: 2345678 2234567 2123456 2012345 HRTIMER: 123456 112345 101234 90123 RCU: 3456789 3345678 3234567 3123456通过观察 NET_RX 和 NET_TX 的数值变化,可以判断网络负载;BLOCK 反映块设备 I/O 活动;RCU 反映 RCU 宽限期处理频率。
10.3 perf 中断追踪
perf 是分析中断性能问题的利器:
# 统计中断处理时间perf stat -e irq:irq_handler_entry,irq:irq_handler_exit -a sleep 5
# 追踪特定 IRQ 的处理perf record -e irq:irq_handler_entry -a -- sleep 10perf report
# 追踪 softirqperf record -e irq:softirq_entry -a -- sleep 10perf report
# 使用 tracepoint 追踪中断perf trace -e irq:*十一、动手实践
实践 1:观察系统中断分布
# 查看中断统计cat /proc/interrupts
# 持续监控中断变化(每秒刷新)watch -n 1 cat /proc/interrupts
# 查看 softirq 统计cat /proc/softirqs
# 持续监控 softirqwatch -n 1 cat /proc/softirqs尝试在监控期间执行网络操作(ping、curl)或磁盘操作(dd),观察 NET_RX、NET_TX、BLOCK 的变化。
实践 2:调整 IRQ Affinity
# 查看网卡中断的当前亲和性grep eth0 /proc/interrupts# 假设 eth0 使用 IRQ 32 和 33
# 查看 IRQ 32 的亲和性cat /proc/irq/32/smp_affinity_list
# 将 IRQ 32 绑定到 CPU 0echo 0 > /proc/irq/32/smp_affinity_list
# 将 IRQ 33 绑定到 CPU 1echo 1 > /proc/irq/33/smp_affinity_list
# 验证cat /proc/irq/32/smp_affinity_listcat /proc/irq/33/smp_affinity_list实践 3:编写内核模块注册中断处理程序
以下是一个完整的内核模块示例,注册一个共享中断处理程序,并在中断触发时调度一个 Tasklet 和一个 Work:
// irq_demo.c — 中断处理演示模块#include <linux/module.h>#include <linux/interrupt.h>#include <linux/workqueue.h>
static int irq_demo_irq = 1; // 默认键盘中断module_param(irq_demo_irq, int, 0644);MODULE_PARM_DESC(irq_demo_irq, "IRQ number to hook");
/* Tasklet 处理函数 */static void irq_demo_tasklet_func(unsigned long data){ pr_info("irq_demo: tasklet executed on CPU %d\n", smp_processor_id());}
DECLARE_TASKLET(irq_demo_tasklet, irq_demo_tasklet_func, 0);
/* Work 处理函数 */static void irq_demo_work_func(struct work_struct *work){ pr_info("irq_demo: work executed on CPU %d (can sleep!)\n", smp_processor_id()); msleep(100); // Workqueue 中可以睡眠! pr_info("irq_demo: work completed after sleeping\n");}
static DECLARE_WORK(irq_demo_work, irq_demo_work_func);
/* 中断处理程序(Top Half) */static irqreturn_t irq_demo_handler(int irq, void *dev_id){ pr_info("irq_demo: ISR executed on CPU %d, irq=%d\n", smp_processor_id(), irq);
/* 调度 Bottom Half */ tasklet_schedule(&irq_demo_tasklet); schedule_work(&irq_demo_work);
return IRQ_HANDLED;}
static int __init irq_demo_init(void){ int ret;
ret = request_irq(irq_demo_irq, irq_demo_handler, IRQF_SHARED, "irq_demo", (void *)&irq_demo_irq); if (ret) { pr_err("irq_demo: request_irq failed: %d\n", ret); return ret; }
pr_info("irq_demo: registered IRQ %d\n", irq_demo_irq); return 0;}
static void __exit irq_demo_exit(void){ free_irq(irq_demo_irq, (void *)&irq_demo_irq); tasklet_kill(&irq_demo_tasklet); cancel_work_sync(&irq_demo_work); pr_info("irq_demo: unregistered\n");}
module_init(irq_demo_init);module_exit(irq_demo_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("Linux Internals Series");MODULE_DESCRIPTION("Interrupt handling demo with tasklet and workqueue");编译并加载模块:
# Makefilecat > Makefile << 'EOF'obj-m += irq_demo.oKDIR := /lib/modules/$(shell uname -r)/build
all: make -C $(KDIR) M=$(PWD) modules
clean: make -C $(KDIR) M=$(PWD) cleanEOF
# 编译make
# 加载模块(注意:共享中断需要谨慎操作)sudo insmod irq_demo.ko irq_demo_irq=1
# 查看内核日志sudo dmesg | tail -20
# 按键盘触发中断,观察日志# 你应该能看到 ISR → tasklet → work 的执行顺序
# 卸载sudo rmmod irq_demo实践 4:使用 perf 追踪中断
# 追踪所有中断事件 10 秒sudo perf record -e irq:irq_handler_entry,irq:irq_handler_exit -a -- sleep 10sudo perf report
# 追踪 softirq 事件sudo perf record -e irq:softirq_entry,irq:softirq_exit -a -- sleep 10sudo perf report
# 实时追踪中断(类似 strace)sudo perf trace -e irq:irq_handler_entry -- sleep 5
# 统计中断处理时间分布sudo perf stat -e irq:irq_handler_entry,irq:softirq_entry -a sleep 5实践 5:观察 ksoftirqd 行为
# 查看 ksoftirqd 线程ps -eo pid,comm,psr | grep ksoftirqd
# 制造网络负载,观察 ksoftirqd 的 CPU 使用iperf3 -s & # 启动服务端iperf3 -c localhost & # 启动客户端top -H -p $(pgrep ksoftirqd) # 监控 ksoftirqd
# 同时观察 softirq 统计变化watch -n 0.5 "cat /proc/softirqs | head -5"参考资料
内核源码
| 路径 | 说明 |
|---|---|
arch/x86/kernel/idt.c | IDT 表的初始化与门描述符设置 |
arch/x86/entry/entry_64.S | 中断入口汇编代码 |
kernel/irq/ | IRQ 核心子系统(desc.c、manage.c、chip.c 等) |
kernel/softirq.c | Softirq 与 Tasklet 实现 |
kernel/workqueue.c | Workqueue(CMWQ)实现 |
include/linux/interrupt.h | 中断相关数据结构与 API 声明 |
include/linux/irqdesc.h | struct irq_desc 定义 |
经典书籍
- 《Linux 内核设计与实现》(Robert Love)— 第 7 章对中断和下半部机制有清晰讲解
- 《深入理解 Linux 内核》(Bovet & Cesati)— 第 4 章详细分析中断与异常处理
- 《Linux 设备驱动程序》(Corbet 等)— 第 10 章聚焦驱动的中断处理实践
在线资源
- Linux 内核文档 — IRQ 子系统
- Bootlin Elixir — kernel/softirq.c
- LWN.net: How the RCU API works — RCU Softirq 相关
- LWN.net: Threaded IRQ handlers — 中断线程化设计
本章从中断的硬件本质出发,逐层剖析了 Linux 中断子系统的完整架构:IDT 门描述符表将硬件信号映射到软件处理程序,Top Half / Bottom Half 分离在响应性与吞吐量之间取得平衡,Softirq 提供了高性能的并行延迟处理能力,Tasklet 简化了驱动的编程模型,Workqueue 则为可睡眠的延迟工作提供了进程上下文。
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






