0/17 已展开

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.cinclude/linux/sched.hkernel/sched/sched.hkernel/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_lock hard 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 最终方案:

  1. rqproxy_pick_seq 计数器,进入 find_proxy_task() 自增 1
  2. task_structproxy_pick_seq 字段
  3. finalize owner 时,若 owner->proxy_pick_seq == rq->proxy_pick_seq,说明本次 walk 第二次见到该 task,立即 __clear_task_blocked_on(p) + goto deactivate
  4. 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:风险与注意点

  1. reschedule_idle 无法解环(v1 隐患):返回 rq->idle__schedule() 立刻再选同一 donor,soft loop。v2 改 deactivate 才真正打破。
  2. owner==donor 只覆盖最浅环(v2 隐患):漏掉 A→B→C→A 中段环,v3 引入 per-pick seq 才解决。
  3. 不要复用 blocked_donor:它已是 donor-stack backlink,加语义会污染 PE 模型;v3 引入独立 per-task marker 更可扩展。
  4. pick_seq 必须随 task activation 清理:否则上一轮 walk 的 seq 可能撞上本轮(v3→v4 期间 Prateek 提出的迁移/激活窗口期问题)。
  5. 单点 deactivate 仍不够:v5 一次只解一个 cycle 成员;futex 走 PE 后可能出现 N 节点长链,需要 proxy_migrate_task() 那种一次性清整条 blocked_donor 链的 follow-up。
  6. detect 时 deactivate 的是 p,不是 owner:v17 讨论显示 deactivate owner 语义更自然,作者已倾向 v6 切换。
  7. futex→PE 即将让 bug 从理论变常态:John 在 v5 评审里点出 proxy futex 在排队,cycle 概率上升,patch 紧迫性升级。

版本变化

  • v1→v2MAX_PROXY_CHAIN_DEPTH 64→1024;proxy_resched_idleproxy_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_ONCEpr_warn_once;获 K Prateek Nayak Reviewed-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