0/21 已展开

LLM 分析

Sleeping Owner Handling for Proxy Execution (v31) 系列分析

系列概况

  • 标题:[RESEND][PATCH v31 0/9] Sleeping Owner Handling for Proxy Execution (v31)
  • 作者:John Stultz jstultz@google.com(含 Vasily Gorbik、Christian Loehle、Andrea Righi、K Prateek Nayak 等署名 patch)
  • 版本:v31(RESEND 重发版)
  • 规模:9 个 patch,合计 +374/-43 行
  • 修改文件:include/linux/sched.h、init/init_task.c、kernel/fork.c、kernel/sched/core.c、kernel/sched/deadline.c、kernel/sched/ext/ext.c、kernel/sched/fair.c、kernel/sched/sched.h(共 8 个文件)
  • 代码统计:374 insertions, 43 deletions
  • Message-ID(封面)20260807035232.1881495-1-jstultz@google.com
  • 完整性:9 封 patch + 11 封 reviewer/maintainer 回复 + 6 封 tip-bot 应用回执;v31 部分 patch 已被合入 tip:sched/core

补丁目的

这是 Proxy Execution 系列计划五个阶段中的第五阶段 —— Sleeping Owner Handling。要解决的核心问题是:

当一个 task 正以 mutex owner 为 donor 做 proxy execution,结果 mutex owner 自己进入了 sleeping(比如被另一个锁或 I/O 阻塞),此时已经无法再把 owner 唤醒、让它加速被代理的 waiter。原来的方案是把 waiter 一直挂在 blocked_donor 链上等 owner 自然醒来,但 owner 沉睡期间整条链都没人加速。

本 patch 的做法是:当 find_proxy_task() 沿 blocked_on 链解析到 owner、发现 owner 在 sleeping 时,把 waiter 从 blocked_donor 链上摘下,挂到 owner 自己的 blocked_head 链表;等 owner 被 try_to_wake_up() 唤醒时,再统一把 owner 后面的整串 waiter 重新激活,让 owner 重新获得被 boost 的"公平份额"。

同时这一批次还附带若干 stabilization fix:DL yield 早退、core-sched cookie 错配回退、donor 被偷修复、nr_iowait/nr_uninterruptible 计数修正、整链迁移优化、sched_ext 标记新增 ENQUEUE_PROXY

旧流程的问题

  • find_proxy_task() 沿 blocked_on 链一路向上解析 owner。一旦链尾 owner 在 sleeping,链上的 waiter 失去了可被 boost 的调度上下文,链路上的加速完全停滞,可能形成长尾等待。
  • owner 被唤醒的瞬间,缺少把挂在 owner blocked_head 上的 waiter 一并叫醒的机制,容易出现"owner 醒但 waiter 还在睡"的优先级反转。
  • try_steal_cookie() 此前只跳过 src->core_picksrc->curr,加上 proxy-exec 后 src->donor 才是当前调度上下文;偷走 donor 会留下 cfs_rq->curr 的悬挂引用,下一次 pick 命中 WARN_ON_ONCE
  • yield_task_dl() 会把当前 DL 实体的 runtime 清零强制 sleep 到下次补给;当 DL 任务作为 donor 被代理时,proxy 路径不应把这种"假性 yield"传递给 donor。
  • core scheduling 已经在 __schedule() 前选好一个 cpu-wide cookie,但 find_proxy_task() 仍可能把执行上下文替换为一个 cookie 不匹配的可运行 owner,绕过 core sched 的挑选。
  • nr_iowait 计数:proxy 把 donor 跨 cpu 迁移后,donor 阻塞在 owner 的 rq 上时 nr_iowait 的加减发生在 owner cpu 而不是 donor 原 cpu,导致观察值不一致。
  • 老的 proxy_migrate_task() 一次只迁移一个任务,整条链反复进出 rq 锁,效率低。

新流程

  1. find_proxy_task() 沿 blocked_on 链解析 owner;若 owner 与当前 cpu cookie 不匹配(且已经在链中),回退到 proxy_resched_idle();否则停用 donor,让 __schedule() 重选。
  2. owner 处于 sleeping:调用 proxy_enqueue_on_owner(),把 waiter 入队到 owner->blocked_head、写 p->sleeping_owner = ownerget_task_struct(owner),再 block_task() 把 waiter 切到非 active。
  3. owner 被唤醒:无论走 try_to_wake_up() 还是 sched_ttwu_pending(),都会在持有 rq 锁的情况下调用 activate_blocked_waiters()
  4. activate_blocked_waiters() 把 owner 的整条 blocked_head 切到本地 bal_head(用 blocked_activation_node 防止并发进入),再循环调用 do_activate_blocked_waiter()
  5. do_activate_blocked_waiter()pi_lock 三连检查(TASK_WAKING / on_rq / on_cpu)后撤销 nr_uninterruptible / nr_iowait,再 proxy_set_task_cpu() + activate_task() + resched_curr()
  6. activate_task()sched_proxy_exec() 下额外做:从 sleeping_owner 摘除 waiter,并加 ENQUEUE_PROXY 入队 flag;sched_ext 的 enqueue_task_scx() 识别 ENQUEUE_PROXY 后清掉 SCX_ENQ_WAKEUP 再调用 BPF,避免把 blocked donor 的激活当成完整 wakeup。
  7. 跨 cpu 迁移时 proxy_migrate_task() 改为沿 blocked_donor 整链 deactivate,再用 __attach_tasks() 一次性 attach 到目标 rq。

Patch 概览

  • 1/9 sched/deadline: yield_task_dl 早退(Christian Loehle)
  • 2/9 sched/core: try_steal_cookie 跳过 src->donor(Vasily Gorbik)
  • 3/9 sched/core: try_steal_cookie 跳过 task_is_blocked 任务
  • 4/9 sched/core: cookie 不匹配的 owner 不做 proxy(Vasily Gorbik)
  • 5/9 sched: proxy_reset_donor() 同步 rq->next_class
  • 6/9 sched: 抽出 __attach_tasks() 共享 helper
  • 7/9 sched: proxy_migrate_task() 沿 blocked_donor 链整体迁移
  • 8/9 sched: 引入 sleeping owner 的 deactivated 处理(核心)
  • 9/9 sched/ext: 引入 ENQUEUE_PROXY 区分 wakeup 与 proxy 激活(Andrea Righi)

关键实现

task_structCONFIG_SCHED_PROXY_EXEC 下新增四个字段:

struct list_head blocked_head;             /* tasks blocked on this task */
struct list_head blocked_node;             /* our entry on someone elses blocked_head */
struct list_head blocked_activation_node;  /* bal_head membership */
struct task_struct *sleeping_owner;        /* task our blocked_node is enqueued on */

并配套 blocked_lock 做精细串行化;原 activate_task() 被拆成 __activate_task(),新的 activate_task()sched_proxy_exec() 路径里负责先把 waiter 从 sleeping_owner 摘掉,再在 blocked_lock 持锁期间入队,避免 find_proxy_task() 还在继续向本任务挂新 waiter。

proxy_enqueue_on_owner() 在持 rq 锁与 owner->blocked_lock 时把 waiter 挂到 owner 的链表,并 get_task_struct(owner) 平衡后续摘除时的 put_task_struct()

proxy_enqueue_on_owner(rq, owner, p):
  assert rq_lock(rq) held
  assert owner->blocked_lock held
  WARN_ON(p == owner)
  WARN_ON(!p->on_rq)
  WARN_ON(p->sleeping_owner)
  get_task_struct(owner)
  WRITE_ONCE(p->sleeping_owner, owner)
  list_add(&p->blocked_node, &owner->blocked_head)
  proxy_resched_idle(rq)
  block_task(rq, p)             /* deactivate + on_rq=0 */

activate_blocked_waiters() 的处理:

activate_blocked_waiters(target_rq, owner, wake_flags):
  if !sched_proxy_exec(): return
  raw_spin_lock_irqsave(owner->blocked_lock)
    if owner->blocked_activation_node already linked:  return  /* reentrancy guard */
    if owner->blocked_head empty:                      return
    get_task_struct(owner)
    list_add_tail(owner->blocked_activation_node, bal_head)  /* mark owner "in flight" */
  raw_spin_unlock_irqrestore(owner->blocked_lock)

  while bal_head not empty:
    take owner from bal_head
    raw_spin_lock_irqsave(owner->blocked_lock)
      list_replace_init(owner->blocked_head, tmp_head)
    raw_spin_unlock_irqrestore(owner->blocked_lock)
    put_task_struct(owner)
    for each p in tmp_head:
      do_activate_blocked_waiter(target_rq, p, en_flags)

do_activate_blocked_waiter() 的并发防御:

scoped_guard(raw_spinlock_irqsave, &p->pi_lock) {
    state = READ_ONCE(p->__state);
    if (state == TASK_WAKING) return;             /* avoid racing with ttwu */
    if (READ_ONCE(p->on_rq)) return;              /* already activated elsewhere */
    if (task_on_cpu(task_rq(p), p)) return;       /* stale on_cpu flag */
    if (p->in_iowait) {
        delayacct_blkio_end(p);
        atomic_dec(&task_rq(p)->nr_iowait);
    }
    proxy_set_task_cpu(p, target_cpu);
    rq_lock_irqsave(target_rq, &rf);
    if (p->sched_contributes_to_load)
        target_rq->nr_uninterruptible--;
    update_rq_clock(target_rq);
    activate_task(target_rq, p, en_flags);
    resched_curr(target_rq);
    rq_unlock_irqrestore(target_rq, &rf);
}

proxy_migrate_task() 改为整链迁移:

LIST_HEAD(migrate_list);
for (; p; p = p->blocked_donor) {
    WARN_ON(p == rq->curr);
    deactivate_task(rq, p, DEQUEUE_NOCLOCK);
    proxy_set_task_cpu(p, target_cpu);
    list_add(&p->se.group_node, &migrate_list);
}
__attach_tasks(target_rq, &migrate_list);

ENQUEUE_PROXY 是新增的入队 flag(0x00200000),由 sched/ext/ext.c::enqueue_task_scx() 识别后清掉 SCX_ENQ_WAKEUP,告诉 BPF 这是 blocked donor 的恢复,不是真正 wakeup。

类比

把 mutex owner 想成自习室里占座的学霸,waiter 想成蹭座的同伴。正常情况下学霸在线,同伴可以通过 proxy execution 让学霸替自己顶一下。

  • sleeping owner 处理就像:学霸睡着了(owner sleeping),管理员不能再让他继续顶座,于是把同伴从代理链上摘下来,登记到学霸的"等候名单"(blocked_head)里。学霸一旦被叫醒(try_to_wake_up()),管理员一次性把名单上的同伴全叫回去继续蹭座(activate_blocked_waiters())。如果名单上的某个同伴已经离开自习室了(on_rq==1on_cpu==1),就跳过,避免把人家赶回已经不属于他的座位。
  • try_steal_cookie 跳过 src->donor就像:从别人嘴里抢走他正在咬的苹果(donor 才是当前调度上下文),结果下一个进来的人会以为苹果还挂在树上。
  • cookie 不匹配回退就像:core scheduling 已经按"宿舍楼栋规则"选好了谁能进自习室,结果 proxy 想塞一个不属于这栋楼的 owner 进来,被门卫拦下、改派 idle。

Highlight:风险与注意点

  • Peter 在 patch 8/9 上指出:task_struct::sleeping_owner 标了 __guarded_by(&blocked_lock),但实际上是受 owner 的 blocked_lock 保护而非自身的,标注与实现不完全匹配,clang 也没报警;lockdep 注释与现实之间存在不一致。
  • Peter 进一步质疑 activate_blocked_waiters() 的双重调用:sched_ttwu_pending()try_to_wake_up() 各调一次,但 sched_ttwu_pending() 的 llist 项本身也会走 ttwu 路径,需要确认是否会引起重复激活。
  • Atul Kumar Pant 追问 do_activate_blocked_waiter() 中间 p 是否可能已经"finished running"导致 task_is_blocked() 假阴性;当前依赖 pi_lock + __state/on_rq/on_cpu 三连检查兜底。
  • K Prateek 提出 nr_iowait 计数归属差异:proxy 迁移后的 donor 阻塞在 owner rq 上时,加减计数发生在 owner cpu 而不是 donor 原始 cpu;Maria Yu 也报告过此前的 nr_iowait 不平衡,需要后续观察。
  • is_dl_boosted()CONFIG_RT_MUTEXES 下原判定不够准确,K Prateek 提了一个替代 patch,把"proxy 中 + donor != curr"作为 boosted 判定,并去掉 dl_task_of(dl_se) == rq->donor 这层检查;该替代 patch 已经在其 tip 上测试无崩溃。
  • 链式迁移复用 se.group_node:依赖 p 已经 deactivate、balancer 看不到,且全程持有 rq_lock,使用前提比较严格,新增调用点时需小心。
  • ENQUEUE_PROXY 标志只在 sched_ext 路径显式消费;如果未来 RT/RR/DL 等其它调度类引入自定义 enqueue hook,需要同步识别,避免把 blocked donor 激活误判为 wakeup。
  • Peter 在 commit message 中明确说 patch 8/9 的"locking 非常棘手",欢迎社区给出简化建议;后续 review 可能改写其中某些串行化路径。

版本变化

  • v31(本次 RESEND):合并 Christian 的 yield_task_dl 早退;K Prateek 的 sleeping owner 唤醒竞态修复(owner 与 waiter enqueue 之间的并行唤醒导致 waiter 卡住);Maria/Tengfei 的 nr_iowait 失衡修复(do_activate_blocked_waiter() 中调整加减时机);Andrea 的 ENQUEUE_PROXY 适配。
  • 整体系列:Andrea 的 sched_ext 与 proxy-exec 适配、K Prateek 的 core-sched 加固、Suleiman 的 proxy-enabled futex 第一版、Håkon Bugge 的 test-ww_mutex 更新都已合入完整分支;proxy-rwsems 修复了一个细微的 rwsem_mark_wake() race。
  • v30 之前:曾用独立 migration_node,后来改为复用 se.group_node(K Prateek 建议);v22 把节点移到 CONFIG_SCHED_PROXY_EXEC 下;v18 在 init_task 中初始化 migration_node(Suleiman 建议)。
  • v7/v6:把单链表重构成按 owner 分层的树形链表,唤醒时只激活目标 owner 的后代,避免误唤醒兄弟分支。
  • v5:从更大的 proxy 大 patch 中拆出 sleeping owner 处理逻辑。

一句话总结

v31 把 Proxy Execution 的"Sleeping Owner"机制补齐:owner 沉睡时把 waiter 挂入 owner 的 blocked_head,owner 醒来后一次性整链激活;并附带 DL yield 早退、core-sched cookie 错配回退、donor 偷取防护、整链迁移、sched_ext ENQUEUE_PROXY 等邻居修复,目前 v31 中若干 patch 已被 tip-bot 推入 tip:sched/core。