sched discussion
[RFC PATCH 00/16][PoC] sched/core: Alternate approach to sleeping-owner handling in PROXY_EXEC
LLM 分析
PROXY_EXEC 中 sleeping-owner 的另一种链式唤醒思路
系列概况
- 标题:
[RFC PATCH 00/16][PoC] sched/core: Alternate approach to sleeping-owner handling in PROXY_EXEC - 作者:K Prateek Nayak(AMD)
- 版本:RFC v1 / PoC,基于 John Stultz 的
proxy-exec-v31-7.2-rc4@18bea15ce9fa - 规模:16 个 patch;thread 共 24 封邮件
- 修改文件:
kernel/sched/{core,fair,deadline}.c、kernel/sched/{sched.h,stats.h}、kernel/locking/mutex.c、include/linux/sched.h、init/init_task.c、kernel/fork.c - 代码统计:累计约 +365 / -44 行
- Message-ID(cover):
20260826062901.2137-1-kprateek.nayak@amd.com - 完整性:完整 RFC;作者明确说明中间构建不可 bisect
补丁目的
PROXY_EXEC 让 mutex 阻塞的 donor 迁到 owner 所在 CPU 上"代理"执行。当 owner 自己睡眠时,所有挂在 blocked_head 上的 donor 形成链等待唤醒。原 John 方案在 owner 唤醒与 donor 入队并发时,必须取 p->blocked_lock 防漏激活新成员。本 RFC 给出替代思路:让 donor 在 ttwu_runnable() 路径尽早脱链,把 chain-wakeup 收敛到单 rq_lock 下批量完成,并对非锁持有者保留 fast-path。
旧流程的问题
- 锁嵌套复杂:
p->pi_lock -> __task_rq_lock -> lock->wait_lock -> p->blocked_lock - 每经过一个 owner 都需重新抢、释放
blocked_lock,锁 bouncing 明显 - delayed task 场景下整链难以收敛到同一 CPU,chain-wakeup 需分别处理 src/dst rq
- 没有"非锁持有者 fast-path",所有 blocked task 激活都走重路径
新流程
- union 化状态:把
p->on_rq与新加的p->is_linked合为p->needs_rq_sync,让try_to_wake_up()在锁外原子读二者的并集。 - 挂链时复查:
proxy_enqueue_on_owner()先在owner->blocked_lock下做list_add+smp_mb(),再读owner->on_rq;owner 已醒就自己脱链重试。 - 单 CPU 整链:SLEEP | MIGRATING 阶段把 donor 迁到
p->blocked_cpu,确保整条blocked_head属于同一 CPU。 - 批量链唤醒:owner 唤醒时把自己与所有 donor 一次性置为
TASK_ON_RQ_MIGRATING、再批量__activate_task();与ttwu_runnable()通过proxy_try_dequeue_from_owner()互斥竞争。
Patch 概览
- 01/16:抽出
__activate_task(),新增activate_blocked_task() - 02/16:用
ENQUEUE/DEQUEUE_MIGRATINGflag 取代task_on_rq_migrating()检查 - 03/16:
update_load_avg()走 ENQUEUE flag 决定DO_ATTACH,修复 PELT 漏算(Fixes b049b81bdff6) - 04/16:
find_proxy_task()无 owner 时主动wake_up_processblocked donor(Fixes f13beb010e4a) - 05/16:delayed owner 直接按 sleeping owner 处理
- 06/16:引入
blocked_head/blocked_node/sleeping_owner与proxy_enqueue_on_owner() - 07/16:挂链时强制 donor 完全 block(带
TASK_WAKING) - 08/16:deadline 适配
ENQUEUE/DEQUEUE_MIGRATING语义 - 09/16:记录
p->blocked_cpu,把整链定位到单一 rq - 10/16:引入
p->is_linked - 11/16:union 化为
p->needs_rq_sync - 12/16:加
sched_migrated_on_blocking与 MIGRATING flag - 13/16:wakeup 路径走
proxy_try_dequeue_from_owner()提前脱链 - 14/16:chain-wakeup:整链置
MIGRATING后批量__activate_task() - 15/16:mutex 引入
p->lock_nesting计数 - 16/16:
lock_nesting == 0时 activate/block 走 fast-path
关键实现
static void proxy_enqueue_on_owner(struct rq *rq,
struct task_struct *owner,
struct task_struct *p)
{
WRITE_ONCE(p->sleeping_owner, owner);
list_add(&p->blocked_node, &owner->blocked_head);
proxy_resched_idle(rq);
smp_mb();
if (READ_ONCE(owner->on_rq)) {
__proxy_dequeue_from_owner(p); /* owner 已醒,自己脱链 */
return;
}
block_task(rq, p, READ_ONCE(p->__state) | TASK_WAKING);
}
类比
把 sleeping owner 想象成"自习室占座的学长":
- 旧流程像有人想坐学长旁边座位时,要全楼广播"学长还在不在"。状态随时变,容易漏算,锁层级越叠越厚。
- 新流程给学长桌上摆一块"我还在自习"的牌子(
on_rq),再用细绳(is_linked)把等位同学连过来。等位同学醒来只需看牌子 + 拉绳就能决定要不要叫醒学长;学长若已离开,等位同学自己解绳走人,不必惊动整栋楼。
Highlight:风险与注意点
- 不可 bisect:作者明确说明中间构建会失败,所有 patch 必须一起应用
lock_nesting放在task_struct顶层而非#ifdef CONFIG_SCHED_PROXY_EXEC,作者自承是 PoC HACK- delayed task 暂未处理:owner 处于 SCHED_DELAYED 时被 05/16 强行转入 sleeping owner 路径,v2 需要重新设计
- chain-wakeup 把争用集中到单
rq_lock,事件稀有但极端长链可能放大 hot CPU 锁争用 - 与
MUTEX_FLAG_HANDOFF交互待评估:Andrea 建议 unlock 端强制 handoff 以避免 spurious wake,Prateek 担心伤害 optimistic spinning - 14/16 patch 注释里 Part 3 描述不完整,作者承认需补全
- 15/16 mutex 与 16/16 fast-path 应可独立评审,避免 sched 与 locking 改动耦合
版本变化
v1 / PoC。v2 计划:通过 proxy_migrate_task() 在 blocking 后迁移处理 delayed task;完成 lock_nesting 的 #ifdef 收敛;跑基准评估"始终强制 handoff"对乐观自旋的影响。
与其他相关 patch 系列的关联
- 基于 John Stultz 的
proxy-exec-v31系列 - 03/16 修复
b049b81bdff6(blocked-waiter migration)的 PELT 偏差 - 04/16 修复
f13beb010e4a(return-migration for PROXY_WAKING)死角
一句话总结
本 RFC 用 ttwu_runnable() + needs_rq_sync 把 sleeping-owner 链唤醒收敛到单 rq_lock 下批量处理,并依靠 lock_nesting 区分 fast/slow path,是 PROXY_EXEC 长链场景下值得讨论的一次简化尝试。
sleeping owner (blocked)
|
| blocked_head
v
donor1
|
v
donor2
v
...
(same CPU)
proxy_enqueue_on_owner() chain-wakeup
---------------------- --------------
lock(owner->blocked_lock) WRITE on_rq = MIGRATING
list_add(donor, blocked_head) smp_mb()
smp_mb() lock(rq_of(blocked_cpu))
if (owner->on_rq) for_each donor
dequeue & retry WRITE donor->on_rq = MIGRATING
else __activate_task(owner)
block_task(donor) __activate_task(donor...)
ttwu_runnable(donor) <--> proxy_try_dequeue_from_owner()
(loser falls back to __task_rq_lock and retries)