0/39 已展开

LLM 分析

Linux Locking / Scheduler:Rust 引用计数中断禁用与 SpinLockIrq

系列概况

  • 标题: [PATCH v5 00/18] Refcounted interrupt disable and SpinLockIrq for Rust
  • 作者: Boqun Feng <boqun@kernel.org>;合著:Lyude Paul、Joel Fernandes、Heiko Carstens
  • 版本: v5 主系列 18 贴 + [PATCH 0/4] 增量 4 贴;提交流向 tip: locking/core
  • 规模: 主系列 35 文件 +1092/-90;增量 4 文件 +37/-2
  • 修改文件: preempt.hhardirq.hspinlock*.hinterrupt_rc.hrefcount_interrupt_test.carch/{arm64,s390,x86,openrisc}/*rust/{kernel,helpers}/*
  • Message-ID: 20260807070218.27144-1-boqun@kernel.org
  • 完整性: 完整——cover letter + 18 patch + 9 封 maintainer/reviewer 回复(Peter Zijlstra、Boqun Feng、Lyude Paul、Shrikanth Hegde、Gary Guo)+ 增量 4 patch + 5 封 tip-bot2 提交通知

补丁目的

让 Rust 内核代码拥有一套可嵌套(引用计数)的本地中断禁用原语,以及与之配对的 SpinLockIrq 自旋锁抽象,对齐 C 端 spin_lock_irq() 族的语义。同时重排 preempt_count() 位图:把 NMI 嵌套迁出,给 HARDIRQ_DISABLE 计数器腾位,并在 64-bit 架构把 PREEMPT_NEED_RESCHED 拆到独立字。

旧流程的问题

  • 现有 spin_lock_irq()local_irq_save/restore,本质仍是单层保存;嵌套时内层会"破坏性恢复",Rust 无法基于此写出类型安全的 spinlock+中断抽象。
  • preempt_count 中 NMI 占 4 bit(用作嵌套计数),没有空间放 8 bit 的 HARDIRQ_DISABLE 计数器。
  • NMI_MASKPREEMPT_NEED_RESCHED 挤在同一 32 bit,64-bit 架构想要单原子读这两段也受限。
  • OpenRISC 的 asm/smp.h 没 include <linux/cpumask.h>,新公共头一引入就在 OpenRISC 上 build break。
  • Rust 侧缺乏"声明 IRQ 已禁用上下文"的 token,也没有 SpinLockIrq 抽象。

新流程

  1. nmi_enter/exit 改用 per-CPU nmi_nesting 计数器,NMI_BITS 缩到 1。
  2. preempt_count 重新布局:HARDIRQ_DISABLE 8 bit(位 16-23)、HARDIRQ 4 bit(24-27)、NMI 1 bit(位 28),高位 NEED_RESCHED。
  3. 新增 local_interrupt_disable/enable:把 HARDIRQ_DISABLE 计数 +1,仅在 0->1 时真正 local_irq_save 并把 flags 存 per-CPU 变量;仅在回到 0 时恢复。
  4. 新增 spin_lock_irq_disable / spin_unlock_irq_enable / *_trylock_irq_disable,内部调用上述计数。
  5. 64-bit 架构(x86_64、arm64、s390)启用 HAS_SEPARATE_PREEMPT_RESCHED_BITS,把 NEED_RESCHED 挪到独立 32-bit 字,此时 NMI 可用 4 bit;32-bit 架构继续走原路径。
  6. Rust 新增 kernel::interrupt 模块(LocalInterruptDisabled token + local_interrupt_disable())与 SpinLockIrq<T> / SpinLockIrqGuardlock_with() 在已知中断已禁用时跳过计数器操作。

Patch 概览

#主题关键作用
01preempt: Track NMI nesting to separate per-CPU counter把 NMI 嵌套从 preempt_count 搬到 per-CPU
02preempt: Introduce HARDIRQ_DISABLE_BITS重新布局 preempt_count,加 8-bit HARDIRQ_DISABLE
03preempt: Introduce __preempt_count_{sub,add}_return()让修改后能直接拿到新值
04openrisc: Include <linux/cpumask.h> in smp.h修新头引发的 OpenRISC build break
05irq & spin_lock: Add counted interrupt disabling/enabling核心 API:local_interrupt_*spin_*lock_irq_disable
06irq: Add KUnit test for refcounted interrupt enable/disable嵌套、混合 save/restore 等基础测试
07irq: Add max nesting level kunit test case边界测试
08locking: Switch to _irq_{disable,enable}() in cleanup guardssched/fair 与 lock guard 全部切换
09sched: Remove unused preempt_offset of __cant_sleep()清理
10sched: Avoid signed comparison of preempt_count() in __cant_migrate()为 64-bit 全位使用做准备
11preempt: Introduce HAS_SEPARATE_PREEMPT_RESCHED_BITSx86/s390/arm64 的基础开关
12arm64: sched/preempt: Enable HAS_SEPARATE_PREEMPT_RESCHED_BITSarm64 启用
13s390/preempt: Enable HAS_SEPARATE_PREEMPT_RESCHED_BITSs390 改用 64-bit preempt_count
14rust: Introduce interrupt moduleRust 的 LocalInterruptDisabled 抽象
15rust: helper: Add spin_{un,}lock_irq_{enable,disable}() helpersRust 侧的 C helper
16rust: sync: Use super::* in spinlock.rs清理
17rust: sync: Add SpinLockIrq主要 Rust 抽象
18rust: sync: Introduce SpinLockIrq::lock_with() and friends优化路径

关键实现

/* include/linux/interrupt_rc.h (core idea) */
static inline void local_interrupt_disable(void)
{
    int new_count;

    WARN_ON_ONCE(in_nmi());
    new_count = hardirq_disable_enter();   /* preempt_count_add_return */

    if (IS_ENABLED(CONFIG_DEBUG_PREEMPT) &&
        DEBUG_LOCKS_WARN_ON(((new_count & HARDIRQ_DISABLE_MASK) +
                             (10 << HARDIRQ_DISABLE_SHIFT))
                            >= HARDIRQ_DISABLE_MASK))
        return;

    /* Interrupts can happen here, but it is OK, see __irq_exit_rcu(). */

    if ((new_count & HARDIRQ_DISABLE_MASK) == HARDIRQ_DISABLE_OFFSET)
        _local_interrupt_disable();        /* local_irq_save + save per-CPU */
}

static inline void local_interrupt_enable(void)
{
    int new_count;

    if (IS_ENABLED(CONFIG_DEBUG_PREEMPT) &&
        DEBUG_LOCKS_WARN_ON((preempt_count() & HARDIRQ_DISABLE_MASK) == 0))
        return;

    new_count = hardirq_disable_exit();

    if ((new_count & HARDIRQ_DISABLE_MASK) == 0)
        _local_interrupt_enable();         /* local_irq_restore(per-CPU) */
}
          preempt_count bit layout (HAS_SEPARATE_PREEMPT_RESCHED_BITS=n)
          +--------------------------------------------------------------+
          | 31..28  | 27..24  | 23..16        | 15..8    | 7..0          |
          | NMI     | HARDIRQ | HARDIRQ       | SOFTIRQ  | PREEMPT       |
          | (1b)    | (4b)    | _DISABLE (8b) | (8b)     | (8b)          |
          +--------------------------------------------------------------+
              ^        ^          ^
              |        |          +-- refcount for local_interrupt_disable
              |        +------------- hardirq count
              +---------------------- NMI flag (nesting tracked in per-CPU var)

          For HAS_SEPARATE_PREEMPT_RESCHED_BITS=y (x86_64 / s390 / arm64):
            - PREEMPT_NEED_RESCHED lives in a separate 32-bit word
            - NMI_MASK becomes 0xf0000000 (4 bits in word0)
            - Saves one per-CPU variable and a few NMI-path instructions

          Lock path:
            spin_lock_irq_disable(lock)
              -> local_interrupt_disable()    // +1 refcount, real disable on 0->1
              -> preempt_disable()
              -> spin_acquire(&lock->dep_map, ...)
              -> _raw_spin_lock(lock)

 spin_unlock_irq_enable(lock)
              -> spin_release(...)
              -> do_raw_spin_unlock(lock)
              -> preempt_enable()
              -> local_interrupt_enable()    // -1 refcount, real enable on ->0

          Rust-side lock_with (no-overhead path):
            SpinLockIrq::lock_with(token)
              -> transmute<&SpinLock> -> SpinLock::lock
            // invariant: SpinLockIrq and SpinLock share identical State layout
/* arch/x86/include/asm/preempt.h (key snippet for HAS_SEPARATE_PREEMPT_RESCHED_BITS) */
#ifdef CONFIG_HAS_SEPARATE_PREEMPT_RESCHED_BITS
#define __pc_dec "decq"
#define __pc_op(op, ...) raw_cpu_##op##_8(__VA_ARGS__)
#else
#define __pc_dec "decl"
#define __pc_op(op, ...) raw_cpu_##op##_4(__VA_ARGS__)
#endif

static __always_inline void preempt_count_set(unsigned long pc)
{
    unsigned long old, new;

    old = __pc_op(read, __preempt_count);
    do {
        new = (old & PREEMPT_NEED_RESCHED) | (pc & ~PREEMPT_NEED_RESCHED);
    } while (!__pc_op(try_cmpxchg, __preempt_count, &old, new));
}

类比

  • 整套 refcounted interrupt disable 像酒店门卡系统:每进一间房拿一张卡(计数 +1),退房还一张(计数 -1)。只有第一张卡插入时才真的锁门(关硬件中断),最后一张卡拔掉时才真的开门(恢复中断);中间过程屋里的人不必反复开关门。
  • SpinLockIrq 像"既要锁房门又要锁房间"的双锁;lock_with() 是"我确定门已锁,请直接给我房间钥匙",省下房卡计数那一步的开销。
  • per-CPU nmi_nesting 像医院的急诊登记表:独立小本本记几层嵌套,不再挤在大病历本(preempt_count)里。

Highlight:风险与注意点

  • NMI bits 重新分配后,tools/testing/selftests/bpf/bpf_experimental.hNMI_BITS 必须同步更新;v5 漏掉了,由 [PATCH 2/4] 增量修复。
  • refcount overflow / underflow 检查只在 CONFIG_DEBUG_PREEMPT=y 生效,生产内核没有兜底——错误使用只能等自检发现。
  • OpenRISC 头补丁(Patch 04)暴露问题:新增公共头可能牵连其它架构 asm/smp.hcpumask.h 的隐式依赖,扩展头时需审慎。
  • 64-bit 启用 HAS_SEPARATE_PREEMPT_RESCHED_BITS 后,preempt_count 取负值成为可能,__cant_migrate 必须改成非负比较(Patch 10)。
  • LocalInterruptDisabled::assume_disabled() 是 unsafe,调用方必须证明中断真的关着;类型系统挡一部分,lockdep 是最终兜底。
  • lock_with()transmuteSpinLockIrqSpinLock 用——前提是二者内存布局相同,这条 invariant 隐式依赖 Backend::State 一致。
  • 性能数据来自 QEMU / MacBook M4 跑分,对真实内核工作负载的影响尚未验证。
  • Rust 侧 helpers 中的 irqs_disabled() 当前未被使用(Peter 质疑,Boqun 确认是 dead code,从增量移除)。
  • Peter 在本机 tip/locking/core + tip/master 上构建 Rust 失败:rust/kernel/block/mq/tag_set.rsblk_mq_tag_set 没有 ops / nr_hw_queues 字段;Gary Guo 回复是 Rust 与 C 侧 bindgen 不同步,需要先 rebase/sync bindings。

版本变化

  • v4 -> v5:按 Peter 反馈修 SoB;按 Shrikanth 建议加 refcount 上下溢出检测;加最大嵌套深度 KUnit 测试;若干文档/注释微调。
  • v5 -> [PATCH 0/4] 增量(应 Peter 要求拆成独立补丁):
    1. irq: Add {over,under}flow detection for local_interrupt_{enable,disable}
    2. selftests/bpf: Use the new NMI_BITS definition
    3. irq: Explain better on NMI_MASK overflow condition
    4. irq: Add max local_interrupt_disable() nesting level kunit test case
  • 提交流向:tip-bot2 显示 Rust 5 个 patch(interrupt module、helper、spinlock super::*、SpinLockIrq、SpinLockIrq::lock_with)已合并到 tip: locking/core;其余由 maintainer 分批拉取。

一句话总结

该系列把 NMI 嵌套迁出 preempt_count、引入 8-bit HARDIRQ_DISABLE 计数器,让 Rust 端拥有与 C 端 spin_lock_irq() 同等语义、可嵌套、可 lockdep 验证的 SpinLockIrq 抽象,是 Rust 内核走向 IRQ-disabled 上下文的关键一步。