0/30 已展开

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-ID20260904083001.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 路径上忘记解锁。

本系列做两件事:

  1. include/linux/sched/signal.h 引入三个新宏 for_each_process_rcu()for_each_thread_rcu()for_each_process_thread_rcu(),内部用 scoped_guard(rcu) 把 RCU 读锁与循环体绑定,自动管理锁的生命周期。
  2. 把内核里散落的手写 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.cunlock: label 因不再 unlock 变成空 label;fgraph.cgoto unlock 改为 goto free
  • Patch 6/8:fs/,proc/base.c__set_oom_adjresctrl/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 残留 labelunlock: 现在不再 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.cunlock: 标签;
    • 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 会出现重命名与减一个宏的调整。