0/25 已展开

LLM 分析

Proxy Execution v30:睡眠 Owner 处理 + Core Scheduling 兼容 RFC

系列概况

  • 标题:[PATCH v30 0/7] Sleeping Owner Handling for Proxy Execution (v30),后接 K Prateek Nayak 的 [RFC PATCH 0/3] sched/core: Allow proxy execution to work with core scheduling
  • 作者:John Stultz(Google);Vasily Gorbik、Peter Zijlstra、K Prateek Nayak 提供关键 patch
  • 版本:v30(7 patches)+ RFC v1(3 patches),均基于 proxy-exec-v30-7.2-rc1
  • 规模:6 文件修改,约 +349/-43 行;RFC 子系列再 +129 行
  • 修改文件include/linux/sched.hinit/init_task.ckernel/fork.ckernel/sched/core.ckernel/sched/fair.ckernel/sched/sched.h
  • 代码统计:v30 主系列 kernel/sched/core.c +338 行、kernel/sched/fair.c 16 行、sched.h +19 行;RFC 0/3 主改 kernel/sched/core.c +129
  • Message-ID:首发 <20260701214615.3773339-1-jstultz@google.com>
  • 完整性:7 封 patch + 8 封 v30 讨论 + 4 封 RFC patch + 4 封 RFC 回复 = 25 封,thread 完整闭合

补丁目的

Proxy Execution 的核心思想:任务 A 因 mutex 阻塞在 task B 上时,让 B 在 A 的 CPU 上“代理执行”,避免 B 等不到 CPU 时 A 长时间空转。

v30 这一段主要解决一个关键缺口:当 mutex owner 自己也在睡眠(如在 futex 或别的等待上)时,没有办法直接 boost owner。本系列的方案是把 waiter 从 runqueue 摘下来,挂到 owner 的私有 blocked_head 链表上,等 owner 一旦被唤醒,再在同一 runqueue 上把它激活,从而让 waiter 立刻又能 boost owner 继续运行。

RFC 子系列则解决一个交叉问题:Core Scheduling 会先选一个 core-wide cookie,再让被选中的任务变成 donor/scheduling context;如果 proxy 出的 mutex owner cookie 与之不符,会绕过 core-sched 的 cookie 选择。

旧流程的问题

  • 被阻塞就空转:mutex owner 一旦睡眠,waiter 只能挂起,无法被推进。
  • Donor 跟 rq->curr 可能分裂:proxy-exec 下 src->donor 是当前调度上下文,与 src->curr 不同;之前 try_steal_cookie() 不知道这层差异,可能偷走 donor,让源 rq 的 cfs_rq->curr 指向已被迁移走的任务,触发 WARN_ON_ONCE in put_prev_entity()
  • 被偷的 blocked_on 任务无意义:偷过去 proxy 逻辑也会把它再迁回 owner rq,纯属浪费。
  • Cookie 不匹配破坏 core-sched:proxy 把 cookie 不一致的 owner 直接放上去,绕过 core-sched 的 cookie 选择。
  • next_class 没复位proxy_reset_donor() 没切 rq->next_class,与 proxy_resched_idle() 的修复不一致。

新流程

  1. 进入 find_proxy_task() 时,先沿 blocked_donor 指针走完整条链。
  2. 如果 owner 可运行且 cookie 匹配,把它当 next 任务执行。
  3. 如果 owner 当前不在 runqueue 上!on_rq || se.sched_delayed),调用 proxy_enqueue_on_owner():把 waiter deactivatelist_add 到 owner->blocked_head,并通过 block_task() 把 waiter 计入不可运行统计。
  4. Owner 一旦被唤醒进入 activate_task(),先 proxy_remove_from_sleeping_owner() 把自己从 owner 的等待链表上摘下,再 __activate_task()
  5. Owner 唤醒路径调用 activate_blocked_waiters(),把整个 blocked_head(以及级联下去的子树)拉下来,递归式(用临时链表避免真正递归)唤醒所有 waiter,使它们与 owner 同 rq。
  6. Cookie 不匹配时:RFC 0/3 通过 sched_core_swap_pick()rq->core_pick 换成 owner,让 owner 的 cookie 成为 core cookie。
              wakeup of owner
                   |
                   v
       activate_blocked_waiters()
                   |
   pulls blocked_head -> bal_head (under blocked_lock)
                   |
        +----------+----------+----------+ ... (tree walk)
        |          |          |
   waiter A    waiter B    waiter C
   (deact)     (deact)     (deact)
        |          |          |
        +--> migrate/activate on owner's rq
  pick_next_task()
        |
        v
  rq->donor = next (blocked_donor)
        |
        v
  find_proxy_task()
        |
        +--- owner on_rq & cookie match --> return owner
        |
        +--- owner sleeping --> proxy_enqueue_on_owner()
        |        deactivate waiter
        |        list_add(waiter, owner->blocked_head)
        |
        +--- cookie mismatch (RFC) --> sched_core_swap_pick()
                 retry with owner as core_pick_leader

Patch 概览

  • 1/7 sched/core: Don't steal a proxy-exec donor(Vasily Gorbik)
    try_steal_cookie() 中加 p == src->donor 守卫,避免偷走当前调度上下文,留下 cfs_rq->curr 悬空。
  • 2/7 sched/core: Avoid migrating blocked_on tasks
    try_steal_cookie() 跳过 task_is_blocked(p),因为 proxy 会把它们迁回。
  • 3/7 sched/core: Don't proxy-exec unmatched cookie lock owners(Vasily Gorbik)
    cookie 不匹配时退化成 idle 或 deactivate donor,避免绕过 core-sched;这是 RFC 的“临时版”。
  • 4/7 sched: Switch rq->next_class in proxy_reset_donor()
    proxy_resched_idle() 对齐,确保 next_class 跟着切。
  • 5/7 sched: Break out core of attach_tasks() helper into sched.h
    抽出 __attach_tasks() 给 chain migration 用(K Prateek 建议)。
  • 6/7 sched: Migrate whole chain in proxy_migrate_task()
    沿 blocked_donor 链表一次性迁移整条链,从 v12 一直演进到 v25(改用 se.group_node)。
  • 7/7 sched: Add deactivated (sleeping) owner handling to find_proxy_task()
    本系列最大一块:在 task_struct 上加 blocked_head/blocked_node/blocked_activation_node/sleeping_ownerproxy_enqueue_on_owner() 把 waiter 挂到 owner;activate_blocked_waiters() 在 owner 唤醒时整条链激活。

RFC 0/3 子系列:

  • 1/3 Track the core-wide pick leader:在 struct rqcore_pick_leader,记录哪个 rq 在 core-sched 里决定 cookie。
  • 2/3 Fix forceidle when lock owner has mismatched cookie:把 Vasily 的 patch 抽成 sched_core_swap_pick(),修正 forceidle 计数与 queue_core_balance()
  • 3/3 Swap to lock owner for core-wide pick on core-cookie mismatch:通过 core_pick_blocked_donor + restart_multi 二次 pick,把 owner 提升为 core_pick,让 owner 的 cookie 主导 core cookie。

关键实现

/* proxy_enqueue_on_owner() 摘 waiter,挂到 owner 链表 */
static void proxy_enqueue_on_owner(struct rq *rq,
                                   struct task_struct *owner,
                                   struct task_struct *p)
{
    lockdep_assert_rq_held(rq);
    lockdep_assert_held(&owner->blocked_lock);

    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);
    block_task(rq, p, READ_ONCE(p->__state));
}
/* activate_task() 在 owner 唤醒时把自己从链表摘下 */
void activate_task(struct rq *rq, struct task_struct *p, int en_flags)
{
    if (!sched_proxy_exec()) {
        __activate_task(rq, p, en_flags);
        return;
    }
    lockdep_assert_rq_held(rq);
    proxy_remove_from_sleeping_owner(p);
    raw_spin_lock(&p->blocked_lock);
    WARN_ON(task_cpu(p) != cpu_of(rq));
    __activate_task(rq, p, en_flags);
    raw_spin_unlock(&p->blocked_lock);
}
/* RFC: pick 阶段 cookie 交换 */
if (!sched_cpu_cookie_match(rq, next)) {
    next = sched_core_swap_pick(rq, next);
    if (next == RETRY_TASK)
        goto pick_again;
}

锁顺序:调用 proxy_enqueue_on_owner() 时必须同时持 rq->lock 与 owner->blocked_lock;__activate_task()blocked_lock 内执行,确保 owner 唤醒后不会有新的 waiter 在它上面挂入。get_task_struct(owner) + put_task_struct(owner) 防止“最后一个 put 释放正在持锁的 owner”。

类比

把这套机制想成“银行窗口叫号系统”:

  • 客户 A 在 3 号窗口(CPU)办业务,但需要柜员 B 出示一份盖章材料才能继续。
  • B 此刻在仓库盘点(睡眠),3 号窗口叫号器把 A 暂时从队列里抽出来,贴到 B 的待办清单(blocked_head)上。
  • B 回到窗口、按下工号(被唤醒),3 号窗口立即把 A 及所有跟着 A 的子客户一起叫回,重新排队。
  • 整条“待办清单”是 B 私有的,避免其他窗口的柜员顺手把它处理掉。

RFC 那部分则像“多柜台共享一个工牌”:core-sched 给整层楼定一个工牌(cookie),原本是叫号时按工牌选人;如果发现 B 的工牌跟当前选的人不一致,不是直接把 B 拉上来,而是先把“选定者”换成 B,再用 B 的工牌重新排队选人。

Highlight:风险与注意点

  • owner 唤醒后再入链的窗口期:Prateek 提到在看到 !owner->on_rq 之后到真正调用 list_add 之前,owner 可能已经被别的路径唤醒并 enqueue,导致误把已运行的 task 阻塞。修复建议:在拿到 blocked_lock重新检查 owner->on_rq,再决定是否入链。
  • proxy_migrate_task() 整链迁移的 list 复用:v25 把 se.group_node 复用为迁移 list,前提是 p 已 deactivate 且持 rq_lock。如果 deactivate 失败或锁丢失,可能出现把还在平衡路径中的实体挂回去的竞争。
  • rq->next_class 遗漏proxy_reset_donor() 没切 next_class,4/7 patch 已修;类似 helper(如未来 reset/cleanup 类)都要同步检查。
  • sched_core_swap_pick() 在 !CORE_SCHED 下 BUG_ON:2/3 RFC patch 中 BUG_ON() 路径需要 #ifdef CONFIG_SCHED_CORE 包裹;否则在 !CONFIG_SCHED_CORE && CONFIG_SCHED_PROXY_EXEC 配置下编译告警或误触发。
  • core-cookie 切换可能影响 forceidle 统计:把 owner 换成 core_pick_leader 时,forceidle 的增减顺序敏感;RFC 通过 core_forceidle_seq 修正,但 chain migration 期间可能仍短暂 miscount。
  • 整体性能回归:v30 涵盖 activate_blocked_waiters() 优化(针对 !proxy 性能下降),仍需 Prateek 复测完整系列。
  • sched_ext 兼容未完全跑通:Andrea Righi 的 sched_ext + proxy 兼容性改进被并入但作者承认“还没充分测试”,是后续验证重点。
  • RT/DL chain balancing 还未稳定:chain migration 与 RT_PUSH_IPI 的不变式尚未充分验证;目前 vanilla upstream RT_PUSH_IPI 也被认为有问题。
  • 更广的锁原语迁移:未来要扩展到 pi-futex、Binder PI,最终目标替换 rt_mutex 以适配 PREEMPT_RT。

版本变化

v30 → 与前几版相比的关键变更(来自 cover letter):

  • 基于 Peter Zijlstra 的 cleanup(7.2-rc1)rebase。
  • activate_blocked_waiters() 加了优化,弥补与 !proxy 路径的性能差距。
  • 拉入 Andrea Righi 的 sched_ext + proxy 兼容改进(待更完整测试)。
  • 6/7 patch 注明历史:v12 起使用 migration_node → v22 移到 CONFIG_SCHED_PROXY_EXEC → v25 改用 se.group_node,并集成 attach_tasks() 拆分。

一句话总结

v30 让 Proxy Execution 在 mutex owner 自己睡眠时把 waiter 挂在 owner 的私有链表上,等 owner 唤醒时再整链激活;RFC 0/3 进一步让 core scheduling 通过 pick-leader 机制在 cookie 不一致时主动让 owner 接管 core cookie,从而解决与 core-sched 的冲突。