Appearance
中断与设备驱动
- 写作时间:
2026-03-04 首次提交,2026-07-13 最近修改 - 当前字符:
7924
I/O 系统把一次请求追到设备控制器:驱动提交 DMA 描述符,设备完成后触发中断,内核再结束请求并唤醒等待任务。但“触发中断”只是硬件通知的开始。CPU 为什么进入正确处理函数,多个设备如何把通知路由到不同核心,中断处理为何不能完成所有后续工作,这些问题决定了驱动能否及时响应设备,同时限制对正常任务的打断时间。
本课先从 异常与中断 的统一入口开始,明确同步异常、外部中断以及 IDT 的职责;然后看 中断控制器 怎样收集、编号并路由硬件通知;处理函数进入内核后,上半部与下半部 把紧急工作和耗时工作拆开;不同耗时工作再落到 延后执行 机制,包括 softirq、tasklet、workqueue 和线程化中断;最后用 Linux 设备模型 把驱动、设备、总线和 sysfs 的生命周期关系接起来。
异常与中断
异常是处理器因指令执行或体系结构条件产生的控制转移,其中大多数与当前指令同步;外部中断则通常由设备或控制器异步于当前指令发出。
特权边界已经定义过这两类事件,也介绍了 x86 用 IDT 记录异常与中断向量对应入口。除零异常、缺页异常和设备中断通过 IDT 进入内核,处理器保存体系结构规定的返回状态并按需要切换特权级。现代 x86-64 的 syscall 指令则使用由专用寄存器配置的快速入口,不查 IDT;只有 int 指令等基于软件中断的路径才使用 IDT 向量。系统调用、异常和外部中断都能跨越特权边界,但入口机制不能合并成同一种。
| 类型 | 与当前指令的关系 | 典型例子 | 返回后的行为 |
|---|---|---|---|
| 故障(Fault) | 指令执行前或执行中发现可修复条件 | 缺页异常 | 修复后通常重新执行原指令 |
| 陷阱(Trap) | 指令已经完成后报告事件 | 断点、单步调试 | 从下一条指令继续 |
| 中止(Abort) | 严重错误,无法可靠恢复当前执行流 | 机器检查中的部分情况 | 通常终止任务或系统 |
| 外部中断 | 与当前指令无直接因果关系 | 网卡、磁盘、定时器 | 处理后恢复被打断执行流 |
IDT 只解决“向量号对应哪个架构入口”。入口代码还要判断事件打断用户态还是内核态、是否切换到特权栈或专用中断栈、哪些寄存器必须保存,以及何时允许重新开启可屏蔽中断。中断请求(Interrupt Request, IRQ)是设备或控制器请求处理器响应事件的通知;架构代码完成入口处理后,才把硬件中断交给 Linux 通用 IRQ 子系统。
外部中断可能在任意指令之间到来,所以驱动不能假设当前进程正在等待该设备。CPU 也可能正在执行另一个进程甚至内核代码。中断处理必须依靠设备队列和请求对象找到真正的 I/O 所有者,再标记完成并唤醒相应等待队列。
中断控制器
中断控制器(interrupt controller)是收集硬件中断请求、分配或携带中断向量,并把通知路由到目标 CPU 的硬件。
早期 PC 使用可编程中断控制器(Programmable Interrupt Controller, PIC),可用中断线和多核路由能力有限。现代 x86 系统主要使用高级可编程中断控制器(Advanced Programmable Interrupt Controller, APIC)体系:每个逻辑 CPU 有本地 APIC,I/O APIC 接收主板设备的引脚中断,再把通知投递到目标本地 APIC。
这里需要区分三个编号。硬件中断号(hwirq)是某个控制器内部的来源编号,Linux IRQ 编号是通用 IRQ 子系统分配的软件编号,x86 中断向量则是 CPU 查 IDT 时使用的编号。irq_domain 把控制器 hwirq 映射成 Linux IRQ 编号,架构代码再为投递分配向量;三者可能数值不同,驱动不应把它们当成同一个全局编号。
传统中断线是电气信号,多台设备共享同一条线时,每个处理函数都要检查自己的设备状态,不是来源时返回 IRQ_NONE。外围组件互连(Peripheral Component Interconnect, PCI)及其高速串行版本 PCI Express(PCIe)是连接处理器与外设的总线标准。PCI/PCIe 设备可以使用消息信号中断(Message Signaled Interrupt, MSI),通过特殊内存写事务携带中断信息,不再依赖专用引脚。MSI-X 为同一设备提供更多可独立配置和屏蔽的向量,使网卡或多队列存储控制器能够使用不同 IRQ。
多队列设备因此可以建立局部路径:某 CPU 向本地软件队列提交,硬件完成后用对应 MSI-X IRQ 通知同一 CPU,减少共享队列和缓存行迁移。但提交 CPU、硬件队列和中断目标并非天然固定一致,驱动、块层或网络子系统需要建立映射。Linux 调度器介绍的 CPU 亲和性约束任务可以在哪些 CPU 上运行;中断亲和性(interrupt affinity)则规定 IRQ 可以投递到哪些 CPU。内核、驱动、用于在 CPU 间分配 IRQ 的用户态 irqbalance 服务或管理员都可能调整它。
驱动通过 request_irq() 或 request_threaded_irq() 把 Linux IRQ 编号与处理函数关联。注册成功后中断可能立即到来,因此设备状态、处理函数所需内存和中断屏蔽顺序必须先准备正确。共享 IRQ 的处理函数还必须验证中断是否来自自己的设备,并分别返回 IRQ_HANDLED 或 IRQ_NONE。
“清除设备中断原因”和“向中断控制器发送处理结束”是两个层次。对于电平触发中断,驱动若不清除设备状态,中断线会继续保持有效;本地 APIC 的中断结束(End Of Interrupt, EOI)等控制器确认通常由架构和通用 IRQ 代码完成,而不是每个驱动直接操作 APIC。中断芯片抽象(IRQ chip)封装了具体控制器的屏蔽、确认和 EOI 操作,这些操作的时机取决于 IRQ chip 与触发方式,不能套用一个固定顺序。
上半部与下半部
上半部(top half)是在硬件中断到来后立即执行的紧急处理,下半部(bottom half)是被推迟到稍后执行的非紧急工作。
硬中断处理期间,当前 CPU 的正常执行流被打断,本地普通抢占不可用,相关 IRQ 也可能被屏蔽。处理函数如果大量计算、争用自旋锁或反复访问慢设备,会延迟该 CPU 处理其他中断和任务。因此上半部只做必须立即完成的工作:确认来源、清除或屏蔽设备状态、读取必要完成信息,并安排后续处理。互斥锁、阻塞式 I/O 和允许睡眠等待内存的 GFP_KERNEL 分配都不能放在硬中断上下文中。
网卡收包可以展示这条边界。NAPI 是 Linux 网络栈处理收发事件的机制,这个名称目前不再代表某个英文短语。硬中断处理函数屏蔽或抑制该队列的后续通知,并调用 napi_schedule();内核随后在稍后运行、但仍不能睡眠的上下文中调用驱动的轮询方法,按处理预算(budget)批量处理接收包。budget 是一次轮询允许处理的接收包上限,防止单个繁忙队列长期占用 CPU。工作完成后 NAPI 才重新启用设备中断,从而减少高包速率下的中断风暴。
为什么硬中断处理函数不能睡眠?
睡眠意味着当前可调度任务改变状态,把 CPU 交给调度器,并在条件满足后恢复。硬中断处理不是独立可调度任务,内核中表示当前任务的 current 仍指向被打断的任务,体系结构还可能使用专用 IRQ 栈。调度器没有一个可以安全标记为睡眠并稍后恢复的“硬中断任务”。
硬中断还可能持有自旋锁、关闭本地中断或处于设备必须尽快确认的协议阶段。若此时调用睡眠接口,锁与设备状态可能长期得不到释放,甚至造成死锁。因此需要阻塞的工作必须推迟到可调度的内核线程中执行;下一节再为这类“现在登记、稍后运行”的安排建立统一名称。
上下半部不是两个固定 API 的名字,而是一种职责划分。Linux 曾经有名为 BH 的历史机制,现代内核则用 softirq、workqueue、线程化中断等多种设施实现“稍后做”。选择哪一种,取决于工作是否允许睡眠、是否需要高并发以及延迟要求。
延后执行
延后执行(deferred execution)是把中断或其他原子上下文中不必立即完成的工作排入稍后运行的执行环境。
Linux 提供了几种不同约束的机制:
| 机制 | 执行上下文 | 能否睡眠 | 并发特征 | 典型用途 |
|---|---|---|---|---|
| 软中断(softirq) | 软中断上下文 | 不能 | 同类可在不同 CPU 并行 | 网络收发、定时器等高频核心子系统 |
| tasklet | 基于 softirq | 不能 | 同一个 tasklet 实例不并发执行 | 兼容旧驱动的遗留机制 |
| 工作队列(workqueue) | 内核工作线程的进程上下文 | 可以 | 由工作线程池调度 | 可能阻塞、需要互斥锁或较耗时的工作 |
| 线程化中断 | 专用内核线程 | 可以 | 每个 IRQ 可有独立线程函数 | 大部分处理可推迟且希望保留 IRQ 语义的驱动 |
softirq 的类型在编译时固定,适合网络和定时器等少数高频核心子系统。它可以在硬中断返回路径中执行,也可能由每 CPU 的 ksoftirqd 内核线程接手;即使代码恰好由 ksoftirqd 运行,softirq 处理函数仍遵守软中断上下文规则,不能调用会睡眠的接口。单次工作还必须有界,NAPI 的 budget 就用于限制接收批次。
tasklet 是建立在 softirq 之上的遗留抽象。它保证同一个 tasklet 实例不会在多个 CPU 同时执行,但仍处于不能睡眠的原子上下文。新驱动应优先选择 workqueue、线程化中断或子系统专用机制,不能把 tasklet 当作默认延后接口。
workqueue 把 work_struct 排入内核工作线程池(worker pool)。工作函数运行在可调度进程上下文中,可以获取 mutex、等待 I/O 或执行允许睡眠的分配。普通 workqueue 可能并发运行多个工作项,也不自动保证提交顺序;需要串行语义时应选择有序 workqueue 或自行同步。取消设备工作时还要使用同步取消接口,确认函数已经退出后才能释放其数据。
request_threaded_irq() 允许驱动注册主处理函数(primary handler)和线程函数 thread_fn。主处理函数仍在硬中断上下文中检查来源,完成设备要求的最小确认或屏蔽操作,再返回 IRQ_WAKE_THREAD;内核随后唤醒 IRQ 线程执行可睡眠的 thread_fn。驱动若使用 IRQF_ONESHOT,通用 IRQ 层会在线程函数结束前保持该中断线被屏蔽。设备移除时,synchronize_irq() 等接口用于等待正在执行的硬处理函数和线程函数结束,防止处理函数访问已经释放的设备状态。
Linux 设备模型
Linux 设备模型是用总线、设备、驱动和统一对象生命周期描述硬件拓扑的框架。
sysfs 是挂载在 /sys、用于导出当前内核对象及其属性的内存文件系统。设备模型把总线、设备和驱动的关系放进这棵目录树,用户空间因而可以观察当前硬件拓扑,而不必读取驱动私有内存。
驱动不只是几组 read() 和 write() 函数。设备可能热插拔,驱动可能以模块形式加载,父子设备必须按依赖顺序创建和销毁,用户空间也需要观察当前拓扑。Linux 用 struct device 表示设备实例,用 struct device_driver 表示可绑定的驱动,用 struct bus_type 提供总线的枚举与匹配规则。驱动注册和设备发现谁先发生都可以,驱动核心(driver core)会在两侧对象出现时尝试匹配。
以 PCI 为例,内核枚举总线后为每个硬件功能创建 struct pci_dev,记录厂商 ID、设备 ID、基址寄存器(Base Address Register, BAR)和中断资源。BAR 描述设备需要的 MMIO 或端口 I/O 地址范围。PCI 驱动注册时提供支持的 ID 表,总线匹配成功后调用 probe();probe() 在进程上下文中验证设备、申请资源、设置表示设备可寻址位宽的 DMA mask、初始化队列、注册中断并向所属子系统注册设备。成功返回 0 才完成绑定,失败必须释放已经取得的资源;依赖尚未就绪时还可返回 -EPROBE_DEFER 请求稍后重试。
设备拔出或驱动卸载会触发解绑和 remove()。正确顺序不只是把 probe() 倒写一遍:驱动先阻止新请求,按设备协议停止 DMA 并屏蔽新的中断,再同步等待已在运行的 IRQ、workqueue 和计时器退出,最后才释放队列、映射和对象。否则设备仍可能向已释放页执行 DMA,或者延后函数继续解引用失效指针。
内核对象(kobject)是设备模型中提供引用计数、父子层次、名称和 sysfs 表示的基础对象。struct device 内部包含 kobject,设备注册后会在 /sys/devices 下形成目录;总线、驱动和设备类(class)再通过链接组织出 /sys/bus、/sys/class 等视图。设备类按用户可见功能组织设备,例如网络接口或块设备,不等同于物理总线。
这些 sysfs 目录随设备生命周期变化,不是磁盘上的持久配置。设备增删还会产生 uevent,用户空间设备管理器可以据此创建节点、加载规则或启动处理。引用计数归零后,设备的 release 回调才允许释放包含 struct device 的最终内存,因而“从 sysfs 消失”和“对象内存已经释放”也可能是不同时间点。
设备模型负责发现、匹配和生命周期,具体数据传输仍由设备驱动和所属子系统完成。把两者分开以后,同一个 PCI 枚举框架可以服务网卡、存储控制器和声卡,而各自驱动只实现本设备协议与上层接口。
小结
| 概念 | 说明 |
|---|---|
| IDT | 记录异常或中断向量对应入口的 x86 描述符表 |
| 故障 / 陷阱 / 中止 | x86 按报告位置和可恢复性区分的异常类别 |
| hwirq / Linux IRQ / 中断向量 | 分别表示控制器来源、内核通用编号和 CPU 的 IDT 向量 |
| APIC | 现代 x86 中负责中断接收、路由和投递的控制器体系 |
| MSI / MSI-X | 设备通过消息写事务发送中断,并可为多队列分配多个向量 |
| 上半部 | 中断到来后立即完成的最小紧急处理 |
| 下半部 | 推迟执行的协议处理、完成回调或其他非紧急工作 |
| softirq | 不能睡眠、适合高频内核子系统的延后执行机制 |
| NAPI | 网卡从中断切换到有处理预算的批量轮询,再恢复中断的事件处理机制 |
| workqueue | 在可调度内核工作线程中运行、允许睡眠的延后执行机制 |
| 线程化中断 | 用硬处理函数确认来源,再由 IRQ 线程执行可睡眠工作 |
| Linux 设备模型 | 统一描述总线、设备、驱动、生命周期和 sysfs 拓扑的框架 |
驱动处理中断的关键不是“尽快把所有工作做完”,而是只在不可推迟的上下文中完成最少工作,再把其余部分交给约束合适的执行环境。
源码与文档入口:
arch/x86/kernel/idt.c:x86 IDT 初始化kernel/irq/:通用 IRQ 子系统kernel/softirq.c:softirq 与 taskletkernel/irq/manage.c:IRQ 注册、同步和线程化中断drivers/base/:Linux 设备模型核心- Linux generic IRQ handling:IRQ 注册、亲和性、同步和线程化接口
- NAPI:网络中断、poll 方法和 budget
- The MSI Driver Guide:MSI/MSI-X 向量分配与亲和性
- Workqueue:worker pool、并发和有序工作队列
- Driver Binding:设备与驱动匹配、probe 和 remove
- The Linux Kernel Device Model:总线、设备、驱动与 sysfs