sched discussion
[RESEND][PATCH v31 0/9] Sleeping Owner Handling for Proxy Execution (v31)
LLM 分析
Sleeping Owner Handling for Proxy Execution (v31) 系列分析
系列概况
- 标题:[RESEND][PATCH v31 0/9] Sleeping Owner Handling for Proxy Execution (v31)
- 作者:John Stultz jstultz@google.com(含 Vasily Gorbik、Christian Loehle、Andrea Righi、K Prateek Nayak 等署名 patch)
- 版本:v31(RESEND 重发版)
- 规模:9 个 patch,合计 +374/-43 行
- 修改文件:include/linux/sched.h、init/init_task.c、kernel/fork.c、kernel/sched/core.c、kernel/sched/deadline.c、kernel/sched/ext/ext.c、kernel/sched/fair.c、kernel/sched/sched.h(共 8 个文件)
- 代码统计:374 insertions, 43 deletions
- Message-ID(封面):20260807035232.1881495-1-jstultz@google.com
- 完整性:9 封 patch + 11 封 reviewer/maintainer 回复 + 6 封 tip-bot 应用回执;v31 部分 patch 已被合入 tip:sched/core
补丁目的
这是 Proxy Execution 系列计划五个阶段中的第五阶段 —— Sleeping Owner Handling。要解决的核心问题是:
当一个 task 正以 mutex owner 为 donor 做 proxy execution,结果 mutex owner 自己进入了 sleeping(比如被另一个锁或 I/O 阻塞),此时已经无法再把 owner 唤醒、让它加速被代理的 waiter。原来的方案是把 waiter 一直挂在 blocked_donor 链上等 owner 自然醒来,但 owner 沉睡期间整条链都没人加速。
本 patch 的做法是:当 find_proxy_task() 沿 blocked_on 链解析到 owner、发现 owner 在 sleeping 时,把 waiter 从 blocked_donor 链上摘下,挂到 owner 自己的 blocked_head 链表;等 owner 被 try_to_wake_up() 唤醒时,再统一把 owner 后面的整串 waiter 重新激活,让 owner 重新获得被 boost 的"公平份额"。
同时这一批次还附带若干 stabilization fix:DL yield 早退、core-sched cookie 错配回退、donor 被偷修复、nr_iowait/nr_uninterruptible 计数修正、整链迁移优化、sched_ext 标记新增 ENQUEUE_PROXY。
旧流程的问题
find_proxy_task()沿blocked_on链一路向上解析 owner。一旦链尾 owner 在 sleeping,链上的 waiter 失去了可被 boost 的调度上下文,链路上的加速完全停滞,可能形成长尾等待。- owner 被唤醒的瞬间,缺少把挂在 owner
blocked_head上的 waiter 一并叫醒的机制,容易出现"owner 醒但 waiter 还在睡"的优先级反转。 try_steal_cookie()此前只跳过src->core_pick与src->curr,加上 proxy-exec 后src->donor才是当前调度上下文;偷走 donor 会留下cfs_rq->curr的悬挂引用,下一次 pick 命中WARN_ON_ONCE。yield_task_dl()会把当前 DL 实体的 runtime 清零强制 sleep 到下次补给;当 DL 任务作为 donor 被代理时,proxy 路径不应把这种"假性 yield"传递给 donor。- core scheduling 已经在
__schedule()前选好一个 cpu-wide cookie,但find_proxy_task()仍可能把执行上下文替换为一个 cookie 不匹配的可运行 owner,绕过 core sched 的挑选。 nr_iowait计数:proxy 把 donor 跨 cpu 迁移后,donor 阻塞在 owner 的 rq 上时nr_iowait的加减发生在 owner cpu 而不是 donor 原 cpu,导致观察值不一致。- 老的
proxy_migrate_task()一次只迁移一个任务,整条链反复进出 rq 锁,效率低。
新流程
find_proxy_task()沿blocked_on链解析 owner;若 owner 与当前 cpu cookie 不匹配(且已经在链中),回退到proxy_resched_idle();否则停用 donor,让__schedule()重选。- owner 处于 sleeping:调用
proxy_enqueue_on_owner(),把 waiter 入队到owner->blocked_head、写p->sleeping_owner = owner、get_task_struct(owner),再block_task()把 waiter 切到非 active。 - owner 被唤醒:无论走
try_to_wake_up()还是sched_ttwu_pending(),都会在持有 rq 锁的情况下调用activate_blocked_waiters()。 activate_blocked_waiters()把 owner 的整条blocked_head切到本地bal_head(用blocked_activation_node防止并发进入),再循环调用do_activate_blocked_waiter()。do_activate_blocked_waiter()用pi_lock三连检查(TASK_WAKING / on_rq / on_cpu)后撤销nr_uninterruptible/nr_iowait,再proxy_set_task_cpu()+activate_task()+resched_curr()。activate_task()在sched_proxy_exec()下额外做:从sleeping_owner摘除 waiter,并加ENQUEUE_PROXY入队 flag;sched_ext 的enqueue_task_scx()识别ENQUEUE_PROXY后清掉SCX_ENQ_WAKEUP再调用 BPF,避免把 blocked donor 的激活当成完整 wakeup。- 跨 cpu 迁移时
proxy_migrate_task()改为沿blocked_donor整链 deactivate,再用__attach_tasks()一次性 attach 到目标 rq。
Patch 概览
- 1/9 sched/deadline: yield_task_dl 早退(Christian Loehle)
- 2/9 sched/core: try_steal_cookie 跳过 src->donor(Vasily Gorbik)
- 3/9 sched/core: try_steal_cookie 跳过 task_is_blocked 任务
- 4/9 sched/core: cookie 不匹配的 owner 不做 proxy(Vasily Gorbik)
- 5/9 sched: proxy_reset_donor() 同步 rq->next_class
- 6/9 sched: 抽出 __attach_tasks() 共享 helper
- 7/9 sched: proxy_migrate_task() 沿 blocked_donor 链整体迁移
- 8/9 sched: 引入 sleeping owner 的 deactivated 处理(核心)
- 9/9 sched/ext: 引入 ENQUEUE_PROXY 区分 wakeup 与 proxy 激活(Andrea Righi)
关键实现
task_struct 在 CONFIG_SCHED_PROXY_EXEC 下新增四个字段:
struct list_head blocked_head; /* tasks blocked on this task */
struct list_head blocked_node; /* our entry on someone elses blocked_head */
struct list_head blocked_activation_node; /* bal_head membership */
struct task_struct *sleeping_owner; /* task our blocked_node is enqueued on */
并配套 blocked_lock 做精细串行化;原 activate_task() 被拆成 __activate_task(),新的 activate_task() 在 sched_proxy_exec() 路径里负责先把 waiter 从 sleeping_owner 摘掉,再在 blocked_lock 持锁期间入队,避免 find_proxy_task() 还在继续向本任务挂新 waiter。
proxy_enqueue_on_owner() 在持 rq 锁与 owner->blocked_lock 时把 waiter 挂到 owner 的链表,并 get_task_struct(owner) 平衡后续摘除时的 put_task_struct():
proxy_enqueue_on_owner(rq, owner, p):
assert rq_lock(rq) held
assert owner->blocked_lock held
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)
proxy_resched_idle(rq)
block_task(rq, p) /* deactivate + on_rq=0 */
activate_blocked_waiters() 的处理:
activate_blocked_waiters(target_rq, owner, wake_flags):
if !sched_proxy_exec(): return
raw_spin_lock_irqsave(owner->blocked_lock)
if owner->blocked_activation_node already linked: return /* reentrancy guard */
if owner->blocked_head empty: return
get_task_struct(owner)
list_add_tail(owner->blocked_activation_node, bal_head) /* mark owner "in flight" */
raw_spin_unlock_irqrestore(owner->blocked_lock)
while bal_head not empty:
take owner from bal_head
raw_spin_lock_irqsave(owner->blocked_lock)
list_replace_init(owner->blocked_head, tmp_head)
raw_spin_unlock_irqrestore(owner->blocked_lock)
put_task_struct(owner)
for each p in tmp_head:
do_activate_blocked_waiter(target_rq, p, en_flags)
do_activate_blocked_waiter() 的并发防御:
scoped_guard(raw_spinlock_irqsave, &p->pi_lock) {
state = READ_ONCE(p->__state);
if (state == TASK_WAKING) return; /* avoid racing with ttwu */
if (READ_ONCE(p->on_rq)) return; /* already activated elsewhere */
if (task_on_cpu(task_rq(p), p)) return; /* stale on_cpu flag */
if (p->in_iowait) {
delayacct_blkio_end(p);
atomic_dec(&task_rq(p)->nr_iowait);
}
proxy_set_task_cpu(p, target_cpu);
rq_lock_irqsave(target_rq, &rf);
if (p->sched_contributes_to_load)
target_rq->nr_uninterruptible--;
update_rq_clock(target_rq);
activate_task(target_rq, p, en_flags);
resched_curr(target_rq);
rq_unlock_irqrestore(target_rq, &rf);
}
proxy_migrate_task() 改为整链迁移:
LIST_HEAD(migrate_list);
for (; p; p = p->blocked_donor) {
WARN_ON(p == rq->curr);
deactivate_task(rq, p, DEQUEUE_NOCLOCK);
proxy_set_task_cpu(p, target_cpu);
list_add(&p->se.group_node, &migrate_list);
}
__attach_tasks(target_rq, &migrate_list);
ENQUEUE_PROXY 是新增的入队 flag(0x00200000),由 sched/ext/ext.c::enqueue_task_scx() 识别后清掉 SCX_ENQ_WAKEUP,告诉 BPF 这是 blocked donor 的恢复,不是真正 wakeup。
类比
把 mutex owner 想成自习室里占座的学霸,waiter 想成蹭座的同伴。正常情况下学霸在线,同伴可以通过 proxy execution 让学霸替自己顶一下。
- sleeping owner 处理就像:学霸睡着了(owner sleeping),管理员不能再让他继续顶座,于是把同伴从代理链上摘下来,登记到学霸的"等候名单"(
blocked_head)里。学霸一旦被叫醒(try_to_wake_up()),管理员一次性把名单上的同伴全叫回去继续蹭座(activate_blocked_waiters())。如果名单上的某个同伴已经离开自习室了(on_rq==1或on_cpu==1),就跳过,避免把人家赶回已经不属于他的座位。 - try_steal_cookie 跳过 src->donor就像:从别人嘴里抢走他正在咬的苹果(donor 才是当前调度上下文),结果下一个进来的人会以为苹果还挂在树上。
- cookie 不匹配回退就像:core scheduling 已经按"宿舍楼栋规则"选好了谁能进自习室,结果 proxy 想塞一个不属于这栋楼的 owner 进来,被门卫拦下、改派 idle。
Highlight:风险与注意点
- Peter 在 patch 8/9 上指出:
task_struct::sleeping_owner标了__guarded_by(&blocked_lock),但实际上是受 owner 的blocked_lock保护而非自身的,标注与实现不完全匹配,clang 也没报警;lockdep 注释与现实之间存在不一致。 - Peter 进一步质疑
activate_blocked_waiters()的双重调用:sched_ttwu_pending()与try_to_wake_up()各调一次,但sched_ttwu_pending()的 llist 项本身也会走 ttwu 路径,需要确认是否会引起重复激活。 - Atul Kumar Pant 追问
do_activate_blocked_waiter()中间p是否可能已经"finished running"导致task_is_blocked()假阴性;当前依赖pi_lock+__state/on_rq/on_cpu三连检查兜底。 - K Prateek 提出
nr_iowait计数归属差异:proxy 迁移后的 donor 阻塞在 owner rq 上时,加减计数发生在 owner cpu 而不是 donor 原始 cpu;Maria Yu 也报告过此前的nr_iowait不平衡,需要后续观察。 is_dl_boosted()在CONFIG_RT_MUTEXES下原判定不够准确,K Prateek 提了一个替代 patch,把"proxy 中 + donor != curr"作为 boosted 判定,并去掉dl_task_of(dl_se) == rq->donor这层检查;该替代 patch 已经在其 tip 上测试无崩溃。- 链式迁移复用
se.group_node:依赖p已经 deactivate、balancer 看不到,且全程持有rq_lock,使用前提比较严格,新增调用点时需小心。 ENQUEUE_PROXY标志只在 sched_ext 路径显式消费;如果未来 RT/RR/DL 等其它调度类引入自定义 enqueue hook,需要同步识别,避免把 blocked donor 激活误判为 wakeup。- Peter 在 commit message 中明确说 patch 8/9 的"locking 非常棘手",欢迎社区给出简化建议;后续 review 可能改写其中某些串行化路径。
版本变化
- v31(本次 RESEND):合并 Christian 的
yield_task_dl早退;K Prateek 的 sleeping owner 唤醒竞态修复(owner 与 waiter enqueue 之间的并行唤醒导致 waiter 卡住);Maria/Tengfei 的nr_iowait失衡修复(do_activate_blocked_waiter()中调整加减时机);Andrea 的ENQUEUE_PROXY适配。 - 整体系列:Andrea 的 sched_ext 与 proxy-exec 适配、K Prateek 的 core-sched 加固、Suleiman 的 proxy-enabled futex 第一版、Håkon Bugge 的
test-ww_mutex更新都已合入完整分支;proxy-rwsems 修复了一个细微的rwsem_mark_wake()race。 - v30 之前:曾用独立
migration_node,后来改为复用se.group_node(K Prateek 建议);v22 把节点移到CONFIG_SCHED_PROXY_EXEC下;v18 在init_task中初始化migration_node(Suleiman 建议)。 - v7/v6:把单链表重构成按 owner 分层的树形链表,唤醒时只激活目标 owner 的后代,避免误唤醒兄弟分支。
- v5:从更大的 proxy 大 patch 中拆出 sleeping owner 处理逻辑。
一句话总结
v31 把 Proxy Execution 的"Sleeping Owner"机制补齐:owner 沉睡时把 waiter 挂入 owner 的 blocked_head,owner 醒来后一次性整链激活;并附带 DL yield 早退、core-sched cookie 错配回退、donor 偷取防护、整链迁移、sched_ext ENQUEUE_PROXY 等邻居修复,目前 v31 中若干 patch 已被 tip-bot 推入 tip:sched/core。