sched discussion
[PATCH v5 00/18] Refcounted interrupt disable and SpinLockIrq for Rust
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.h、hardirq.h、spinlock*.h、interrupt_rc.h、refcount_interrupt_test.c、arch/{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_MASK与PREEMPT_NEED_RESCHED挤在同一 32 bit,64-bit 架构想要单原子读这两段也受限。- OpenRISC 的
asm/smp.h没 include<linux/cpumask.h>,新公共头一引入就在 OpenRISC 上 build break。 - Rust 侧缺乏"声明 IRQ 已禁用上下文"的 token,也没有 SpinLockIrq 抽象。
新流程
nmi_enter/exit改用 per-CPUnmi_nesting计数器,NMI_BITS缩到 1。preempt_count重新布局:HARDIRQ_DISABLE 8 bit(位 16-23)、HARDIRQ 4 bit(24-27)、NMI 1 bit(位 28),高位 NEED_RESCHED。- 新增
local_interrupt_disable/enable:把 HARDIRQ_DISABLE 计数 +1,仅在 0->1 时真正local_irq_save并把 flags 存 per-CPU 变量;仅在回到 0 时恢复。 - 新增
spin_lock_irq_disable / spin_unlock_irq_enable / *_trylock_irq_disable,内部调用上述计数。 - 64-bit 架构(x86_64、arm64、s390)启用
HAS_SEPARATE_PREEMPT_RESCHED_BITS,把 NEED_RESCHED 挪到独立 32-bit 字,此时 NMI 可用 4 bit;32-bit 架构继续走原路径。 - Rust 新增
kernel::interrupt模块(LocalInterruptDisabledtoken +local_interrupt_disable())与SpinLockIrq<T>/SpinLockIrqGuard;lock_with()在已知中断已禁用时跳过计数器操作。
Patch 概览
| # | 主题 | 关键作用 |
|---|---|---|
| 01 | preempt: Track NMI nesting to separate per-CPU counter | 把 NMI 嵌套从 preempt_count 搬到 per-CPU |
| 02 | preempt: Introduce HARDIRQ_DISABLE_BITS | 重新布局 preempt_count,加 8-bit HARDIRQ_DISABLE |
| 03 | preempt: Introduce __preempt_count_{sub,add}_return() | 让修改后能直接拿到新值 |
| 04 | openrisc: Include <linux/cpumask.h> in smp.h | 修新头引发的 OpenRISC build break |
| 05 | irq & spin_lock: Add counted interrupt disabling/enabling | 核心 API:local_interrupt_*、spin_*lock_irq_disable |
| 06 | irq: Add KUnit test for refcounted interrupt enable/disable | 嵌套、混合 save/restore 等基础测试 |
| 07 | irq: Add max nesting level kunit test case | 边界测试 |
| 08 | locking: Switch to _irq_{disable,enable}() in cleanup guards | sched/fair 与 lock guard 全部切换 |
| 09 | sched: Remove unused preempt_offset of __cant_sleep() | 清理 |
| 10 | sched: Avoid signed comparison of preempt_count() in __cant_migrate() | 为 64-bit 全位使用做准备 |
| 11 | preempt: Introduce HAS_SEPARATE_PREEMPT_RESCHED_BITS | x86/s390/arm64 的基础开关 |
| 12 | arm64: sched/preempt: Enable HAS_SEPARATE_PREEMPT_RESCHED_BITS | arm64 启用 |
| 13 | s390/preempt: Enable HAS_SEPARATE_PREEMPT_RESCHED_BITS | s390 改用 64-bit preempt_count |
| 14 | rust: Introduce interrupt module | Rust 的 LocalInterruptDisabled 抽象 |
| 15 | rust: helper: Add spin_{un,}lock_irq_{enable,disable}() helpers | Rust 侧的 C helper |
| 16 | rust: sync: Use super::* in spinlock.rs | 清理 |
| 17 | rust: sync: Add SpinLockIrq | 主要 Rust 抽象 |
| 18 | rust: 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.h的NMI_BITS必须同步更新;v5 漏掉了,由[PATCH 2/4]增量修复。 - refcount overflow / underflow 检查只在
CONFIG_DEBUG_PREEMPT=y生效,生产内核没有兜底——错误使用只能等自检发现。 - OpenRISC 头补丁(Patch 04)暴露问题:新增公共头可能牵连其它架构
asm/smp.h缺cpumask.h的隐式依赖,扩展头时需审慎。 - 64-bit 启用
HAS_SEPARATE_PREEMPT_RESCHED_BITS后,preempt_count取负值成为可能,__cant_migrate必须改成非负比较(Patch 10)。 LocalInterruptDisabled::assume_disabled()是 unsafe,调用方必须证明中断真的关着;类型系统挡一部分,lockdep 是最终兜底。lock_with()用transmute把SpinLockIrq当SpinLock用——前提是二者内存布局相同,这条 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.rs报blk_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 要求拆成独立补丁):irq: Add {over,under}flow detection for local_interrupt_{enable,disable}selftests/bpf: Use the new NMI_BITS definitionirq: Explain better on NMI_MASK overflow conditionirq: 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 上下文的关键一步。