SMP 内核的中断与并发问题
本文档详细阐述了在对称多处理(SMP)系统中中断的来源,以及由此引入的、比单核系统更为复杂的并发问题和解决方案。
1. SMP 系统中的中断源
在 SMP 系统中,每个 CPU 核心都是一个独立的执行单元。中断不再仅仅是外部设备与单个 CPU 之间的事情,而是可以来自多个源,并作用于多个核心。
1.1. 本地中断 (Local Interrupts)
这类中断与特定的 CPU 核心绑定,只会被该核心响应。
- 时钟中断 (Timer Interrupts): 每个核心都有自己独立的本地定时器。这对于实现核本地的抢占式调度至关重要。当核心A的时钟中断触发时,它只会中断核心A,而不会影响核心B。
- 软件中断 (Software Interrupts): 一个核心可以给自己发送软件中断,用于处理延迟的或低优先级的任务。
1.2. 处理器间中断 (Inter-Processor Interrupts, IPIs)
IPI 是 SMP 系统独有的核心机制,允许一个 CPU 核心向另一个或所有其他核心发送中断。这是实现多核协作的基础。
- TLB 刷落 (TLB Shootdown): 当核心A修改了一个共享的页表项(例如,取消一个页的映射)后,其他核心(如核心B)的 TLB (Translation Lookaside Buffer) 中可能还缓存着旧的、无效的映射。核心A必须向核心B发送一个 IPI,通知它刷新其 TLB 中对应的条目,以保证内存视图的一致性。
- 调度协作: 当一个高优先级的任务在核心A上被唤醒,但核心A正在运行一个不可抢占的内核任务时,调度器可以向一个正在运行低优先级任务的空闲核心B发送 IPI,请求它立即重新调度,以便高优先级任务能够尽快运行。
- 系统停机/Panic: 当一个核心检测到无法恢复的致命错误时,它可以向所有其他核心广播一个 IPI,命令它们停止所有活动并进入停机状态,以防止进一步的数据损坏。
1.3. 全局/共享中断 (Global/Shared Interrupts)
这是来自外部物理设备(如网卡、磁盘、键盘)的中断。在 SMP 系统中,这些中断通过一个高级中断控制器(如 RISC-V 中的 PLIC)被路由到某一个当前可用的 CPU 核心。
这意味着,同个设备(如网卡)的两次中断,第一次可能由核心A处理,而第二次可能由核心B处理。
2. SMP 中的并发来源与挑战
在单核(UP)系统中,并发主要来源于任务代码与中断处理程序之间的竞争。通过禁用中断(IntrGuard),我们就可以阻止这种并发。
但在 SMP 系统中,真正的并行(Parallelism) 带来了全新的、更复杂的并发来源。禁用本地核心的中断,完全无法阻止其他核心的并行执行。
来源一:任务 vs. 任务
- 描述: 两个或多个任务在不同的CPU核心上同时执行。
- 问题: 如果这些任务访问任何共享的全局数据(如全局计数器、共享缓冲区),就会发生数据竞争。
- 解决方案: 必须使用
SpinLock或其他原子同步原语来保护共享数据。
来源二:任务 vs. 中断
- 描述: 一个任务在核心A上执行,而一个中断处理程序(可以是外部设备中断或IPI)在核心B上并行执行。
- 问题: 这是最常见的并发场景之一。即使核心A上的任务禁用了本地中断,也无法阻止核心B上的中断处理程序访问共享数据。
- 示例:
- 核心A 上的任务正在访问一个全局共享数据
G。 - 同时,一个外部设备中断被路由到 核心B,其中断处理程序也需要访问
G。 - 核心A即使禁用了自己的本地中断,也无法阻止核心B并行地执行中断处理程序。它们会同时访问
G,导致数据竞争。
- 核心A 上的任务正在访问一个全局共享数据
- 解决方案: 必须使用自旋锁 (
SpinLock) 来保护G。SpinLock利用跨核心同步的原子指令,确保无论代码运行在哪个核心上,只有一个执行流能进入临界区。
来源三:中断 vs. 中断
- 描述: 两个中断处理程序在不同的核心上并行执行。这可能是两个不同的外部设备中断,或一个外部中断和一个IPI。
- 问题: 如果这两个中断处理程序访问了共同的内核数据结构(例如,设备驱动程序中的共享状态),就会发生竞争。
- 示例:
- 一个网卡中断被路由到 核心A,其处理程序开始访问网卡驱动的共享数据结构
N。 - 几乎同时,另一个磁盘中断被路由到 核心B,其处理程序也需要访问
N(例如,一个通用的设备管理结构)。 - 两个中断处理程序在两个不同的核心上并行执行,产生了竞争。
- 一个网卡中断被路由到 核心A,其处理程序开始访问网卡驱动的共享数据结构
- 解决方案: 同样,必须使用
SpinLock来保护共享数据N。
来源四:IPI 引入的复杂同步
- 描述: 处理器间中断(IPI)本身就是一种并发事件,它要求发送方和接收方之间有精确的同步协议,以避免状态不一致。
- 问题: 在核心A发送 IPI 和核心B处理 IPI 之间,核心B可能正在使用即将失效的旧状态。
- 示例 (TLB Shootdown 协议):
- 核心A 获取一个用于保护该页表的
SpinLock。 - 核心A 修改页表项。
- 核心A 向核心B发送 IPI。
- 核心A 自旋等待,直到核心B确认 IPI 已处理完毕。
- 核心B 收到 IPI,执行
sfence.vma指令刷新其 TLB。 - 核心B 通过原子变量等方式通知核心A,它已完成刷新。
- 核心A 收到确认后,才释放页表锁,并继续执行。
- 核心A 获取一个用于保护该页表的
3. SMP 环境下的锁使用规则
-
IntrGuard不足以保证多核安全IntrGuard或直接屏蔽中断,只能阻止本地核心的并发(即任务与本地中断的竞争)。它对于来自其他核心的并行访问是完全无效的。- 规则: 任何可能在多核环境下被并行访问的数据,必须使用
SpinLock或其他更高级的同步原语来保护。
-
中断处理程序中的锁使用限制
- 中断处理程序(包括 IPI 处理程序)的执行上下文是特殊的,它没有关联的任务,不能被调度。
- 规则: 中断处理程序绝对不能获取任何可能导致睡眠的锁(如
SleepLock),也不能执行任何可能触发内存分配或任务调度的操作。否则,整个系统将死锁或崩溃。 - 结论: 中断处理程序中唯一可以安全使用的锁就是
SpinLock。
-
保持中断处理程序简短快速
- 当一个核心在中断处理程序中持有一个
SpinLock时,其他核心如果也想获取这个锁,就会一直自旋等待。如果中断处理程序执行时间过长,会严重影响整个系统的性能和响应能力。 - 规则: 中断处理程序应尽可能快地完成其工作。对于耗时较长的任务,应采用“上半部/下半部”模型:在中断处理程序(上半部)中只完成紧急的操作(如从硬件读取数据、应答中断),然后将耗时的工作注册为一个延迟任务(下半部),交由正常的任务上下文去执行。
- 当一个核心在中断处理程序中持有一个