6Linux 内核设计与实现(第3版)

中断和中断处理

异步事件

中断中断处理上半部下半部

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第6章 中断和中断处理(Interrupts and Interrupt Handling)

一、导读

1.1 本章学习目标

理解中断的基本概念:什么是中断、为什么需要中断机制

区分硬件中断和软件中断(异常/系统调用)的不同处理方式

掌握 Linux 中断处理的上半部(top half)和下半部(bottom half)分工机制

深入理解软中断(softirq)、tasklet 和工作队列(workqueue)三种下半部机制

了解中断上下文(interrupt context)的限制和特点

掌握中断请求(IRQ)的注册、共享和控制方法

理解中断亲和性(IRQ affinity)的概念及其在多核系统中的优化作用

了解中断风暴(interrupt storm)的原因和防护措施

1.2 为什么本章如此重要

中断是操作系统与硬件交互的核心机制。没有中断,CPU 只能通过轮询(polling)来检查设备状态,这不仅浪费 CPU 资源,而且无法及时响应硬件事件。中断机制使得设备可以在需要服务时主动通知 CPU,让 CPU 在没有中断时执行其他任务,极大地提高了系统效率和响应性。

中断处理的质量直接影响系统的性能和稳定性。一个设计不良的中断处理程序可能导致系统延迟增加、吞吐量下降,甚至系统崩溃。在高性能网络场景中,每秒可能处理数百万个中断,中断处理的效率成为系统瓶颈。Linux 的中断处理架构经过多次优化,从最初的所有处理都在中断上下文中完成,到引入下半部机制将耗时操作延后执行,再到 IRQ threading(中断线程化)进一步提高响应性。

对于设备驱动开发者而言,中断处理是最常涉及的子系统之一。正确注册中断处理函数、合理分配上半部和下半部的工作、避免中断上下文中的错误操作,是编写可靠驱动程序的基本要求。

1.3 核心问题引导

中断和异常有什么区别?CPU 如何处理它们?

为什么中断处理要分为上半部和下半部?这种设计解决了什么问题?

中断上下文为什么不能睡眠?如果在中断处理中调用可能睡眠的函数会发生什么?

软中断、tasklet 和工作队列各自适用于什么场景?它们之间有什么区别?

多个设备如何共享同一个中断线?内核如何确定哪个设备触发了中断?

什么是中断亲和性?如何利用它优化多核系统的网络性能?

什么是中断风暴?内核如何防止一个失控的设备耗尽 CPU 资源?

中断线程化(threaded interrupts)解决了什么问题?它是如何工作的?


二、核心概念详解

6.1 中断的本质与分类

中断是硬件设备向 CPU 发出的异步信号,表示设备需要 CPU 的关注。当中断到达时,CPU 暂停当前执行的代码,保存上下文,跳转到预先注册的中断处理程序(Interrupt Service Routine, ISR)。ISR 执行完毕后,CPU 恢复之前的上下文继续执行。

中断可以分为几类:硬件中断(Hardware Interrupt),由外部设备通过中断控制器(如 APIC)发出,如键盘输入、网络数据包到达、磁盘 I/O 完成;软件中断(Software Interrupt),由软件触发,如系统调用(通过 syscall 或 int 0x80 指令);异常(Exception),由 CPU 在执行指令时检测到异常条件触发,如缺页异常(page fault)、除零错误、无效指令等。

硬件中断又分为可屏蔽中断(Maskable Interrupt)和不可屏蔽中断(Non-Maskable Interrupt, NMI)。可屏蔽中断可以通过设置 CPU 的中断标志位来暂时禁止,内核在进入临界区时通常会禁止可屏蔽中断。NMI 用于处理严重的硬件事件(如内存校验错误),不能被软件禁止。

6.2 上半部与下半部

传统的中断处理方式是在 ISR 中完成所有工作,但这会导致中断被禁止的时间过长(因为处理同一个中断线的其他中断在 ISR 执行期间被禁止),影响系统的响应性和其他设备的性能。Linux 将中断处理分为两部分:

上半部(Top Half):在中断上下文中执行,完成最紧急的工作——确认中断、读取设备状态、将数据从设备缓冲区复制到内存缓冲区。上半部必须尽可能快地完成,因为它禁止了中断。

下半部(Bottom Half):在稍后的安全时间执行,完成耗时的工作——数据处理、协议解析、数据拷贝到用户空间等。下半部执行时,中断是开启的,可以响应新的中断。

这种分工的核心思想是:将中断处理中必须在原子上下文中完成的工作(上半部)和可以延后执行的工作(下半部)分离,最小化中断禁止的时间。

6.3 下半部机制

Linux 提供了三种主要的下半部机制:

软中断(SoftIRQ):软中断是一组静态定义的下半部接口,全局最多 10 种(如网络接收 NET_RX_SOFTIRQ、网络发送 NET_TX_SOFTIRQ、块设备 I/O BLOCK_SOFTIRQ 等)。软中断可以在多个 CPU 上并行执行,但同一种软中断不能同时在多个 CPU 上运行。软中断的触发通过 raise_softirq() 完成,在下次中断返回或内核调度时检查并执行。软中断适合对性能要求极高的场景,如网络数据包处理。

Tasklet:Tasklet 是基于软中断实现的,但提供了更灵活的动态注册机制。每个 tasklet 有一个处理函数和状态标志。与软中断不同,同一个 tasklet 保证不会在多个 CPU 上同时执行(通过原子标志实现),但不同的 tasklet 可以并行执行。Tasklet 通过 tasklet_schedule() 触发,在中断返回时执行。Tasklet 适合大多数设备驱动的下半部需求。

工作队列(Workqueue):工作队列将下半部工作放在内核线程(进程上下文)中执行。与软中断和 tasklet 不同,工作队列在进程上下文中运行,因此可以睡眠、可以阻塞、可以使用信号量等。工作队列通过 schedule_work() 提交工作项,由内核工作线程(如 kworker/N)执行。工作队列适合需要执行耗时操作或可能睡眠的下半部任务。

6.4 中断注册与共享

设备驱动通过 request_irq() 函数注册中断处理函数。该函数将 ISR 与一个 IRQ 号关联,并指定处理函数的标志(如 IRQF_SHARED 表示中断线可以共享)。当设备触发中断时,内核调用注册的 ISR。

中断共享是常见的需求,因为系统的 IRQ 数量有限。多个设备可以共享同一条中断线,前提是它们都使用 IRQF_SHARED 标志注册。当共享中断发生时,内核依次调用所有注册的 ISR。每个 ISR 通过检查设备状态寄存器来判断是否是自己设备触发的中断。如果没有设备需要服务,ISR 返回 IRQ_NONE;如果处理了中断,返回 IRQ_HANDLED。


三、深入分析与讨论

3.1 中断亲和性与多核优化

在多核系统中,中断亲和性(IRQ Affinity)允许将特定的中断绑定到指定的 CPU 核心。默认情况下,中断由内核的中断平衡器自动分配到各 CPU。但在高性能场景中,手动设置中断亲和性可以显著优化性能。

例如,在网络服务器中,将网卡的接收中断绑定到专门处理网络数据的 CPU 核心上,可以提高缓存局部性(网络数据和处理逻辑在同一个核心的缓存中),减少跨核通信的开销。通过 /proc/irq/[IRQ号]/smp_affinity 文件可以设置中断亲和性,值是一个 CPU 掩码。

Linux 的 irqbalance 守护进程负责自动平衡中断分配。它定期监测各 CPU 的中断负载,动态调整中断分配。但在需要精细控制的场景中,管理员通常会禁用 irqbalance 并手动配置。

3.2 中断线程化

中断线程化(Threaded Interrupts)是 Linux 2.6.30 引入的特性,将 ISR 的大部分工作放在内核线程中执行。在中断线程化模式下,上半部只负责确认中断和唤醒线程,实际的处理工作在内核线程中完成。这样做的好处是:中断处理可以使用完整的内核 API(包括可能睡眠的操作);中断处理可以被其他中断抢占,提高响应性;可以使用标准的进程调度机制来管理中断处理的优先级。

中断线程化对于实时系统尤为重要。在实时内核配置(CONFIG_PREEMPT_RT)中,几乎所有中断处理都被线程化,以最小化中断禁止的时间。


四、实践案例

4.1 案例一:分析 /proc/interrupts 的输出

通过 cat /proc/interrupts 可以查看系统中每个 IRQ 的中断计数和分配情况。分析输出可以了解:每个 CPU 处理了哪些中断、各类中断的频率、中断在 CPU 之间的分布是否均衡。例如,如果某个 CPU 处理了绝大多数网络中断,可能说明中断分配不均衡,可以通过调整 IRQ 亲和性来优化。

4.2 案例二:编写简单的中断处理内核模块

编写一个内核模块,注册一个中断处理函数(可以使用共享的 IRQ 或通过 GPIO 模拟),在中断处理中记录时间戳,统计中断频率。通过上半部记录中断到达时间,下半部(使用 tasklet)处理数据分析。这个练习帮助理解中断注册、上半部/下半部分工、以及中断统计的基本方法。


五、常见误区与注意事项

误区:中断处理越快越好,应该把所有工作放在上半部。上半部在中断禁止的环境中执行,过长的上半部会导致其他中断延迟,降低系统响应性。应尽量将非紧急工作移到下半部。

误区:在中断上下文中可以使用任何内核 API。中断上下文不能睡眠,因此不能调用 kmalloc(GFP_KERNEL)(可能睡眠)、mutex_lock()(可能睡眠)、wait_event() 等。应使用 kmalloc(GFP_ATOMIC)、spin_lock() 等原子操作。

误区:tasklet 可以并行执行。同一个 tasklet 实例保证不会在多个 CPU 上同时执行(通过原子标志),但不同的 tasklet 可以并行。如果需要跨 CPU 的并行性,应使用软中断或工作队列。

注意:中断共享时每个 ISR 都必须正确判断是否自己的设备触发了中断。如果 ISR 错误地处理了不属于自己的中断,会导致中断无法被正确确认,设备可能停止工作。

注意:中断风暴可能导致系统不可用。如果设备持续触发中断而 ISR 无法处理(如设备故障),CPU 会被中断处理占满,无法执行其他任务。内核通过检测异常高的中断频率来禁用问题 IRQ。


六、本章小结

本章详细讲解了 Linux 内核的中断处理机制。中断是硬件设备异步通知 CPU 的核心方式,分为硬件中断、软件中断和异常。为了减少中断禁止时间,Linux 将中断处理分为上半部(快速确认和缓冲)和下半部(延后处理)。下半部机制包括软中断(高性能、静态定义、可并行)、tasklet(动态注册、同一实例串行)和工作队列(进程上下文、可睡眠)。中断注册通过 request_irq() 完成,支持中断共享。中断亲和性优化了多核系统的中断分配。中断线程化进一步提高了中断处理的灵活性和实时性。理解中断处理机制对于编写设备驱动和优化系统性能至关重要。


七、思考题与练习

画出中断处理的完整流程:从中断到达 CPU 到 ISR 执行完毕的每一步。

比较软中断、tasklet 和工作队列的特点,为以下场景选择最合适的下半部机制:(a)网络数据包处理;(b)磁盘 I/O 完成通知;(c)需要读取磁盘数据的后处理。

分析为什么中断上下文不能睡眠。如果必须在中断处理中等待某个事件,应该怎么做?

编写一个脚本,监控 /proc/interrupts 的变化,统计每秒各类中断的数量。

研究 IRQ 亲和性的配置方法,在多核系统上实验不同的中断分配策略对网络性能的影响。

分析中断线程化的优缺点。为什么不是所有中断都被线程化?

研究 NMI(不可屏蔽中断)的用途和处理方式。什么情况下会触发 NMI?

解释"中断合并"(Interrupt Coalescing)的概念,以及网卡驱动如何利用它减少中断数量。

6/10中断和中
0%