0/16 已展开

LLM 分析

sched:引入带 RCU 锁范围的进程/线程迭代宏并大规模转换

系列概况

  • 标题:[PATCH v2 0/8] sched: introduce for_each_process_rculock and for_each_thread_rculock
  • 作者:Ye Liu liuye@kylinos.cn
  • 版本:v2(前作 lore8 月 13 日 PATCH v1 与 9 月 4 日前身)
  • 规模:8 个 patch,共 17 个文件,+54/-81
  • 修改文件include/linux/sched/signal.h, fs/proc/base.c, fs/resctrl/rdtgroup.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, lib/is_single_threaded.c, mm/ksm.c, mm/memory-failure.c, mm/oom_kill.c, security/landlock/tsync.c
  • 代码统计:17 files changed, 54 insertions(+), 81 deletions(-)
  • Message-ID20260907081334.1152889-1-ye.liu@linux.dev
  • 完整性:完整 8/8 patch + 7 封回复(sashiko-bot 自动审阅 2 封、Lorenzo Stoakes 4 封、Oleg Nesterov 1 封);Lorenzo 对 patch 3/8 给 Reviewed-by,Oleg 对 patch 1/8 给 Reviewed-by,Michal Hocko 全程 Acked-by,patch 2/8 由 SJ Park Review,patch 4/8 由 Miaohe Lin Ack,patch 8/8 由 Justin Suess 与 Günther Noack Review。

补丁目的

内核中现存的 for_each_process()for_each_thread()for_each_process_thread() 周边通常要手写 rcu_read_lock() / rcu_read_unlock()guard(rcu)()。这套系列:

  1. include/linux/sched/signal.h 新增三个 _rculock 宏,把加解锁放进 scoped_guard(rcu) 作用域;
  2. 把全内核 17 个文件 21 处手工锁配对一一改写为新宏调用;
  3. 解决"return/goto/多重 break 时漏写解锁"的脆弱性问题,让锁范围与循环体作用域一一对应。

旧流程的问题

每个站点都重复 rcu_read_lock() ... for_each_* ... rcu_read_unlock() 模板:

rcu_read_lock();
for_each_process(p) {
    do_something(p);
    if (cond)
        return -EAGAIN;   /* 漏解锁 */
}
rcu_read_unlock();

多重 return/goto/复杂 break 易漏写 rcu_read_unlock(),留下 RCU 临界区泄漏;反之锁范围比预期大、阻塞 writer。

新流程

for_each_process_rculock(p) {
    do_something(p);
    if (cond)
        return -EAGAIN;   /* scoped_guard 自动解锁 */
}

锁范围 = 循环体作用域,离开循环(break/goto/return)自动释放。

Patch 概览

  • 1/8 sched:在 signal.h 中定义三个宏本体。
  • 2/8 mm/oom_kill:5 处(oom_cpuset_eligibleselect_bad_processdump_taskstask_will_free_mem__oom_kill_process)。
  • 3/8 mm/ksm:1 处(collect_procs_ksm)。
  • 4/8 mm/memory-failure:4 处(anon/file/fsdax/pfn),并解释 page_pgoff() 在锁外仍安全。
  • 5/8 kernel:9 文件(cpu/freezer/hung_task/lockdep/rcu/sched.core/sched.debug/fgraph/unwind)。
  • 6/8 fsproc/base.c + resctrl/rdtgroup.c
  • 7/8 libis_single_threaded.c
  • 8/8 security/landlocktsync.c 两处。

关键实现

核心定义(patch 1/8):

#define for_each_process_rculock(p)                  \
    scoped_guard(rcu)                                \
    for (p = &init_task ; (p = next_task(p)) != &init_task ; )

#define for_each_thread_rculock(p, t)                \
    scoped_guard(rcu)                                \
    __for_each_thread((p)->signal, t)

#define for_each_process_thread_rculock(p, t)        \
    scoped_guard(rcu)                                \
    for_each_process(p) for_each_thread(p, t)

scoped_guard(rcu) 来自 <linux/cleanup.h>,离开作用域自动 rcu_read_unlock()。双重循环里 break 只退内层 for_each_thread(),要同时跳出两层必须 goto,这一约束被加进注释。patch 5/8 把 hung_task.c 过时的 unlock: 标签改名 out:fgraph.cgoto unlock 改为 goto free,因为解锁已不再需要手动写在标签处。

典型调用(patch 4/8,collect_procs_anon):

for_each_process_rculock(tsk) {
    struct task_struct *t = task_early_kill(tsk, force_early);
    if (t)
        add_to_kill_anon(t, page, vma, to_kill, addr);
}

类比

把 RCU 读锁想象成图书馆借阅证:

  • 旧流程:进门先在柜台拿证(rcu_read_lock),一本本翻书(for_each_process),最后必须自己记得把证交回柜台。中途换书、走开、跑去看别的房间,柜台就一直等你回来——但你可能忘了还。
  • 新流程:借阅证塞进"智能卡套"(scoped_guard(rcu)),只要你离开那个阅览区(循环作用域),卡套就会自动归还柜台。换书、跑开、临时出去都不用操心。
  • 双重循环 = "先按姓氏翻目录(外层 process)再按名字翻抽屉(内层 thread)"。只 break 只能跳出抽屉,要离开整区必须 goto——让卡套感知到你要结束整个借阅。

Highlight:风险与注意点

  1. goto + scoped_guard 混用:patch 5/8 在 fgraph.cgoto unlock 改为 goto freehung_task.c 同步改名 out:。sashiko-bot 把这条标为 Low,所有手动跳转都要确认目标仍在 guard 作用域内。
  2. break 仅退内层for_each_process_thread_rculock 双重循环里 break 不会触发 RCU 解锁(外层 for 还在跑),需 goto 才能完整退出,已写进注释(Thomas Gleixner 提示)。
  3. page_pgoff() 在锁外仍安全:patch 4/8 把 page_pgoff() 挪到 RCU 临界区外,只读 folio->index,不依赖 RCU 保护结构(SJ Park 验证)。
  4. checkpatch 误报:patch 1 会触发"Macros with complex values should be enclosed in parentheses"警告,作者说明是 scoped_guard 控制流伪语句造成的误报。
  5. 覆盖维护者:每封 patch 都 CC 对应子系统 maintainer(Lorenzo 建议)。
  6. 机械转换回归风险:21 处站点需逐个确认没有遗漏 rcu_read_lock() 之外的额外语义(如 preempt_disable)被错误合并。

版本变化

v1 → v2 主要改动(cover letter 列出):

  1. 宏名 *_rcu*_rculock,避免与现有 *_rcu() 列表迭代器混淆(Steven Rostedt,Thomas Gleixner 同意)。
  2. for_each_process_thread_rculock 注释强化,指出 break 只退内层(Thomas Gleixner)。
  3. hung_task.c 中过时的 unlock: 标签改名 out:(Günther Noack)。
  4. patch 4 commit message 明确 page_pgoff() 在 RCU 临界区外安全(SJ Park)。
  5. 每封 patch 都 CC 相关 maintainer(Lorenzo Stoakes)。
  6. patch 1 前缀由 mm: 改为 sched:(Michal Hocko)。
  7. 校对所有 patch 的 Acked-by/Reviewed-by 列表,避免漏维护者。

一句话总结

通过 scoped_guard(rcu) 把 RCU 读锁与 for_each_process/thread 系列迭代宏绑定到同一作用域,把内核 17 个文件 21 处手写锁配对统一收敛为一行宏调用,消除漏锁漏解锁的脆弱性。

+------------------+ +----------------------+
|  Old: hand pair |         | New: _rculock macro |
+------------------+         +----------------------+
| rcu_read_lock()  |         | for_each_*_rculock(p)|
| for_each_*(p) {  |   ==> |   scoped_guard(rcu)  |
|   ...            |         |   { |
|   return X       |         |     ...              |
|     --> leak! |         |     return X         |
| } |         |     --> auto unlock  |
| rcu_read_unlock()|         |   }                  |
|                  |         | } // auto unlock     |
+------------------+         +----------------------+

Double loop (for_each_process_thread_rculock):
+---------------------------------------------+
| break  -> exits only inner for_each_thread  |
| goto   -> exits both loops + auto unlock    |
+---------------------------------------------+