sched discussion
[PATCH v7 0/6] Introduce per-CPU debugfs files
LLM 分析
scheduler debugfs:Introduce per-CPU debugfs files(v7)
系列概况
- 标题:
[PATCH v7 0/6] Introduce per-CPU debugfs files - 作者:Aaron Tomlin
<atomlin@atomlin.com> - 版本:v7(前序 v1–v6,跨度2026-07-28 至 2026-08-26,共 7 版)
- 规模:6 个 patch,7 个文件,合计 +248/-127 行(cover letter 统计)
- 修改文件:
kernel/sched/core.c、kernel/sched/deadline.c、kernel/sched/debug.c、kernel/sched/fair.c、kernel/sched/rt.c、kernel/sched/sched.h、kernel/sched/topology.c - 代码统计:core.c(±)、debug.c 大幅改写、fair.c 新增 RCU 宏、rt.c(±)、sched.h 新增调试头文件宏- Message-ID(首封):
20260826224238.936456-1-atomlin@atomlin.com - 完整性:6 个子 patch 完整;后续 v7 回复涉及 Vincent Guittot、Peter Zijlstra、Aaron Tomlin 自身;附
base-commit: 68e37487810a3da43c48340fab7a55b3b6efdae3、-- 2.55.0
补丁目的
本系列同时解决三件事:
- 给
struct rq上的rd指针打__rcu注释,让 Sparse /CONFIG_PROVE_RCU把整个调度子系统纳入正确的 RCU 检查。 - 修补
/sys/kernel/debug/sched/debug打印路径里的 4 类 RCU 违例:UAF(dl_bw、task_struct、mm_struct)、TOCTOU(p->mm)、以及leaf_cfs_rq_list的无锁遍历在 churn 时反向回跳触发 RCU stall。 - 新增
/sys/kernel/debug/sched/cpu/cpu<N>/debug的 per-CPU 调试入口,离线时返回-ENODEV。
旧流程的问题
/sys/kernel/debug/sched/debug 走 seq_file,不持任何锁读取 rq->rd、rq->curr、p->mm 和 leaf_cfs_rq_list:
rq->rd由rq_attach_root()用普通 C 赋值发布,旧 root_domain 经call_rcu(&old_rd->rcu, free_rootdomain)回收;写端缺 release barrier,读端缺 RCU 临界区。rq->curr走task_pid_nr()时若进程正在exit,put_task_struct()走call_rcu释放task_struct。sched_show_numa()对p->mm的检查与mm->numa_scan_seq引用不在同一原子窗口。for_each_leaf_cfs_rq_safe()展开为list_for_each_entry_safe,无READ_ONCE;cgroup throttle/wakeup 频繁 re-link 时会出现「读到 next 指针但内部字段未初始化」、或反向遍历触发 RCU stall。
读端打印数据「best-effort」没错,但 UAF/Oops 是真错,不是「数据陈旧」。
新流程
按 6 个 patch 顺次:
- Patch 1:
rq->rd加__rcu;core/deadline/fair/rt/topology 的无锁读者改rcu_dereference_sched()(读路径)或rcu_dereference_protected()(持锁路径);写端rq_attach_root()改rcu_assign_pointer();init 用RCU_INIT_POINTER()。 - Patch 2:
print_dl_rq()包guard(rcu)(),dl_bw走rcu_dereference();rq_attach_root()走rcu_assign_pointer()。 - Patch 3:
print_cpu()中rq->curr在rcu_read_lock()/rcu_read_unlock()临界区内用rcu_dereference()取,再做task_pid_nr()。 - Patch 4:
sched_show_numa()用task_lock(p)/task_unlock(p)包裹p->mm检查与P(mm->numa_scan_seq),与exit_mm()在同一锁下完成 NULL 与释放。 - Patch 5:新增
for_each_leaf_cfs_rq_rcu()(list_for_each_entry_rcu),打印时包guard(rcu)(),并加SCHED_DEBUG_MAX_ITER=4096的 per-CPU circuit-breaker,超出时显式打SCHED_DEBUG_TRUNCATED_MSG。 - Patch 6:
debugfs_create_dir("cpu")下为每个 possible CPU 建cpu<N>子目录,挂debug文件,复用print_cpu();用cpu_online()做离线检查。
Patch 概览
+------------------------+----------------------------+---------------------------+
| Patch | Key path | Fix type |
+------------------------+----------------------------+---------------------------+
| 1/6 rq->rd __rcu | core/deadline/fair/rt/... | RCU annotation + readers |
| 2/6 print_dl_rq UAF | debug.c + topology.c | rq->rd writer+reader |
| 3/6 print_cpu UAF | debug.c | rq->curr UAF |
| 4/6 sched_show_numa | debug.c | p->mm TOCTOU + UAF |
| 5/6 print_cfs_stats | debug.c + fair.c + sched.h | leaf list RCU + ceiling |
| 6/6 per-CPU debugfs | debug.c | new debugfs entry |
+------------------------+----------------------------+---------------------------+
关键实现
/* Patch 1 -- let rq->rd become a real RCU pointer */
struct root_domain *rd = rcu_dereference_protected(rq->rd,
lockdep_is_held(&rq->__lock));
cpumask_set_cpu(rq->cpu, rd->online);
/* Patch 2 -- writer uses rcu_assign_pointer */
void rq_attach_root(struct rq *rq, struct root_domain *rd)
{
...
rcu_assign_pointer(rq->rd, rd);
}
/* Patch 3 -- rq->curr must hold RCU read lock */
rcu_read_lock();
SEQ_printf(m, " .%-30s: %ld\n", "curr->pid",
(long)(task_pid_nr(rcu_dereference(rq->curr))));
rcu_read_unlock();
/* Patch 4 -- p->mm and exit_mm share task_lock */
task_lock(p);
if (p->mm)
P(mm->numa_scan_seq);
task_unlock(p);
/* Patch 5 -- RCU-safe traversal with a safety ceiling */
#define for_each_leaf_cfs_rq_rcu(rq, cfs_rq) \
list_for_each_entry_rcu(cfs_rq, &(rq)->leaf_cfs_rq_list, \
leaf_cfs_rq_list)
guard(rcu)();
for_each_leaf_cfs_rq_rcu(cpu_rq(cpu), cfs_rq) {
if (--max_iter < 0) { /* print truncation notice and break */ }
}
/* Patch 6 -- per-CPU debugfs */
d_cpu_dir = debugfs_create_dir("cpu", debugfs_sched);
for_each_possible_cpu(cpu) {
snprintf(buf, sizeof(buf), "cpu%lu", cpu);
d_cpu = debugfs_create_dir(buf, d_cpu_dir);
debugfs_create_file("debug", 0444, d_cpu,
(void *)cpu, &sched_debug_cpu_fops);
}
类比
把 /sys/kernel/debug/sched/debug 想成一份全校体检报告:4000 份体检表一页纸打出来,想盯单个人都没法单独取;而且每份表都是「边印边改」的,没有任何保护,医生(reader)可能拿到半张表就被护士(writer)擦掉重写。
Patch 1–5 是在报告打印处加复印 + 装订:把每份表(rq->rd、rq->curr、p->mm、leaf_cfs_rq_list)放进 RCU 临界区复印一份,确保复印期间原件不会被销毁,同时给每页盖个「最多 4000 页」的章防止死循环。
Patch 6 是新增一条分诊通道 cpu/cpu<N>/debug:想盯某个人直接走分诊台,他下班了(offline)柜台直接亮「未到岗」。
Highlight:风险与注意点
- 必须全系列合入:Patch 2 写端
rcu_assign_pointer()单独合入没意义——读者还在无锁读会留下 release barrier 对不齐的窗口。 task_lock(p)与 RCU 不要混:Patch 4 选用task_lock()而非rcu_read_lock()是因为exit_mm()自身就在task_lock(p)内做mmput(),混用 RCU 无法阻止mmput立即释放。SCHED_DEBUG_MAX_ITER=4096是兜底:cgroup throttle 风暴下读文件仍可能截断,但比 RCU stall 报警 + watchdog 软死好得多;截断提示对现场解释非常关键。cpu_online(cpu)必须放在single_open之后读 m->private,否则 hot-unplug 边沿仍可能 race;当前实现把它放在sched_debug_cpu_show内是稳妥的。- helper 命名争论:Peter 倾向复用现有
rcu_dereference_sched_domain(),Aaron 倾向新增rcu_dereference_root_domain()维持rd/sd命名对称;最终 Peter 表示「随便,要么 alias 一下」。 - 遗留风险:
print_cpu()输出仍包含大量rq->lock之外读取的字段(rq->clock、rq->nr_running等),本系列只修了已发现的高危点,未做全字段审计。
版本变化
- v6 → v7:所有
rq->rd读取按 lock-held/lockless 拆分语义;移除!CONFIG_CGROUP_SCHEDstub 上的__printf(3,4)消除-Wformat警告;扩展 Patch 5 解释SCHED_DEBUG_MAX_ITERper-CPU 含义与 cfs_rq 回收安全保证。 - v5 → v6:rebase 到
tip/sched/core (sched-core-2026-08-17)。 - v4 → v5:新增 Patch 1(
rq->rd__rcu);print_dl_rq()用guard(rcu)()+rcu_dereference();sched_show_numa()改task_lock();SCHED_DEBUG_MAX_ITER由 1024 升 4096;SEQ_printf_task_group_path移到sched.h,strcpy换strscpy;Patch 5 的 Fixes 修正为039ae8bcf7a5。 - v3 → v4:Patch 1 用
rcu_dereference(rq->curr)保留__rcu;Patch 2 增rcu_assign_pointer();新增 Patch 3(TOCTOU viaREAD_ONCE(p->mm));Patch 4 增迭代上限。 - v2 → v3:
print_cpu()加rcu_read_lock()+READ_ONCE();print_dl_rq()防 hot-unplug/cpuset race;新增for_each_leaf_cfs_rq_rcu()。 - v1 → v2:commit message 围绕「大 SMP 上针对性调试」重新组织;
sched_debug_cpu_show()用cpu_online()返回-ENODEV(Zhan Xusheng 提议)。
一句话总结
本系列把 /sys/kernel/debug/sched/debug 里 4 处 RCU 违规(rq->rd 缺注解、rq->curr UAF、p->mm TOCTOU、leaf list 反向遍历)一次性清理,并新增 cpu/cpu<N>/debug 让大机器上能按 CPU 抓现场。