preempt/irq:NMI 嵌套计数迁出 + 中断重调度热路径优化

preempt 计数 · NMI/中断开关热路径 · Rust 计数式中断锁铺路

💡 一句话总结

在 NMI 与中断开关高频进出 preempt_count 的处理器热路径上,preempt_count 位域紧张(NMI 嵌套计数占 4 bit),每次 NMI 进出都要操作共享计数器、中断开关最外层解锁还要重读一次 preempt_count:本系列把 NMI 嵌套深度迁到独立的 per-CPU 计数器 nmi_nesting(NMI 位域从 4 bit 缩为 1 bit 标志),并让 local_interrupt_enable() 复用 hardirq_disable_exit() 的返回值、省掉一次 preempt_count 内存重读,从而为新的 Rust 计数式中断开关 SpinLockIrq 腾出位域并精简中断开关热路径。补丁未提供基准数据,收益为机制层面的逻辑分析(解读(AI 分析))。

📋 补丁基本信息

项目内容
补丁类型优化(性能重构:位域腾挪 + 热路径减读)为主,兼有“新特性”性质(为 Rust 计数式中断开关铺路)
状态In Review(作为 git pull request 提交给 tip 树;其中 irq 优化补丁已被作者撤回)
当前版本本 pull request 提交于 2026-07-31(rust-sync.20260731a)· cover letter 链接
版本演进 NMI 补丁:Lyude v17(2026-01-21) → 本 pull 01/24(2026-07-31)
irq 补丁:Boqun v3(2026-06-05) → 本 pull 13/24(2026-07-31) → 2026-08-03 撤回
作者机构Boqun Feng(kernel.org / rust-for-linux 维护者);NMI 补丁原作者 Joel Fernandes(NVIDIA)、Lyude Paul(Red Hat)
提交日期2026-07-31(系列 24 补丁,41 文件,+1184/-121)
改动范围NMI 补丁:include/linux/hardirq.h / preempt.h / kernel/softirq.c / BPF selftest,4 文件 +23/-7;irq 补丁:arm64/s390/x86/asm-generic preempt.h + interrupt_rc.h,5 文件 +32/-23
核心函数__nmi_enter() / __nmi_exit() / local_interrupt_enable() / hardirq_disable_exit() / __preempt_count_add_return()
原始链接cover letter Message-ID · NMI 补丁 · irq 补丁

📊 速览卡片

核心机制
NMI计数迁移
优化目标
中断路径减读
适用场景
NMI/IRQ使能
实测提升
未提供

🎯 解决什么问题

背景 / 原始动机
Rust 侧需要一个“关中断自旋锁” SpinLockIrq:C 语言里 spin_lock_irqsave() 需要调用方手动保存/恢复中断标志 flags,容易漏配对;Rust 的 RAII guard 希望在 drop() 时自动恢复中断状态。方案是把“中断禁用的嵌套层级”记进 preempt_count(即“计数式中断开关” local_interrupt_disable()/enable()),这样最外层退出临界区时自动恢复。

cover letter 原话:“The major changes are the introduction of preempt_count backed interrupt disabling level tracking, i.e. the local_interrupt_{enable, disable}(), along with the user (the Rust SpinLockIrq). This should unblock a few drivers in Rust.”(来源:cover letter)

但 preempt_count 只有 32 bit,已被 preempt(8)+softirq(8)+hardirq(4)+NMI(4)+NEED_RESCHED(1) 占掉 25 bit。要容纳新的硬中断禁用计数位,必须先压缩 NMI 位域——这就是 NMI 补丁的直接动因(推断为:位域腾挪,来自 cover letter 所述目的 + diff 中“free up the NMI bits…allowing those bits to be repurposed for other uses”)。
系统层面:preempt_count 位域挤占 + 中断开关热路径冗余读
缺陷 1(NMI 补丁解决):preempt_count 是一个 32 位、每线程(thread_info 或 lowcore)的整型,用位域同时承载“可抢占深度、软中断深度、硬中断深度、NMI 嵌套深度、是否需要重调度”。NMI 嵌套用 4 bit(NMI_MASK=0x00f00000,最多 15 层),而现实中 NMI 嵌套几乎不会超过 1–2 层,4 bit 明显冗余;但它把位域撑满,导致无法再放入新的计数字段(如 HARDIRQ_DISABLE)。NMI 进出本应是最“轻”的路径,却要和硬中断、软中断、抢占深度挤在同一变量上做加减。

缺陷 2(irq 补丁解决):计数式中断开关最外层解锁 local_interrupt_enable() 时,hardirq_disable_exit() 刚返回了递减后的 preempt_count 新值;但原代码仍调用 should_resched(0),后者内部重新 READ_ONCE(preempt_count) 从内存再读一次同一变量。commit message 里留有 TODO:“re-read preempt count can be avoided”(本次把它做掉)。
场景层面:irq-disabled 自旋锁的获取/释放高频路径
受影响的是“关中断 + 自旋锁”这一内核最常用的互斥原语组合(spin_lock_irqsave()/spin_unlock_irqrestore() 家族)。改造后新增的 spin_lock_irq_disable()/spin_unlock_irq_enable()(patch 05/24 引入)在每次 lock/unlock 都走 local_interrupt_disable()/enable(),即 __preempt_count_add_return()/__preempt_count_sub_return()——这条路径的频率与自旋锁一样高(驱动、内核同步到处都是)。

NMI 路径(__nmi_enter()/__nmi_exit())是处理器最高优先级异常入口/出口,任何 perf、watchdog、硬件错误处理都会触发;在 NMI 下内核原则上“不吃锁、不调度”,要求进出尽可能廉价。把 NMI 嵌套计数从共享的 preempt_count 位域搬到独立 per-CPU 计数器,既腾出位域,也让 NMI 嵌套与硬/软中断计数解耦。

Rust 侧场景:即将接入的若干驱动(cover letter 提到 “unblock a few drivers in Rust”,具体驱动未列出,属推断)会使用 SpinLockIrq,其 guard 的 drop 走 spin_unlock_irq_enable() → local_interrupt_enable()。
受影响负载:自旋锁高频临界区、NMI/watchdog/perf 路径、Rust SpinLockIrq 驱动 · 为什么此特性解决此场景:位域腾挪解锁新原语 + 中断开关热路径省一次内存读

🧩 核心机制

两个补丁解决两个不同的“热路径/位域”问题:NMI 补丁把嵌套计数迁到 per-CPU 变量、把 NMI 位域从 4 bit 压到 1 bit 标志;irq 补丁让中断开关最外层解锁直接复用已有的返回计数,省一次 preempt_count 内存读。前者是后者的前提——只有把 NMI 位域从 4 bit 压到 1 bit 标志,才能连同原 HARDIRQ 位域一起重新组织出 8 bit 的 HARDIRQ_DISABLE 计数字段(patch 02/24 引入)。

从系统层面看
NMI 补丁(01/24):新增 DEFINE_PER_CPU(unsigned int, nmi_nesting)(在 kernel/softirq.c)。__nmi_enter() 里 __this_cpu_inc(nmi_nesting)(上限 15),再 __preempt_count_add(HARDIRQ_OFFSET) 保持“硬中断计数”语义,并用 preempt_count_set(preempt_count() | NMI_MASK) 置上唯一的 NMI 标志位;__nmi_exit() 反向:先 __preempt_count_sub(HARDIRQ_OFFSET),__this_cpu_dec_return(nmi_nesting) 到 0 才用 preempt_count_set(preempt_count() & ~NMI_MASK) 清标志。为什么这么改:嵌套深度不再占用 preempt_count 的 4 bit,只留 1 bit“是否在 NMI 中”的布尔标志,in_nmi() 语义保持不变,腾出的位域(连同原 HARDIRQ 位域)在 patch 02/24 中被重新组织为 8 bit 的 HARDIRQ_DISABLE 计数字段。

irq 补丁(13/24):把 __preempt_count_{add,sub}_return() 的返回类型从 int 改为 unsigned long(arm64 上从读 u32 preempt.count 改为读 64 位 preempt_count),使其返回完整位域(含 bit31 的 PREEMPT_NEED_RESCHED)。新增 local_interrupt_enable_reched(pc):pc 非 0(仍在某些禁用上下文)则直接返回;为 0 且架构无 PREEMPT_NEED_RESCHED 位时先查 tif_need_resched(),否则直接 __preempt_schedule()。这样 local_interrupt_enable() 用 hardirq_disable_exit() 的返回值即可完成“是否需要重调度”的判断,不再重读 preempt_count。
preempt_count 位域改造前后对比
图 1:preempt_count 位域改造前后对比——NMI 嵌套深度从 4 bit 迁出到独立 per-CPU 计数器,位域重排后 HARDIRQ_DISABLE(8 bit)占 bits 16-23、HARDIRQ 上移至 bits 24-27、NMI 标志落在 bit 28
来源:基于 lore 真实补丁 diff(PATCH 01/24)绘制
local_interrupt_enable 重调度检查优化前后对比
图 2:local_interrupt_enable() 重调度检查优化——before 用 should_resched(0) 重读 preempt_count,after 复用 hardirq_disable_exit() 的返回值(unsigned long 全位域)
来源:基于 lore 真实补丁 diff(PATCH 13/24)绘制
步骤操作目的
NMI 进入__this_cpu_inc(nmi_nesting) + __preempt_count_add(HARDIRQ_OFFSET) + preempt_count_set(preempt_count() | NMI_MASK)嵌套深度记到 per-CPU 计数器;preempt_count 只留 1 bit NMI 标志 + 硬中断计数
NMI 退出__preempt_count_sub(HARDIRQ_OFFSET) + __this_cpu_dec_return(nmi_nesting),归 0 才清 NMI_MASK只有最外层 NMI 退出才清除“在 NMI 中”标志,保持 in_nmi() 语义
中断开关解锁new_count = hardirq_disable_exit()(返回 unsigned long 全位域)把 NEED_RESCHED 位一并返回,避免再读一次 preempt_count
重调度判断local_interrupt_enable_reched(new_count):pc 为 0 且需要重调度才 __preempt_schedule()最外层且确实需要重调度时才走慢路径

🔬 关键代码

以下 diff 为 lore 中补丁的逐字原文。核心逻辑点有两个:① NMI 嵌套计数迁出 + 位域压缩;② 中断开关解锁复用返回计数。

逻辑点 1:NMI 嵌套计数迁出(补丁 01/24)

diff --git a/include/linux/hardirq.h b/include/linux/hardirq.h
index d57cab4d4c06..8d4895531a45 100644
--- a/include/linux/hardirq.h
+++ b/include/linux/hardirq.h
@@ -10,6 +10,8 @@
 #include <linux/vtime.h>
 #include <asm/hardirq.h>
 
+DECLARE_PER_CPU(unsigned int, nmi_nesting);
+
 extern void synchronize_irq(unsigned int irq);
 extern bool synchronize_hardirq(unsigned int irq);
 
@@ -102,14 +104,17 @@ void irq_exit_rcu(void);
  */
 
 /*
- * nmi_enter() can nest up to 15 times; see NMI_BITS.
+ * nmi_enter() can nest - nesting is tracked in a per-CPU counter.
  */
 #define __nmi_enter()						\
 	do {							\
 		lockdep_off();					\
 		arch_nmi_enter();				\
-		BUG_ON(in_nmi() == NMI_MASK);			\
-		__preempt_count_add(NMI_OFFSET + HARDIRQ_OFFSET);	\
+		/* Maximum NMI nesting is 15. */		\
+		BUG_ON(__this_cpu_read(nmi_nesting) >= 15);	\
+		__this_cpu_inc(nmi_nesting);			\
+		__preempt_count_add(HARDIRQ_OFFSET);		\
+		preempt_count_set(preempt_count() | NMI_MASK);	\
 	} while (0)
 
 #define nmi_enter()						\
@@ -124,8 +129,12 @@ void irq_exit_rcu(void);
 
 #define __nmi_exit()						\
 	do {							\
+		unsigned int nesting;				\
 		BUG_ON(!in_nmi());				\
-		__preempt_count_sub(NMI_OFFSET + HARDIRQ_OFFSET);	\
+		__preempt_count_sub(HARDIRQ_OFFSET);		\
+		nesting = __this_cpu_dec_return(nmi_nesting);	\
+		if (!nesting)					\
+			preempt_count_set(preempt_count() & ~NMI_MASK);	\
 		arch_nmi_exit();				\
 		lockdep_on();					\
 	} while (0)
diff --git a/include/linux/preempt.h b/include/linux/preempt.h
index d964f965c8ff..586f96688325 100644
--- a/include/linux/preempt.h
+++ b/include/linux/preempt.h
@@ -17,6 +17,8 @@
  *
  * - bits 0-7 are the preemption count (max preemption depth: 256)
  * - bits 8-15 are the softirq count (max # of softirqs: 256)
+ * - bits 16-19 are the hardirq count (max # of hardirqs: 16)
+ * - bit 20 is the NMI flag (no nesting count, tracked separately)
  *
  * The hardirq count could in theory be the same as the number of
  * interrupts in the system, but we run all interrupt handlers with
@@ -24,16 +26,19 @@
  * there are a few palaeontologic drivers which reenable interrupts in
  * the handler, so we need more than one bit here.
  *
+ * NMI nesting depth is tracked in a separate per-CPU variable
+ * (nmi_nesting) to save bits in preempt_count.
+ *
  *         PREEMPT_MASK:	0x000000ff
  *         SOFTIRQ_MASK:	0x0000ff00
  *         HARDIRQ_MASK:	0x000f0000
- *             NMI_MASK:	0x00f00000
+ *             NMI_MASK:	0x00100000
  * PREEMPT_NEED_RESCHED:	0x80000000
  */
 #define PREEMPT_BITS	8
 #define SOFTIRQ_BITS	8
 #define HARDIRQ_BITS	4
-#define NMI_BITS	4
+#define NMI_BITS	1
 
 #define PREEMPT_SHIFT	0
 #define SOFTIRQ_SHIFT	(PREEMPT_SHIFT + PREEMPT_BITS)

▲ 这段在 __nmi_enter()/__nmi_exit() 里把 NMI 嵌套深度从 preempt_count 的 4 bit 位域搬到一个独立 per-CPU 计数器 nmi_nesting。关键点:__this_cpu_inc()/dec_return() 是单 CPU 无锁操作,NMI 在同一 CPU 上不会并发;preempt_count_set(preempt_count() | NMI_MASK) 只置 1 bit 标志,in_nmi() 语义不变。为什么这么改:把 4 bit 的“冗余计数”压成 1 bit 的“布尔标志”(bit 28),腾出的空间连同原 HARDIRQ 位域被重组为 8 bit 的 HARDIRQ_DISABLE 计数(本系列 patch 02/24 引入,HARDIRQ 上移至 bits 24-27,NMI 标志在 bit 28)。注意 boqun 在 commit message 中说明:用 preempt_count_set() 清标志是为避免 __preempt_count_sub(NMI_OFFSET) 在嵌套场景下计数下溢。

逻辑点 2:中断开关解锁复用返回计数(补丁 13/24)

diff --git a/arch/arm64/include/asm/preempt.h b/arch/arm64/include/asm/preempt.h
index 0dd8221d1bef..e9f597d87413 100644
--- a/arch/arm64/include/asm/preempt.h
+++ b/arch/arm64/include/asm/preempt.h
@@ -55,20 +55,20 @@ static inline void __preempt_count_sub(int val)
 	WRITE_ONCE(current_thread_info()->preempt.count, pc);
 }
 
-static inline int __preempt_count_add_return(int val)
+static inline unsigned long __preempt_count_add_return(int val)
 {
-	u32 pc = READ_ONCE(current_thread_info()->preempt.count);
+	u64 pc = READ_ONCE(current_thread_info()->preempt_count);
 	pc += val;
-	WRITE_ONCE(current_thread_info()->preempt.count, pc);
+	WRITE_ONCE(current_thread_info()->preempt_count, pc);
 
 	return pc;
 }
 
-static inline int __preempt_count_sub_return(int val)
+static inline unsigned long __preempt_count_sub_return(int val)
 {
-	u32 pc = READ_ONCE(current_thread_info()->preempt.count);
+	u64 pc = READ_ONCE(current_thread_info()->preempt_count);
 	pc -= val;
-	WRITE_ONCE(current_thread_info()->preempt.count, pc);
+	WRITE_ONCE(current_thread_info()->preempt_count, pc);
 
 	return pc;
 }
diff --git a/include/asm-generic/preempt.h b/include/asm-generic/preempt.h
index c8683c046615..7629e23102d1 100644
--- a/include/asm-generic/preempt.h
+++ b/include/asm-generic/preempt.h
@@ -59,14 +59,14 @@ static __always_inline void __preempt_count_sub(int val)
 	*preempt_count_ptr() -= val;
 }
 
-static __always_inline int __preempt_count_add_return(int val)
+static __always_inline unsigned long __preempt_count_add_return(int val)
 {
 	*preempt_count_ptr() += val;
 
 	return *preempt_count_ptr();
 }
 
-static __always_inline int __preempt_count_sub_return(int val)
+static __always_inline unsigned long __preempt_count_sub_return(int val)
 {
 	*preempt_count_ptr() -= val;
 
diff --git a/include/linux/interrupt_rc.h b/include/linux/interrupt_rc.h
index dd4444c61330..c044dc395452 100644
--- a/include/linux/interrupt_rc.h
+++ b/include/linux/interrupt_rc.h
@@ -27,7 +27,7 @@ DECLARE_PER_CPU(struct interrupt_disable_state, local_interrupt_disable_state);
 static inline void local_interrupt_disable(void)
 {
 	unsigned long flags;
-	int new_count;
+	unsigned long new_count;
 
 	WARN_ON_ONCE(in_nmi());
 
@@ -41,9 +41,25 @@ static inline void local_interrupt_disable(void)
 	}
 }
 
+#ifdef CONFIG_PREEMPTION
+static inline void local_interrupt_enable_reched(unsigned long pc)
+{
+	if (pc)
+		return;
+	/* No PREEMPT_NEED_RESCHED bit? Check tif_need_resched() */
+#ifndef PREEMPT_NEED_RESCHED
+	if (!tif_need_resched())
+		return;
+#endif
+	__preempt_schedule();
+}
+#else
+static inline void local_interrupt_enable_reched(unsigned long pc) {}
+#endif
+
 static inline void local_interrupt_enable(void)
 {
-	int new_count;
+	unsigned long new_count;
 
 	new_count = hardirq_disable_exit();
 
@@ -52,15 +68,8 @@ static inline void local_interrupt_enable(void)
 
 		flags = raw_cpu_read(local_interrupt_disable_state.flags);
 		local_irq_restore(flags);
-		/*
-		 * TODO: re-read preempt count can be avoided, but it needs
-		 * should_resched() taking another parameter as the current
-		 * preempt count
-		 */
-#ifdef CONFIG_PREEMPTION
-		if (should_resched(0))
-			__preempt_schedule();
-#endif
+
+		local_interrupt_enable_reched(new_count);
 	}
 }

▲ 这段把 __preempt_count_{add,sub}_return() 的返回类型放宽到 unsigned long(arm64 上改为读 64 位 preempt_count),从而把 bit31 的 PREEMPT_NEED_RESCHED 也带回给调用方。关键点:新增的 local_interrupt_enable_reched(pc) 用 if (pc) return; 快速跳过——只要 preempt_count 非 0(仍在软/硬中断或其他禁用上下文),就无需考虑抢占调度;只有归 0 且(无 NEED_RESCHED 位的架构上)tif_need_resched() 置位时才调用 __preempt_schedule()。为什么这么改:should_resched(0) 内部会 READ_ONCE(preempt_count) 重读同一变量,而 hardirq_disable_exit() 刚返回过它——直接用返回值即可,省一次热路径内存读。

💬 讨论焦点

Peter Zijlstra 对 NMI 补丁的审查(2026-02-03,针对 Lyude v17)
两条意见均针对 nmi_nesting 与 preempt_count_set():
作者回应:commit message 记录了 “Solve Steven Rostedt's comment on the BUG_ON() condition” 与 “Use preempt_count_set() in __nmi_exit() to avoid underflow”——即 boqun 采纳了 Steven Rostedt 对 BUG_ON 条件的意见,并主动用 preempt_count_set() 避免嵌套退出时计数下溢(来源:NMI 补丁 commit message,PATCH 01/24)。当前版本仍保留 preempt_count_set(),Peter 的“计算偏移避开 cmpxchg”建议尚未被采纳。
Peter Zijlstra 对 irq 优化补丁的质疑(2026-08-03)→ 作者撤回
Peter Zijlstra 针对本系列(rust-sync v7.3)的 irq: Optimize reschedule check in local_interrupt_enable() 回复:“As stated before; this whole thing is a mystery to me, this should not be needed.”——他对整个“计数式中断开关”基础设施的必要性存疑(此前在 v3 系列已表达过“as stated before”)。作者 Boqun Feng 随即回复:“I will drop this patch entirely per the discussion.”——即 irq 优化补丁按讨论意见整体撤回,不再进入最终 pull request。
影响:irq 优化补丁(13/24)已被作者撤回,报告对其性能收益的讨论属于对历史提交的分析;最终合入版本将不含该补丁。
其他讨论线索
NMI 补丁的最初来源是 Lyude Paul 的 v17 系列(PATCH v17 02/16,2026-01-21),cover letter 讨论区有 Gary Guo、Boqun Feng 的回复(Gary Guo、Boqun Feng,2026-01-26)。补丁系列在 rust-for-linux 与 x86/s390/arm64 维护者间经历了多轮(v17 → 本 pull)。未检索到 Thomas Gleixner、Paul McKenney 在本 pull 中对这两个补丁的直接公开回复。

📈 性能影响

场景/用例运行环境改进前改进后
(补丁未提供基准数据)———

补丁未提供 benchmark 数据,也不含“作者自报”的量化提升。以下为基于 diff 的机制层面逻辑分析(解读(AI 分析)),非实测。

NMI 补丁:主要收益是“腾位域”,而非直接减指令
从执行路径看,__nmi_enter()/__nmi_exit() 改造后仍要 __preempt_count_add/sub(HARDIRQ_OFFSET),并新增 __this_cpu_inc/dec(nmi_nesting) 与 preempt_count_set()(cmpxchg 循环)。因此在 on-CPU 指令数上,NMI 单次进出未必更少——Peter Zijlstra 也正是因此批评 preempt_count_set() 偏贵(解读(AI 分析):此为“为功能腾位付出的热路径代价”,作者目标是位域腾挪,不是 NMI 路径减指令)。真正的系统性收益是:preempt_count 空出 3 bit,解锁了 Rust SpinLockIrq 等新原语(功能/生态收益,off-CPU 上无直接影响)。
irq 补丁:中断开关热路径省一次内存读(已撤回)
local_interrupt_enable()(对应 spin_unlock_irq_enable())最外层解锁时,原实现 should_resched(0) 会从内存 READ_ONCE(preempt_count) 再读一次;改造后直接用 hardirq_disable_exit() 返回的 new_count。这是典型的 on-CPU 收益(减少一次内存读/依赖链),且发生在“自旋锁解锁 + 中断恢复”这条高频路径上。注意:该补丁已按 review 撤回,最终合入版本不含此优化。

on-CPU vs off-CPU:以上均为 on-CPU(指令/内存访问)层面的收益;无 off-CPU(锁等待/唤醒延迟)数据。补丁未提供 off-CPU 数据,此项为逻辑分析(解读(AI 分析))。

🔄 方案演进

两个补丁都有明确的前序版本,且演进受 review 推动:

NMI 补丁演进:Lyude v17(2026-01)→ 本 pull(2026-07)
Lyude Paul v17 02/16(2026-01-21):最初形态 +23/-8(3 文件),BUG_ON 条件为 __this_cpu_read(nmi_nesting) == UINT_MAX,__nmi_exit 用 __preempt_count_sub(NMI_OFFSET) 清位。
Peter Zijlstra review(2026-02-03):质疑 nmi_nesting 放置、preempt_count_set() 的 cmpxchg 开销。
本 pull 01/24(2026-07-31):boqun 按 Steven Rostedt 意见改 BUG_ON 条件为 >= 15,并改用 preempt_count_set() 避免嵌套退出下溢(commit message 明确标注)。
irq 补丁演进:v3(2026-06)→ 本 pull(2026-07)→ 撤回(2026-08)
Boqun v3 13/13(2026-06-05):diff 与本 pull 13/24 完全一致(5 文件 +32/-23)。
Peter Zijlstra(2026-08-03):“As stated before; this whole thing is a mystery to me, this should not be needed.”
Boqun Feng(2026-08-03):“I will drop this patch entirely per the discussion.”——撤回。
设计权衡 / 讨论推进
核心争议是 Peter Zijlstra 对“计数式中断开关”整体必要性的质疑(解读(AI 分析):他倾向沿用现有 spin_lock_irqsave/restore 语义,而非在 preempt_count 里维护独立的硬中断禁用计数)。作者选择撤回 irq 优化补丁,但 NMI 位域迁出(本报告主要分析对象)仍保留在 pull request 中——它独立解决“位域紧张”问题,不依赖 irq 优化补丁。

⚠️ 风险与局限

潜在回归 / 并发 / 边界
  • NMI 路径引入 cmpxchg(Peter 批评):preempt_count_set() 在 __nmi_enter()/__nmi_exit() 中会把 NMI 进出变成 cmpxchg 循环,理论上比原 __preempt_count_add/sub() 更贵。严重度 MINOR(NMI 非高频,且为功能腾位的必要代价;Peter 提供了可避开的替代写法)。(解读(AI 分析))
  • nmi_nesting 与 preempt_count 分处不同 cacheline:per-CPU 变量与 thread_info/lowcore 不必然同 cacheline,NMI 路径多一处内存访问(Peter 建议合并)。严重度 MINOR。(解读(AI 分析))
  • NMI 嵌套深度上限 15:BUG_ON(nmi_nesting >= 15) 是防御性断言,现实中 NMI 嵌套几乎不会超 1–2 层,风险极低;但若未来出现更深嵌套(如递归 NMI 风暴)会直接 BUG_ON 崩溃,属“失败模式”需注意。(解读(AI 分析))
  • irq 补丁的返回类型放宽:__preempt_count_{add,sub}_return() 从 int 改为 unsigned long,调用方若按 int 截断会丢位;本系列中其唯一调用方是 hardirq_disable_{enter,exit}(),改动被限定在可控范围。若未来新增调用方,需注意位宽语义。严重度 MAJOR(若实现有误)——但该补丁已撤回,实际风险归零。
严重度:MINOR(NMI 补丁为主)· review 质疑:Peter Zijlstra 的“整个机制存疑”已如实呈现,作者以撤回 irq 补丁回应;NMI 补丁的 cmpxchg/cacheline 意见仍未解决

🔗 交叉引用

📌 关联补丁 / 讨论
[PATCH 05/24] irq & spin_lock: Add counted interrupt disabling/enabling — 引入 local_interrupt_disable()/enable() 与 hardirq_disable_enter/exit(),是 irq 优化补丁依赖的基础设施。
[PATCH 02/24] preempt: Introduce HARDIRQ_DISABLE_BITS — 使用 NMI 补丁腾出的位域定义 8 bit 的 HARDIRQ_DISABLE_MASK(bits 16-23),并将 HARDIRQ 上移至 bits 24-27、NMI 标志置于 bit 28。
[PATCH 14/24] rust: Introduce interrupt module — Rust 侧计数式中断开关的用户接口(SpinLockIrq 前奏)。
Lyude Paul [PATCH v17 00/16] Refcounted interrupts, SpinLockIrq for rust — NMI 补丁原始系列(cover letter)。
[GIT PULL] Rust synchronization changes for v7.3 — 本系列 cover letter(git pull 请求,base 5e601ab3615c,head 93ff5657a5f1)。

✅ 关键洞察

  • 发现:两个补丁都在“preempt_count 热路径”上做减法——NMI 补丁把冗余的 4 bit NMI 嵌套计数迁到独立 per-CPU 计数器(腾位域给 HARDIRQ_DISABLE),irq 补丁让中断开关最外层解锁复用返回值、省一次 preempt_count 内存读。前者是后者(以及 Rust SpinLockIrq)的前提。
  • 证据:无基准数据(补丁未提供)。机制证据来自 diff 逐字:NMI_BITS 4→1、新增 nmi_nesting per-CPU 计数、__preempt_count_{add,sub}_return() 返回 unsigned long、local_interrupt_enable_reched(new_count)。
  • 边界:NMI 嵌套深度实际极少超过 1–2 层,上限 15 的 BUG_ON 几乎不可达;irq 优化仅对 CONFIG_PREEMPTION 生效(非 PREEMPT 时 local_interrupt_enable_reched() 为空函数),且 arm64 等无 PREEMPT_NEED_RESCHED 位的架构会退回查 tif_need_resched()。
  • 风险 / 建议:irq 优化补丁已由作者撤回(Peter Zijlstra 对计数式中断开关整体必要性存疑);NMI 补丁仍在 pull request 中,但 Peter 关于 preempt_count_set() cmpxchg 与 nmi_nesting 缓存局部性的两点意见尚未解决,建议后续版本按“计算偏移 + __preempt_count_add()”和“与 preempt_count 同 cacheline”的方向修订。
⚠️ 免责声明

本站内容均由 AI 基于公开知识辅助生成,仅供学习参考,请勿直接引用作为依据。作者不对信息的准确性、完整性及适用性作保证,亦不对因使用本站内容产生的任何损失承担责任。