0/6 已展开

LLM 分析

sched/proxy:延迟 donor 提交直到代理解析

系列概况

  • 标题:[PATCH RFC v2] sched/proxy: Defer donor commit until proxy resolution
  • 作者:Xukai Wang <kingxukai@zohomail.com>
  • 版本:v2(RFC)
  • 规模:1 个 patch
  • 修改文件:kernel/sched/core.c
  • 代码统计:+49 / -41
  • base-commit:19b7bdc3a1550ab2550427c33395bec7caeaf72d
  • change-id:20260707-sched-proxy-4404b6e97ad9
  • Message-ID:
    • cover 20260713-sched-proxy-v2-0-729170082633@zohomail.com
    • patch 20260713-sched-proxy-v2-1-729170082633@zohomail.com
  • 完整性:6 封邮件 = 1 cover + 1 patch + 4 回复(作者复述、John Stultz 提问、作者再次引用、kernel test robot 编译告警)

补丁目的

proxy execution 启用后,pick_next_task() 既选任务又通过 put_prev_set_next_task() 把候选 commit 到调度类的 current 状态。但选出的可能是 blocked donor,真正可以运行的 owner 要由 find_proxy_task() 解析代理链才能得到。若 find_proxy_task() 返回 NULL__schedule()pick_again,可此时已经发生过一次投机性的 set_next(),需要再撤销。

本 RFC 把“选”与“commit”解耦:pick_next_task() 只返回候选;put_prev_set_next_task()rq_set_donor() 推迟到 __schedule()find_proxy_task() 解析成功之后。

旧流程的问题

pick_next_task() 兼任“选 + commit”,与 __schedule() 中“proxy 解析后才知道真正运行哪个任务”相互交织。当候选是 blocked donor 时,find_proxy_task() 失败走 pick_again,但调度类的 current 状态已经被投机性切换,必须在重试前撤销,造成一次多余的 put_prev/set_next

新流程

pick_next_task() 退化为纯选择器。__schedule() 拿到候选后先解析 proxy 链;解析成功且不是退回 idle 才执行真正的 commit。失败路径不再留有“先 commit 再撤销”的痕迹,重试只需重新选。

关键实现

  • __pick_next_task() 删除 put_prev_set_next_task(rq, rq->donor, p) 调用。
  • pick_next_task() 出口 out_set_next 改名 out_return_next,去掉末尾的 commit 段。
  • proxy_resched_idle() 内新增 zap_balance_callbacks(rq),因为该函数做了一次 idle commit,回调必须在这里清掉。
  • proxy_deactivate() 增加 if (donor == rq->donor) 守卫:只有当传入的 donor 仍是已提交 donor 时才 proxy_resched_idle();否则只作为 queued 候选,可直接 block_task()
  • proxy_migrate_task() 注释改写:迁移的是 proxy 链中某个任务,候选不一定已 commit;丢锁前依旧把已 commit 的 donor 切到 idle,保持 rq 与 class 状态定义明确。
  • find_proxy_task() 增加出参 *donor,把调用方持有的候选交回 __schedule()
  • __schedule()donor = next;只在 find_proxy_task() 返回非 NULL 且非 idle 后执行:
    put_prev_set_next_task(rq, rq->donor, donor);
    rq_set_donor(rq, donor);
    
    外层 NULL/idle 路径不再无条件 zap_balance_callbacks()
static void __sched notrace __schedule(int sched_mode)
{
    ...
    donor = next;                          /* only a candidate, no commit yet */
    donor->blocked_donor = NULL;

    if (unlikely(donor->is_blocked)) {
        next = find_proxy_task(rq, donor, &rf);
        if (!next)                         /* candidate stays uncommitted */
            goto pick_again;
        if (next == rq->idle)              /* fall back to idle, no commit */
            goto pick_again;
    }

    /* commit only after proxy chain is resolved */
    put_prev_set_next_task(rq, rq->donor, donor);
    rq_set_donor(rq, donor);
    ...
}

Patch 概览

v1 -> v2 关键改动:

  • put_prev_set_next_task() / rq_set_donor()pick_next_task() 内部迁到 __schedule() 中 proxy/non-proxy 分支之后。
  • zap_balance_callbacks() 从外层 NULL/idle 路径收回 proxy_resched_idle(),紧邻 idle commit。
  • proxy_deactivate() / proxy_migrate_task() 注释与条件更新,反映“候选 != 已提交 donor”。
  • find_proxy_task() 增加 *donor 出参以解耦选择与解析。

旧流程 vs 新流程

OLD FLOW (commit happens inside pick_next_task)
=================================================
                       |
                       v
              +------------------+
              | pick_next_task() |
              |  - select task   |
              |  - set_next()    |  <-- commit before proxy resolution
              +--------+---------+
                       |
                       v
              +------------------+
              | find_proxy_task()|
              +--------+---------+
                       |
                NULL? --+--> go back to pick_again
                       |     but class current is already on
                       |     a blocked donor (must undo set_next)
                       v
                    retry

NEW FLOW (commit moved into __schedule after resolution)
========================================================
                       |
                       v
              +------------------+
              | pick_next_task() |
              |  - select only   |  <-- no commit, return candidate
              +--------+---------+
                       |
                       v
              +------------------+
              | find_proxy_task()|
              +--------+---------+
                       |
                NULL? --+--> pick_again (candidate not committed,
                       |     nothing to undo)
                       v
               not NULL and not idle?
                       |
                       v
              +------------------+
              | put_prev_set_next|
              | rq_set_donor()   |  <-- commit only on success
              +------------------+

类比

把调度器想成餐厅的点单流程:

  • 旧流程:服务员一边报菜名一边把菜直接端上桌(pick + set_next 一体)。如果临时发现后厨这道菜还没做(proxy 解析失败),服务员得先把端上来的菜撤回去,再去换别的菜——既浪费动作,又让“已上桌”这个状态短暂错乱。
  • 新流程:服务员先口头确认“这位客人点 X”(只选不 commit),确认 X 已经做好(proxy 解析成功)才端上桌。如果中途发现 X 还没好,直接换菜即可,不必先撤。

代理(proxy)则是“朋友托我先点,他随后就到”:调度器先用 donor 占位,确认 owner 真的可以上桌再端。

Highlight:风险与注意点

  • John Stultz 要求下一版 commit message 顶部回答“为什么 set_next_task(donor) 有问题?”——是单纯优化 pick_again 时的重复 put_prev/set_next,还是另有收益?影响是否可被 benchmark 测量?
  • kernel test robot 在 alpha-defconfig(GCC 16.1.0,W=1)下报 kernel/sched/core.c:5103:13: warning: 'zap_balance_callbacks' defined but not used:本 patch 把外层 zap_balance_callbacks() 调用全部收进 proxy_resched_idle(),但函数定义未变,alpha 配置路径上看不到任何调用方,需要加 __maybe_unused 或留至少一个外部调用点。
  • 本 patch 与更上游 proxy execution RFC 系列耦合:find_proxy_task() 新增的 *donor 出参、proxy 链解析语义需要 John Stultz 确认上游没有反向依赖(即是否依赖 rq->donor 在解析前就被更新)。
  • 选择与 commit 解耦后,任何在 pick_next_task() 中“信赖已 commit 状态”的旁路代码(如 hrtick、fair load tracking)必须确认不会提前看到未经 commit 的候选。
  • proxy_resched_idle() 现在内含 zap_balance_callbacks();调用顺序必须保证“idle commit -> zap -> 返回”,否则外层 lockdep 或回调链表可能产生双重 zap 风险。

版本变化

v1 -> v2:

  • put_prev_set_next_task() / rq_set_donor() 推迟到 proxy/non-proxy 分支之后再统一提交。
  • zap_balance_callbacks() 从外层 NULL/idle 路径收回,统一放进 proxy_resched_idle()
  • proxy_deactivate() / proxy_migrate_task() 注释由“必须先 proxy_resched_idle”改为“仅当仍是已提交 donor 时才需要”。
  • find_proxy_task() 出参 donor 上提到 __schedule() 持有。

一句话总结

pick_next_task() 改成“只选不交”,把 put_prev_set_next_task()rq_set_donor() 推迟到 __schedule()find_proxy_task() 解析成功之后执行,从而消除 proxy 解析失败时“投机提交再撤销”的浪费与状态混乱。