sched discussion
[PATCH 0/8] mm: introduce for_each_process_rcu and for_each_thread_rcu
LLM 分析
mm:引入 for_each_process_rcu 与 for_each_thread_rcu 宏
系列概况
- 标题:[PATCH 0/8] mm: introduce for_each_process_rcu and for_each_thread_rcu
- 作者:Ye Liu liuye@kylinos.cn
- 版本:v1(无明确的 vN 编号,日期 2026-09-04)
- 规模:8 个 patch;覆盖 17 个文件,+46 / -79 行
- 修改文件:include/linux/sched/signal.h、mm/oom_kill.c、mm/ksm.c、mm/memory-failure.c、kernel/cpu.c、kernel/freezer.c、kernel/hung_task.c、kernel/locking/lockdep.c、kernel/rcu/update.c、kernel/sched/core.c、kernel/sched/debug.c、kernel/trace/fgraph.c、kernel/unwind/deferred.c、fs/proc/base.c、fs/resctrl/rdtgroup.c、lib/is_single_threaded.c、security/landlock/tsync.c
- 代码统计:46 insertions(+), 79 deletions(-)
- Message-ID:20260904083001.553587-1-ye.liu@linux.dev
- 完整性:8 个 patch 全部到位;30 封邮件中包含 Sashiko AI 自动审查、Oleg/Lorenzo/Michal/Steven/Thomas/Günther/Justin/SJ/Miaohe 等多位 reviewer 回复,maintainer 反馈密集
补丁目的
在内核中,对 task_struct 链表(进程/线程)做迭代通常需要先调用 rcu_read_lock()、写 for_each_process(...) 循环、再写 rcu_read_unlock()。这种三行样板代码遍布 mm/、kernel/、fs/、lib/、security/,既冗长又容易在 break/goto/return 路径上忘记解锁。
本系列做两件事:
- 在
include/linux/sched/signal.h引入三个新宏for_each_process_rcu()、for_each_thread_rcu()、for_each_process_thread_rcu(),内部用scoped_guard(rcu)把 RCU 读锁与循环体绑定,自动管理锁的生命周期。 - 把内核里散落的手写
rcu_read_lock()/rcu_read_unlock()与guard(rcu)()全部替换为新宏;声明"无功能性变更"。
该想法是 Michal Hocko 在审 oom_kill 路径时建议的。
旧流程的问题
旧写法需要程序员手动管理 RCU 临界区:
rcu_read_lock();
for_each_process(p) {
/* 这里 break/goto/return 时容易漏 unlock */
...
}
rcu_read_unlock();
- 代码冗长,4 行模板只为保护一个循环;
- 任何非常规退出路径(
break、错误处理goto、早期return)都隐含漏锁风险; - 与 C++/Rust 风格的 RAII、
scoped_guard抽象相比,C 语言版本需要纪律性。
新流程
新宏把锁的获取与释放放进宏内部,由编译器插入 __attribute__((__cleanup__)) 风格的清理调用:
for_each_process_rcu(p) {
/* 离开循环体(包括 break/goto/return)自动 rcu_read_unlock */
...
}
锁的生命周期 = 循环体的词法作用域。
Patch 概览
- Patch 1/8:核心。在
include/linux/sched/signal.h增加 19 行,定义三个宏;引入#include <linux/cleanup.h>以拿到scoped_guard。 - Patch 2/8:mm/oom_kill.c,五处
rcu_read_lock/for_each_process改写。 - Patch 3/8:mm/ksm.c,单点改写。
- Patch 4/8:mm/memory-failure.c,四处改写(anon/file/fsdax/pfn 收集)。
- Patch 5/8:kernel/ 跨多个子系统 9 文件 11 处改写;
hung_task.c中unlock:label 因不再 unlock 变成空 label;fgraph.c把goto unlock改为goto free。 - Patch 6/8:fs/,
proc/base.c的__set_oom_adj与resctrl/rdtgroup.c的两个查询点。 - Patch 7/8:lib/
is_single_threaded.c,简化 ret/label/goto found 结构为直接的return false/true。 - Patch 8/8:security/landlock/tsync.c,两处
guard(rcu) + for_each_thread改写。
关键实现
核心宏定义(来自 Patch 1):
#define for_each_process_rcu(p) \
scoped_guard(rcu) \
for (p = &init_task ; (p = next_task(p)) != &init_task ; )
#define for_each_thread_rcu(p, t) \
scoped_guard(rcu) \
__for_each_thread((p)->signal, t)
/* Careful: this is a double loop, 'break' won't work as expected. */
#define for_each_process_thread_rcu(p, t) \
scoped_guard(rcu) \
for_each_process(p) for_each_thread(p, t)
要点:
scoped_guard(rcu)展开为一个带__attribute__((__cleanup__(rcu_read_unlock)))的局部变量定义;- 循环体结束(正常、break、goto、return)都触发 cleanup 调用,等价于自动
rcu_read_unlock(); - 第三个宏注释里已警示"双层循环中
break行为反直觉",因为break只跳出for_each_thread,会回到外层for_each_process。
Patch 5 中 kernel/hung_task.c 的一个副作用:原来 unlock: 标签后面紧跟 rcu_read_unlock();,宏替换后 unlock 不再有 unlock 动作,标签名变成误导。Günther Noack 提出应重命名标签。
Patch 7 中 lib/is_single_threaded.c 借助宏改写顺便把 ret = false; ... goto found; found: rcu_read_unlock(); return ret; 这种 label+goto 模式直接简化为 return false; ... return true;,是少有的"顺手改进"。
类比
可以把 for_each_process_rcu(p) 想成博物馆的参观通道:进入时刷身份证(rcu_read_lock),出口无论正门、侧门还是紧急出口都会自动刷一下离场记录(rcu_read_unlock)。scoped_guard(rcu) 就是门口的电子门禁——只要观众离开通道,闸机自动记录离场,无需每个出口单独安排一位员工值守。旧写法相当于每条出口都要站一个人提醒"别忘了打卡"。
线程组版本的双层循环(for_each_process_thread_rcu)就像大楼先按公司(进程)再按员工(线程)巡查:内层 break 只是停止查这家公司的员工,会跳到下一家公司,外层进程循环继续——这是注释里警告的反直觉点。
流程图:宏展开与控制流
call site:
for_each_process_rcu(p) {
do_something(p);
if (bad) break;
}
expands to (conceptually):
{
struct rcu_guard_struct ___rcu_guard \
__attribute__((__cleanup__(rcu_read_unlock))) = rcu_guard_init();
for (p = &init_task;
(p = next_task(p)) != &init_task; ) {
do_something(p);
if (bad) break; // __cleanup__ still fires
}
} // rcu_read_unlock() called here for normal exit
// break / goto / return also trigger __cleanup__
流程图:评审分歧
Ye Liu (author)
|
v
+--------------------+
| PATCH 0/8..8/8 |
+---------+----------+
|
+---------+-----------------------+---------------------+
| | | |
v v v v
Michal Oleg Thomas Gleixner Günther / SJ
Hocko Nesterov + Steven Rostedt / Justin / M.
(Ack x6) (命名混淆?) (宏命名 _rculock; (label 名 /
砍掉双层循环宏) pgoff RCU 位置)
Highlight:风险与注意点
- 命名混淆:Thomas Gleixner 建议把
*_rcu改名为*_rculock。现有list_for_each_entry_rcu()是"假定 RCU 区域已存在"的安全遍历宏,而新宏自己"取锁",两者语义相反。Oleg Nesterov 也提到这一命名令人困惑。若上游接受改名,Patch 2–8 都要跟随机械替换。 for_each_process_thread_rcu是双层循环:break行为反直觉,且唯一"跳出整体"的方法是goto。Thomas 明确表态不希望引入这个宏。scoped_guard配合goto跳出当前 scope 是 OK 的(tglx 在 Patch 1/8 与 1/8 后续回复中给出完整解释:只有"asm goto"才会在 clang 下编译失败,GCC 历史上发过 buggy code;普通goto由编译器在__cleanup__变量作用域离开时插入解锁调用),但要求读者对 macro 行为足够熟悉,文档成本高。- hung_task.c 残留 label:
unlock:现在不再 unlock,应改为如out:或干脆删除(用if (!this_round_count) return;替代)。 - memory-failure.c 的 RCU 临界区形状变化:SJ Park 在 Patch 4 上指出,
collect_procs_file中把pgoff = page_pgoff(folio, page);从 RCU 临界区里挪到了外面;Miaohe Lin 紧接着给出"with SJ's comment addressed"的 Acked-by,说明 v2 已经处理。 - checkpatch 误报:作者在 cover letter 中明示 Patch 1 会触发"复杂宏应加括号"警告,是 false positive——
scoped_guard是控制流结构,不是多语句宏。 - Sashiko AI 提示:在
alloc_retstack_tasklist里同时存在goto free(处理 EAGAIN 后释放缓存)和for_each_process_thread_rcu(自带 scoped_guard),mixed-paradigm 风险偏低,但提醒作者以后不要再在同一函数里把 goto error-path 与 scoped_guard 混用。 - Landlock 改动收益弱:Günther Noack 认为把
guard(rcu)(); for_each_thread换成for_each_thread_rcu没有显著优势,原写法更直白;这是一个"是否值得改"的争议点,需要作者权衡一致性与局部偏好。
版本变化
- 这是 v1,没有 v0 → v1 的演进记录。
- 邮件中能看到 v1 收到的反馈已经指向 v2 需要的修订:
- 改名为
*_rculock或在 commit message 中澄清命名差异; - 删除或重新设计
for_each_process_thread_rcu; - 修
hung_task.c的unlock:标签; - Patch 4 已经在 v2 处理 SJ 的 pgoff 位置反馈(从 Miaohe 的 Acked-by "with SJ's comment addressed" 推断)。
- 改名为
一句话总结
Ye Liu 提议用 scoped_guard(rcu) 包装 RCU 读锁,把"三行模板"压缩成一个宏,并据此把 mm/kernel/fs/lib/security 里 17 个文件的 79 行样板代码清掉;总体机械改动干净,但 Thomas Gleixner 对宏命名与双层循环版本提出原则性意见,预计 v2 会出现重命名与减一个宏的调整。