sched discussion
[PATCH v30 0/7] Sleeping Owner Handling for Proxy Execution (v30)
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.h、init/init_task.c、kernel/fork.c、kernel/sched/core.c、kernel/sched/fair.c、kernel/sched/sched.h - 代码统计:v30 主系列
kernel/sched/core.c+338 行、kernel/sched/fair.c16 行、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()的修复不一致。
新流程
- 进入
find_proxy_task()时,先沿blocked_donor指针走完整条链。 - 如果 owner 可运行且 cookie 匹配,把它当 next 任务执行。
- 如果 owner 当前不在 runqueue 上(
!on_rq || se.sched_delayed),调用proxy_enqueue_on_owner():把 waiterdeactivate、list_add到 owner->blocked_head,并通过block_task()把 waiter 计入不可运行统计。 - Owner 一旦被唤醒进入
activate_task(),先proxy_remove_from_sleeping_owner()把自己从 owner 的等待链表上摘下,再__activate_task()。 - Owner 唤醒路径调用
activate_blocked_waiters(),把整个blocked_head(以及级联下去的子树)拉下来,递归式(用临时链表避免真正递归)唤醒所有 waiter,使它们与 owner 同 rq。 - 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_owner;proxy_enqueue_on_owner()把 waiter 挂到 owner;activate_blocked_waiters()在 owner 唤醒时整条链激活。
RFC 0/3 子系列:
- 1/3 Track the core-wide pick leader:在
struct rq加core_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 的冲突。