0/1 已展开

LLM 分析

sched/proxy:延迟 donor 提交直到 proxy 解析完成

系列概况

  • 标题:[PATCH RFC v3] sched/proxy: Defer donor commit until after proxy resolution
  • 作者:Xukai Wang
  • 版本:v3(正文仅以链接回引 v1 / v2,无独立 diff 摘要)
  • 规模:单 patch,仅改 kernel/sched/core.c
  • 修改文件:kernel/sched/core.c
  • 代码统计:改动集中在 finish_task / zap_balance_callbacks / __pick_next_task / pick_next_task / proxy_resched_idle / proxy_deactivate / proxy_migrate_task / find_proxy_task / __schedule
  • Message-ID:20260827-sched-proxy-v3-1-1af63ac5ae56@zohomail.com
  • 完整性:commit message +完整 diff + 临时计数器论证;末尾一句被截断(...how frequently the redundant put_prev/set_next ca),但论证已自洽

补丁目的

pick_next_task() 当前耦合的"挑选任务"与"立即提交"两件事拆开:

  • 当前流程:挑到候选就立刻 put_prev_set_next_task(),把它写进 rq->donor 与类状态。
  • proxy 模式下,被选中的可能是 blocked donor,还要再走 find_proxy_task() 解析 proxy 链。
  • 若解析返回 NULL__schedule()pick_again 重选,前一次的 put_prev/set_next 工作即被覆盖、纯属浪费。

目标:在 __schedule() 拿到真正可跑的 next 之后才提交,删掉这条空路径上的冗余调用。

旧流程的问题

  1. pick_next_task() 内部就 put_prev_set_next_task(),候选一被选出立刻提交。
  2. 候选可能是 blocked donor,要交给 find_proxy_task() 解析 proxy 链。
  3. 解析可能返回 idleNULLNULL 会触发 pick_again
  4. 重选前后两次 put_prev_set_next_task() 中的第一次被覆盖。
  5. 期间 rq->donorcfs_rq->currrq->dl_server 状态机被一个"终究被放弃"的候选污染,调试和 RT/DL balance 都可能受影响。

新流程

  1. pick_next_task() 只挑候选,返回前不再做 put_prev_set_next_task()
  2. __schedule()pick 拿到的结果作为 donor;若 donor->is_blocked,保存 rq->dl_server、清零之,再进 find_proxy_task() 解析。
  3. 解析结束后恢复 dl_server,再做 put_prev_set_next_task(rq, rq->donor, donor),把类状态切到真正要跑的任务。
  4. 仅当 sched_proxy_exec()prev != next 时手动触发一次类回调 put_prev_task/set_next_task,补齐 RT/DL balance 机会。
  5. 最后 rq_set_donor(rq, donor),把 rq->donor 与类状态对齐。
  6. proxy_deactivate() / proxy_migrate_task() 中只在 @donor == rq->donor 时才走 proxy_resched_idle(),避免错撤。

Patch 概览

位置修改要点
finish_task()增加 #ifdef CONFIG_SCHED_PROXY_EXEC 包裹的 zap_balance_callbacks
zap_balance_callbacks()加同条件 #ifdef 包裹
__pick_next_task()删除两处 put_prev_set_next_task(rq, rq->donor, p)
pick_next_task()out_set_next 改名为 out_return_next,删 put_prev_set_next_task
proxy_resched_idle()真正做 idle 提交后追加 zap_balance_callbacks(rq)
proxy_deactivate()把对 proxy_resched_idle() 改为 if (donor == rq->donor) 条件调用
proxy_migrate_task()注释更新,说明被迁移任务不一定是当前 rq->donor
find_proxy_task()新增局部变量 donor,为 __schedule 解耦做准备
__schedule()pick / commit 解耦;保存并恢复 rq->dl_serversched_proxy_exec() 守卫下的兜底 put/set

关键实现

pick_next_task() 不再"提交"

out_return_next:
    return next;

out_set_next 处的 put_prev_set_next_task(rq, rq->donor, next); 被移除。

__schedule() 的提交顺序

donor = pick_next_task(rq, &rf);
rq->next_class = donor->sched_class;
next = donor;
donor->blocked_donor = NULL;
if (unlikely(donor->is_blocked)) {
    struct sched_dl_entity *donor_dl_server = rq->dl_server;
    rq->dl_server = NULL;

    next = find_proxy_task(rq, donor, &rf);
    ...
    rq->dl_server = donor_dl_server;
}

put_prev_set_next_task(rq, rq->donor, donor);

if (sched_proxy_exec() &&
 donor == rq->donor && prev != next) {
    donor->sched_class->put_prev_task(rq, donor, donor);
    donor->sched_class->set_next_task(rq, donor, true);
}

rq_set_donor(rq, donor);

dl_server 在解析窗口临时为 NULL,结束恢复;prev != next 时手动调一次类回调,弥补 put_prev_set_next_taskrq->donor == donor 而短路导致的 RT/DL balance 缺失。

双重 zap 与 proxy_deactivate

static inline struct task_struct *proxy_resched_idle(struct rq *rq)
{
    ...
    zap_balance_callbacks(rq); /* 不 drop rq->lock 的路径在这里 zap */
}

static void proxy_deactivate(struct rq *rq, struct task_struct *donor)
{
    if (donor == rq->donor)
        proxy_resched_idle(rq);
    ...
}

proxy_resched_idle 注释明示:持有锁返回 __schedule() 的路径只 zap 一次;后续会丢锁再选任务时再 zap 一次。未提交的候选不一定是 rq->donor,所以 proxy_deactivate 不能无条件撤 idle。

流程图

+-----------------------+      +----------------------+      +--------------------------+
| pick_next_task()       | ---> | find_proxy_task()    | ---> | put_prev_set_next_task() |
|   returns donor only   |      | resolves proxy chain |      | commits donor to rq      |
+-----------------------+      +----------------------+      +--------------------------+
         |                            | |
         v                            v                              v
   rq->donor unchanged          +------+------+ rq_set_donor(rq, donor)
   rq->dl_server saved          |  NULL | idle |                 rq->dl_server restored
                                +------+------+                     put/set callbacks fire |   |
                                retry|   |run idle v   v
                              __schedule() loops
                              back to pick_next_task()

类比

把餐厅点餐做比喻:

  • 旧流程 = 服务员一拿到菜单就喊厨房下单(commit),结果客人改主意(proxy 解析失败),厨房已经炒了一份没人要的菜。
  • 新流程 = 服务员先确认客人真的下单(解析),再让厨房真正动手。这样厨房只看到最终决定,不会做无用功。

rq->donor 像灶台上"正在炒的菜",未提交的 donor 候选像"刚写下来的点单"——只有确认后才需要把灶台状态从旧菜切到新菜。

Highlight:风险与注意点

  • dl_server 临时 NULL 区间:解析期间 rq->dl_server = NULL,任何新增 early-return 必须先恢复,否则 DL server 会失效或漏更新。
  • 双重 zap_balance_callbacks:注释明确"持有锁路径 zap 一次,会丢锁的路径再 zap 一次"。任何在 proxy_resched_idle 之外的返回路径都要遵守同样规则,避免 balance callback 累积。
  • rq->donor 与类 curr 的时序分离:pick 阶段不再 commit,rq->donor 滞后于实际选出的 task。调试器 / 打印要意识到这一点,否则会误判"这个 rq 怎么 donor 还没换"。
  • put_prev_set_next_task 短路rq->donor == donor 时类回调不会真正调用,sched_proxy_exec() 守卫下的兜底手动调用就是为了补这一刀,新增路径不要忘记。
  • 数据支撑充分:60s 压力测试中 3224 次 NULL 重试里 0 次选回同一 donor,证明 defer 提交几乎没有"代价"——之前的 commit 几乎总是徒劳。

版本变化

  • v1 → v2:内部重构(正文以链接引用前版),无独立 diff 摘要。
  • v2 → v3:当前公开版。进一步把 pick 与 commit 解耦,并在 __schedule() 中显式保存 /恢复 rq->dl_server,同时调整 proxy_deactivate / proxy_migrate_task 的引用语义。

一句话总结

把"选中即提交"拆成两阶段:pick_next_task 只挑候选,__schedulefind_proxy_task 解析完 proxy 链后才把 donor 提交到 rq,避免 proxy 解析失败时白白做一次 put_prev/set_next