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,导致数据竞争。
  • 解决方案: 必须使用自旋锁 (SpinLock) 来保护 GSpinLock 利用跨核心同步的原子指令,确保无论代码运行在哪个核心上,只有一个执行流能进入临界区。

来源三:中断 vs. 中断

  • 描述: 两个中断处理程序在不同的核心上并行执行。这可能是两个不同的外部设备中断,或一个外部中断和一个IPI。
  • 问题: 如果这两个中断处理程序访问了共同的内核数据结构(例如,设备驱动程序中的共享状态),就会发生竞争。
  • 示例:
    • 一个网卡中断被路由到 核心A,其处理程序开始访问网卡驱动的共享数据结构 N
    • 几乎同时,另一个磁盘中断被路由到 核心B,其处理程序也需要访问 N(例如,一个通用的设备管理结构)。
    • 两个中断处理程序在两个不同的核心上并行执行,产生了竞争。
  • 解决方案: 同样,必须使用 SpinLock 来保护共享数据 N

来源四:IPI 引入的复杂同步

  • 描述: 处理器间中断(IPI)本身就是一种并发事件,它要求发送方和接收方之间有精确的同步协议,以避免状态不一致。
  • 问题: 在核心A发送 IPI 和核心B处理 IPI 之间,核心B可能正在使用即将失效的旧状态。
  • 示例 (TLB Shootdown 协议):
    1. 核心A 获取一个用于保护该页表的 SpinLock
    2. 核心A 修改页表项。
    3. 核心A 向核心B发送 IPI。
    4. 核心A 自旋等待,直到核心B确认 IPI 已处理完毕。
    5. 核心B 收到 IPI,执行 sfence.vma 指令刷新其 TLB。
    6. 核心B 通过原子变量等方式通知核心A,它已完成刷新。
    7. 核心A 收到确认后,才释放页表锁,并继续执行。

3. SMP 环境下的锁使用规则

  1. IntrGuard 不足以保证多核安全

    • IntrGuard 或直接屏蔽中断,只能阻止本地核心的并发(即任务与本地中断的竞争)。它对于来自其他核心的并行访问是完全无效的。
    • 规则: 任何可能在多核环境下被并行访问的数据,必须使用 SpinLock 或其他更高级的同步原语来保护。
  2. 中断处理程序中的锁使用限制

    • 中断处理程序(包括 IPI 处理程序)的执行上下文是特殊的,它没有关联的任务,不能被调度。
    • 规则: 中断处理程序绝对不能获取任何可能导致睡眠的锁(如 SleepLock),也不能执行任何可能触发内存分配或任务调度的操作。否则,整个系统将死锁或崩溃。
    • 结论: 中断处理程序中唯一可以安全使用的锁就是 SpinLock
  3. 保持中断处理程序简短快速

    • 当一个核心在中断处理程序中持有一个 SpinLock 时,其他核心如果也想获取这个锁,就会一直自旋等待。如果中断处理程序执行时间过长,会严重影响整个系统的性能和响应能力。
    • 规则: 中断处理程序应尽可能快地完成其工作。对于耗时较长的任务,应采用“上半部/下半部”模型:在中断处理程序(上半部)中只完成紧急的操作(如从硬件读取数据、应答中断),然后将耗时的工作注册为一个延迟任务(下半部),交由正常的任务上下文去执行。