sched discussion
[PATCH RFC v3] sched/proxy: Defer donor commit until after proxy resolution
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 之后才提交,删掉这条空路径上的冗余调用。
旧流程的问题
pick_next_task()内部就put_prev_set_next_task(),候选一被选出立刻提交。- 候选可能是
blocked donor,要交给find_proxy_task()解析 proxy 链。 - 解析可能返回
idle或NULL;NULL会触发pick_again。 - 重选前后两次
put_prev_set_next_task()中的第一次被覆盖。 - 期间
rq->donor、cfs_rq->curr、rq->dl_server状态机被一个"终究被放弃"的候选污染,调试和 RT/DL balance 都可能受影响。
新流程
pick_next_task()只挑候选,返回前不再做put_prev_set_next_task()。__schedule()把pick拿到的结果作为donor;若donor->is_blocked,保存rq->dl_server、清零之,再进find_proxy_task()解析。- 解析结束后恢复
dl_server,再做put_prev_set_next_task(rq, rq->donor, donor),把类状态切到真正要跑的任务。 - 仅当
sched_proxy_exec()且prev != next时手动触发一次类回调put_prev_task/set_next_task,补齐 RT/DL balance 机会。 - 最后
rq_set_donor(rq, donor),把rq->donor与类状态对齐。 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_server;sched_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_task 因 rq->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 只挑候选,__schedule 在 find_proxy_task 解析完 proxy 链后才把 donor 提交到 rq,避免 proxy 解析失败时白白做一次 put_prev/set_next。