0/23 已展开

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}.ckernel/sched/{sched.h,stats.h}kernel/locking/mutex.cinclude/linux/sched.hinit/init_task.ckernel/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 激活都走重路径

新流程

  1. union 化状态:把 p->on_rq 与新加的 p->is_linked 合为 p->needs_rq_sync,让 try_to_wake_up() 在锁外原子读二者的并集。
  2. 挂链时复查:proxy_enqueue_on_owner() 先在 owner->blocked_lock 下做 list_add + smp_mb(),再读 owner->on_rq;owner 已醒就自己脱链重试。
  3. 单 CPU 整链:SLEEP | MIGRATING 阶段把 donor 迁到 p->blocked_cpu,确保整条 blocked_head 属于同一 CPU。
  4. 批量链唤醒: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_MIGRATING flag 取代 task_on_rq_migrating() 检查
  • 03/16:update_load_avg() 走 ENQUEUE flag 决定 DO_ATTACH,修复 PELT 漏算(Fixes b049b81bdff6)
  • 04/16:find_proxy_task() 无 owner 时主动 wake_up_process blocked donor(Fixes f13beb010e4a)
  • 05/16:delayed owner 直接按 sleeping owner 处理
  • 06/16:引入 blocked_head/blocked_node/sleeping_ownerproxy_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)