0/86 已展开

LLM 分析

Linux locking/scheduler:引用计数中断禁用与 Rust SpinLockIrq v4

系列概况

  • 标题:[PATCH v4 00/17] Refcounted interrupt disable and SpinLockIrq for Rust
  • 作者:Boqun Feng;参与者包括 Lyude Paul、Joel Fernandes、Heiko Carstens。
  • 版本:v4,17 个 patch;另有 patch 05/17 的 v4.1 修订。
  • 规模:初始 cover letter 报告 36 个文件,新增 1060 行、删除 91 行。
  • 修改文件:涉及 include/linux/{preempt.h,hardirq.h,interrupt_rc.h,spinlock*.h}kernel/{irq,locking,sched}、x86/arm64/s390/OpenRISC 架构代码,以及 Rust helper、interrupt 和 spinlock 模块。
  • 代码统计:新增 interrupt_rc.h、中断引用计数 KUnit 测试、Rust interrupt.rs;Rust spinlock.rs 增加 SpinLockIrq 及其快路径。
  • Message-ID:首封为 20260804161447.84806-1-boqun@kernel.org
  • 完整性:输入包含 17 个系列 patch、v4.1 修订、tip-bot 合入通知、patch 07 的后续 revert,以及随后关于 Rust IRQ API 的延伸讨论;部分后续回复正文为空,因此只能忠实标注为空回复。

补丁目的

本系列的核心目标是把嵌套的本地中断禁用从 local_irq_save()/local_irq_restore() 的“保存并恢复现场”模式,改成可计数的 disable/enable 模式。

这样,多个锁或 guard 嵌套时,只有最外层调用真正关闭硬件中断,只有最后一次 enable 才恢复原始状态,避免内层释放操作意外重新打开中断。

在此基础上,系列为 Rust 增加 LocalInterruptDisabled 标记类型和 SpinLockIrq,让 Rust 可以用生命周期表达“该锁的整个保护区内本地中断必须关闭”。

旧流程的问题

irqsave/irqrestore 的每一层调用都保存自己的 flags。若锁的释放顺序与获取顺序不匹配,内层 irqrestore 可能恢复“中断开启”的旧状态,即使另一个 IRQ-safe 锁仍然持有。

典型情况是:

spin_lock_irqsave(l1, flags1);
spin_lock_irqsave(l2, flags2);
spin_unlock_irqrestore(l1, flags1);
/* l2 仍持有,但本地中断可能已开启 */

cleanup guard 和细粒度锁的 hand-to-hand 传递会让这种问题更容易发生,也使 Rust 难以提供安全的 IRQ-safe 锁抽象。

新流程

  • preempt_count() 增加 8-bit HARDIRQ_DISABLE 字段,记录中断禁用嵌套深度。
  • 新增 __preempt_count_add_return()__preempt_count_sub_return(),修改计数后直接返回新值。
  • 第一次 local_interrupt_disable() 保存原始 flags 并真正关闭中断;后续嵌套调用只增加计数。
  • 只有计数降为零时,local_interrupt_enable() 才恢复原始 flags。
  • 新增 spin_lock_irq_disable()spin_unlock_irq_enable() 和 trylock 变体。
  • 32-bit 路径把 NMI 嵌套深度移到 per-CPU nmi_nesting;支持 64-bit 计数的架构则可启用 HAS_SEPARATE_PREEMPT_RESCHED_BITS
  • Rust 用 RAII guard 管理中断禁用,并用 LocalInterruptDisabled 证明已处于 no-IRQ 上下文。
disable depth     hardware IRQ     operation
------------      ------------     ---------
0                 enabled          initial state
1                 disabled         first disable saves flags
2                 disabled         nested disable only increments
1                 disabled         inner enable only decrements
0                 restored         outer enable restores flags

Patch 概览

Patch作用
01把 NMI 嵌套计数移到 per-CPU 变量,释放 preempt_count
02增加 HARDIRQ_DISABLE_BITS,为中断禁用计数分配 8 位
03增加返回新计数值的 preempt 原语
04修复 OpenRISC smp.hcpumask.h 的隐式依赖
05实现引用计数中断 API 和 IRQ-safe spinlock 变体
06增加中断引用计数 KUnit 测试
07将 cleanup guard 改用 _irq_disable/_irq_enable
08、09清理 scheduler 检查逻辑,避免无用参数和有符号比较
10–12建立并在 x86、arm64、s390 上启用独立 resched 位方案
13–15增加 Rust interrupt 模块、C helpers 和导入整理
16增加 Rust SpinLockIrq
17增加 lock_with()/try_lock_with() 快路径

关键实现

1. 重新划分 preempt_count

普通 32-bit 路径大致变为:

#define HARDIRQ_DISABLE_BITS 8
#define HARDIRQ_DISABLE_SHIFT (SOFTIRQ_SHIFT + SOFTIRQ_BITS)
#define HARDIRQ_DISABLE_OFFSET (1UL << HARDIRQ_DISABLE_SHIFT)

#define hardirq_disable_enter() \
        __preempt_count_add_return(HARDIRQ_DISABLE_OFFSET)
#define hardirq_disable_exit() \
        __preempt_count_sub_return(HARDIRQ_DISABLE_OFFSET)

NMI 嵌套不再占用多个 preempt_count 位,而由 nmi_nesting 保存;preempt_count 中保留 NMI 状态标志。

2. 引用计数中断 API

static inline void local_interrupt_disable(void)
{
    int new_count = hardirq_disable_enter();

    if ((new_count & HARDIRQ_DISABLE_MASK) == HARDIRQ_DISABLE_OFFSET)
        _local_interrupt_disable();
}

static inline void local_interrupt_enable(void)
{
    int new_count = hardirq_disable_exit();

    if ((new_count & HARDIRQ_DISABLE_MASK) == 0)
        _local_interrupt_enable();
}

_local_interrupt_disable() 只在最外层保存 per-CPU flags 并调用 local_irq_save();对应的 enable 只在最外层恢复。

3. IRQ-safe spinlock

SMP、UP 和 PREEMPT_RT 路径都增加新的 lock/unlock 变体。SMP 路径的逻辑是:

  1. local_interrupt_disable() 增加 IRQ 禁用深度;
  2. preempt_disable()
  3. 获取 raw spinlock;
  4. 解锁时先释放 spinlock;
  5. local_interrupt_enable() 减少 IRQ 禁用深度;
  6. preempt_enable()

v4.1 删除了 trylock 失败/成功路径中多余的 spin_acquire(),避免 lockdep 状态重复记录。

4. 独立 PREEMPT_NEED_RESCHED

启用 HAS_SEPARATE_PREEMPT_RESCHED_BITS 时,64-bit 架构将计数扩展到 64-bit,并把 reschedule 状态放在独立字中。这样可以让 preempt_count 使用更多有效位,同时避免更新 reschedule 标志时破坏计数。

arm64 原本已经采用类似布局,因此只需启用 Kconfig。s390 则修改 lowcore 布局和汇编操作,从 32-bit preempt count 转为 64-bit。

5. Rust 类型封装

Rust 新增:

  • LocalInterruptDisabled:零大小、不可在线程间转移的中断关闭凭证;
  • SpinLockIrq<T>:使用 spin_lock_irq_disable() 的锁;
  • SpinLockIrqGuard:释放时解锁并减少 IRQ 禁用计数;
  • new_spinlock_irq!():初始化宏。
pub fn local_interrupt_disable() -> LocalInterruptDisabled {
    unsafe { bindings::local_interrupt_disable() };
    LocalInterruptDisabled(NotThreadSafe)
}

pub fn lock_with<'a>(
    &'a self,
    context: &'a LocalInterruptDisabled,
) -> SpinLockGuard<'a, T> {
    self.as_lock_in_interrupt(context).lock()
}

lock_with() 要求调用者提供与 guard 同生命周期的 LocalInterruptDisabled 引用,因此在锁仍然存活时不能重新打开本地中断。

类比

旧的 irqsave/irqrestore 像每个房间各自保管一把“电闸钥匙”:房间 A 和 B 嵌套进入后,A 先离开并恢复自己的钥匙状态,可能把整栋楼的电闸重新打开,即使 B 还在进行危险操作。

新 API 像大楼总控室维护一个进入深度:

  • 第一个人进入时拉下总闸并保存原状态;
  • 后续人员只登记人数;
  • 中间人员离开只减少人数;
  • 最后一个人离开时才恢复总闸。

LocalInterruptDisabled 则是总控室发出的凭证。持有凭证时,Rust 可以调用 lock_with(),直接使用普通 spinlock 后端,不必重复切换中断状态。

Highlight:风险与注意点

  1. patch 07 已被 revert:cleanup guard 的生命周期和 _irq_disable/_irq_enable 语义存在不兼容,Peter Zijlstra 后续提交了 revert,并进入 locking/urgent
  2. 计数溢出问题仍需关注HARDIRQ_DISABLE 只有 8 位,理论最大深度为 256;讨论中没有形成通用运行时溢出检查,因为检查本身可能获取 IRQ-safe 锁并产生递归问题。
  3. NMI 深度限制没有消失:32-bit 路径仍检查最大 15 层;64-bit 路径利用 preempt_count 中的额外位表达 NMI 状态。
  4. 架构布局必须同步preempt_countNMI_MASKPREEMPT_NEED_RESCHED 的定义在内核与 BPF selftest 头文件中必须保持一致。
  5. transmute 依赖布局等价SpinLockIrqSpinLock 当前都以 spinlock_t 为底层状态;若以后 Rust 锁结构布局变化,as_lock_in_interrupt() 的安全前提必须重新审查。
  6. PREEMPT_RT 语义不同:RT 路径直接使用 rt_spin_lock()/rt_spin_unlock(),不能简单套用非 RT 路径的硬件中断切换假设。
  7. 性能数据仍属微基准lock_with() 在部分 arm64 测试中更快,但 x86 测试有时仅持平;真实工作负载收益尚未确定。
  8. 后续方向发生变化:Thomas Gleixner 随后提出把 local_irq_enable/disable() 本身迁入 Rust,说明最终 API 形态仍可能继续调整。
  9. 署名和提交链曾有问题:Peter 指出 patch 02 的 From 与 Signed-off-by 顺序不正确,Boqun 表示会修正。

版本变化

  • v3 → v4:删除 local_interrupt_enable() 中的 preempt_count 检查;简化 per-CPU 状态结构;调整 CFS timer 调用方;完善 HAS_SEPARATE_PREEMPT_RESCHED_BITS 文档;更新 s390 汇编注释;修正署名、格式和链接。
  • v4 → v4.1:删除 _raw_spin_trylock_irq_disable() 中多余的 spin_acquire()
  • v4 → tip/locking/core:多个系列 patch 被 tip-bot 通知进入 locking/core
  • tip/locking/core → locking/urgent:patch 07 因 cleanup guard 回归被单独 revert。
  • 后续 Rust IRQ patch:Thomas Gleixner 在 2026-09-04 提交 irq: Move local_irq_enable/disable() into Rust,并继续与 Boqun 讨论设计方向。

一句话总结

该系列为 Linux 建立了以 preempt_count 为基础的引用计数中断禁用机制,并为 Rust 提供 SpinLockIrq 与类型安全的 no-IRQ 凭证;但 cleanup guard 迁移已被 revert,整体 Rust IRQ API 仍在继续演进。