sched discussion
[PATCH] sched/proxy_exec: Limit find_proxy_task() chain depth to prevent CPU hang
LLM 分析
sched/proxy_exec:检测 proxy walk 死链环避免 CPU hang
系列概况
- 标题:
[PATCH vN] sched/proxy_exec: Detect cycles in proxy walks(v1 标题为 Limit find_proxy_task() chain depth to prevent CPU hang) - 作者:zhidao su (Xiaomi),
soolaugust@gmail.com - 版本:v1 → v5,共 5 轮迭代
- 规模:v1/v2 仅 17 行单文件改动;v3 跨 4 文件 +41/-2;v4 跨 3 文件 +30/-2;v5 跨 3 文件 +31/-2
- 修改文件:
kernel/sched/core.c、include/linux/sched.h、kernel/sched/sched.h、kernel/fork.c - 评审参与:John Stultz(Google,倾向简单深度上限)、K Prateek Nayak(AMD,主导 pick_seq 标记方案)
- 关键 Message-ID:
<20260414053625.3582936-1-soolaugust@gmail.com>(v1 首封);最新 v5 为<20260722120346.93000-1-soolaugust@gmail.com> - 完整性:thread 完整,5 个 patch 全部覆盖,含 17 封邮件
补丁目的
Proxy execution(PE)让被 mutex 阻塞的 donor 仍留在 runqueue 上,由 find_proxy_task() 沿 blocked_on 链找到一个可运行的 owner 代为执行。
当 A 等 B 的 mutex、B 又等 A 的 mutex 时:
- A 被
pick_next_task()选为 donor find_proxy_task()沿 A→mutex_B→owner=B→mutex_A→owner=A 永无止境- 整个遍历持有
rq->lock,调度器无法前进 - NMI watchdog 报
do_raw_spin_lockhard LOCKUP
补丁目标是在持 rq 锁时打断这种死链环——发现同一个 walk 内重复走到某个 task,就清掉它的 blocked_on 并 deactivate,让调度器继续工作。
旧流程的问题
未加防护的 find_proxy_task() 只检查 WARN_ON(owner == p),这只覆盖「任务等自己已经持有的 mutex」这一直接自环场景,对 A→B→A 这种多任务环完全没有检测。
后果:
__schedule()持rq->lock死循环- NMI watchdog 在
watchdog_thresh秒后报 hard LOCKUP - hung task detector 自身走不到该 task(被卡在
__schedule里) - 即便有 LOCKDEP,PE 下也不一定能拿到完整 splat
新流程
v5 最终方案:
- 给
rq加proxy_pick_seq计数器,进入find_proxy_task()自增 1 - 给
task_struct加proxy_pick_seq字段 - finalize owner 时,若
owner->proxy_pick_seq == rq->proxy_pick_seq,说明本次 walk 第二次见到该 task,立即__clear_task_blocked_on(p)+goto deactivate - task 重新入队时通过
sched_proxy_enqueue_task()把 marker 清 0
相比 v1~v2 的「加深度上限」「deactivate donor」「owner==donor 简单比对」三套近似方案,v5 的 pick_seq 标记能识别链中任意位置的环(A→B→C→A),不依赖回到原始 donor。
Patch 概览
v1: MAX_PROXY_CHAIN_DEPTH=64 + proxy_resched_idle() (+17, 1 file)
v2: depth=1024 + owner==donor + deactivate() (+17, 1 file)
v3: drop depth, (cpu,seq) per-pick marker (+41/-2, 4 files)
v4: single per-task proxy_pick_seq, clear in activate_task (+30/-2, 3 files)
v5: sched_proxy_enqueue_task(), owner-only mark, pr_warn_once (+31/-2, 3 files)
关键实现
v5 find_proxy_task() 末尾 owner finalize 处:
+ if (owner->proxy_pick_seq == rq->proxy_pick_seq) {
+ pr_warn_once("sched/pe: deadlock cycle detected, pid %d\n",
+ p->pid);
+ __clear_task_blocked_on(p, NULL);
+ goto deactivate;
+ }
+ owner->proxy_pick_seq = rq->proxy_pick_seq;
入口处自增 seq:
+ rq->proxy_pick_seq++;
新增 sched_proxy_enqueue_task() 在 task 重新入队时清 marker:
static inline void sched_proxy_enqueue_task(struct task_struct *p)
{
#ifdef CONFIG_SCHED_PROXY_EXEC
if (!sched_proxy_exec())
return;
p->proxy_pick_seq = 0;
#endif
}
activate_task() 在 enqueue_task 之前调用之,确保 stale seq 不跨 pick 串台。
类比
把 find_proxy_task() 想成探险家走迷宫:每进入一次新迷宫就在门口举起一面独一无二的旗(proxy_pick_seq++),走过的每间房墙上盖同号章(task->proxy_pick_seq = rq->proxy_pick_seq)。如果发现某间房的墙上已经有本次的章,说明走进死循环——最稳的做法不是继续走、也不是退回门口,而是直接把这间房锁上(__clear_task_blocked_on + deactivate),让它不再出现在候选名单里,调度器自然能换别的房探。
另一个类比:银行叫号系统。如果叫号机里又出现你已经领过的号码,就知道系统死循环了——正确做法是作废你这张票让你重排,而不是让叫号机继续无限重复叫同一个号。
Highlight:风险与注意点
- reschedule_idle 无法解环(v1 隐患):返回
rq->idle后__schedule()立刻再选同一 donor,soft loop。v2 改 deactivate 才真正打破。 - owner==donor 只覆盖最浅环(v2 隐患):漏掉 A→B→C→A 中段环,v3 引入 per-pick seq 才解决。
- 不要复用
blocked_donor:它已是 donor-stack backlink,加语义会污染 PE 模型;v3 引入独立 per-task marker 更可扩展。 - pick_seq 必须随 task activation 清理:否则上一轮 walk 的 seq 可能撞上本轮(v3→v4 期间 Prateek 提出的迁移/激活窗口期问题)。
- 单点 deactivate 仍不够:v5 一次只解一个 cycle 成员;futex 走 PE 后可能出现 N 节点长链,需要
proxy_migrate_task()那种一次性清整条 blocked_donor 链的 follow-up。 - detect 时 deactivate 的是 p,不是 owner:v17 讨论显示 deactivate owner 语义更自然,作者已倾向 v6 切换。
- futex→PE 即将让 bug 从理论变常态:John 在 v5 评审里点出 proxy futex 在排队,cycle 概率上升,patch 紧迫性升级。
版本变化
- v1→v2:
MAX_PROXY_CHAIN_DEPTH64→1024;proxy_resched_idle→proxy_deactivate;新增owner == donor检测。 - v2→v3:删深度上限;引入
(proxy_pick_cpu, proxy_pick_seq)per-pick 标记;新增 fork 初始化和proxy_migrate_task清理。 - v3→v4:合并成单
proxy_pick_seq;删 fork-time 初始化;改在activate_task清理;去掉READ_ONCE/WRITE_ONCE与 wraparound 防护。 - v4→v5:抽出
sched_proxy_enqueue_task();只 mark finalized owner,不 mark donor;WARN_ONCE→pr_warn_once;获 K Prateek NayakReviewed-by。 - v5 之后(邮件 17):deactivate 改 owner;考虑一次性清整条 blocked_donor 链。
一句话总结
用 rq 上的 proxy_pick_seq 给每次 proxy walk 盖独一无二章,任何被同一 walk 第二次走到的 task 立即 __clear_task_blocked_on 并 deactivate,从根上断开 A→B→A 类 mutex 环在持 rq 锁时 spin 死的硬挂路径。
+----------------------------+
| __schedule() 走 PE 路径 |
+-------------+--------------+
|
v
+----------------------------+
| find_proxy_task(donor) |
| rq->proxy_pick_seq++ |
+-------------+--------------+
|
walk blocked_on v
|
+------ p = donor, is_blocked ------+
| |
v v
+-------------------+ +-------------------+
| resolve owner=B | | resolve owner=C |
| (链继续往下走) | | (链继续往下走) |
+---------+---------+ +---------+---------+
| |
v v
finalize owner=B finalize owner=C
owner->seq != rq->seq owner->seq == rq->seq
| |
v v
mark B, continue pr_warn_once("deadlock
cycle detected, pid X")
|
v
__clear_task_blocked_on(p)
goto deactivate p
|
v
scheduler picks another
task, CPU makes progress