0/286 已展开

LLM 分析

Linux 7.1.9-rc1 stable review:sched_ext / sched-psi / rqspinlock 多个 scheduler 补丁的合订

系列概况

  • 标题[PATCH 7.1 000/271] 7.1.9-rc1 review
  • 作者:Greg Kroah-Hartman(gregkh@linuxfoundation.org),stable 维护者
  • 目标内核:Linux 7.1 stable 分支,候选版本 7.1.9-rc1
  • 规模:本系列共 271封邮件,作为 [PATCH 7.1 000/271] 的回复一次性发出;thread 末尾另有 13 封 Tested-by/Ack 类回复,总计286 封
  • 修改文件:横跨众多子系统(sched、sched_ext、psi、scsi、net、ovpn、nfs、xfs、arm64 dts、drm、usb、alsa、bpf、tracing、ring-buffer、mm …),scheduler 直接相关补丁8 枚
  • 代码统计:难以在邮件正文中获得精确 diffstat,单 patch 体量多在 1–40 行
  • Message-ID(封顶)20260817132536.752504388@linuxfoundation.org
  • 完整性:完整,271 枚 patch + 13 封 acked-by/tested-by 回复均已收录

本 thread 来自 stable 频道(Greg KH 的稳定分支公告),虽标记 sched,但实质是覆盖整个内核的稳定版本合订,并非纯 scheduler 设计讨论;下文聚焦 scheduler 子树,并简要概述其他 patch 的多样性。

补丁目的

7.1.9-rc1 是一次例行的 stable 回滚聚合,覆盖三类 scheduler 相关问题:

  1. sched_ext 子树(patch 013/014/015/022/260):修复 cgroup sub-sched enable/disable 在 race / 错误状态下的拒绝逻辑与生命周期,让非-ext任务(SCX_OPS_SWITCH_PARTIAL 下可能存在)不会被错误地调用 ops.enable(),并修正 disallow 标志位在非 enable 路径上的策略,避免 silently-rewritten policy。
  2. sched/psi(patch 258/259):把 psimon kthread 的创建移出 cgroup_mutex 临界区,并保证 rtpoll_timer 在 cgroup 销毁时被及时关闭,避免 uaf / lockdep误报。
  3. rqspinlock(patch 115):在 deadlock-保留队列场景下重置 tail,避免后续 queue waiter 永远卡死。

旧流程的问题

  • ops.init_task() 在 root enable 之外(例如 sub-sched re-init)通过 disallow 强行把 SCHED_EXT 改回 SCHED_NORMAL:没有 reader 真的去读这个 policy,结果任务继续在 ext class 上以"已被重写"的策略运行,nr_rejected 计数与实际行为不一致。
  • sub-sched 在 scx_link_sched() 之前 enable失败 → scx_sub_disable() 仍执行 task-walk/cgroup复位,与 root disable 的 drain_descendants() 失序,出现 WARN_ON(!scx_task_on_sched) 与 re-home 出错任务的 UAF。
  • numa_select_cpu() 在 task affinity 迁移期间可能选择 task_cpu 落在 cpus_ptr 之外,导致 scx_pick_*_cpu_node() 返回 -EBUSY,self-test 退出。
  • psi psimon kthread 在 cgroup_mutex 下创建,可能与 cgroup 文件系统交互形成循环依赖;rtpoll_timer 在 cgroup 析构时未关闭,留下 dangling timer。
  • rqspinlock 在 deadlock 检测后保留队列但未重置 tail,后续 waiter 在尾节点上 spin永远等不到 owner。

新流程

+----------------------------------+ +----------------------------+
| ops.init_task() == fork branch |       | ops.init_task() == re-init |
|   -> scx_error("...during fork") |       |   -> SCX_ENABLING only, |
+----------------------------------+       |      otherwise scx_error   |
                                          +----------------------------+

 scx_sub_disable(sch):
 if list_empty(&sch->sibling)        /* never linked, skip walk */
          goto dump;
      /* cgroup reset + task walk + membership WARN stay valid */

  scx_enable_task(parent, p): /* sub enable loop */
      if (p->sched_class == &ext_sched_class)
          scx_enable_task(parent, p);     /* non-ext stays READY */

Patch 概览

#Subsystem关键变更
013sched_extdisallow 仅在 SCX_ENABLING 路径上接受,文档中错误的 debugfs 路径修正
014sched_extscx_sub_disable()sibling 为空的 sch 直接走 dump标签
015sched_extsub enable/disable 循环中 scx_enable_task 加 class判定
022selftests/sched_extnuma_select_cpu 在 affinity 变化时回退 prev_cpu
115rqspinlockdeadlock-保留路径重置 tail
258sched/psipsi_cgroup_free() 关掉 rtpoll_timer
259sched/psipsimon kthread 移到 cgroup_mutex 外创建
260sched_extscx_cgroup_lock() 先拿 cgroup_lock(),再 percpu_down_write(&scx_fork_rwsem)

关键实现

/* patch 013:仅在 enable 路径下允许 init_task() 拒绝 SCX */
} else if (unlikely(scx_enable_state() != SCX_ENABLING)) {
    scx_error(sch,
        "ops.init_task() set task->scx.disallow for %s[%d %s outside the enable path",
        p->comm, p->pid);
} else if (unlikely(fork)) {
    scx_error(sch,
        "ops.init_task() set task->scx.disallow for %s[%d] during fork",
        p->comm, p->pid);
}
/* patch 014:从未链入的 sub-sched跳过 walk */
if (list_empty(&sch->sibling))
 goto dump;
/* ... cgroup reset + task walk ...
 * By the time control reaches here, all linked descendant
 * schedulers should have been disabled.
 */
/* patch 260:锁顺序——cgroup_lock 在外,fork_rwsem 在内 */
void scx_cgroup_lock(struct cgroup_subsys_state *css_css)
{
    percpu_down_write(&scx_fork_rwsem);
    cgroup_lock(); /* 反过来即 patch 260 的修法 */
}

ASCII流程图

   userland kernel data path
   -------- ------                                ---------
     | | |
     |  OVPN_CMD_PEER_SET (REMOTE_IP)      |                                       |
     |------------------------------------>|                                       |
     |                                     |  ovpn_peer_reset_sockaddr()          |
     |                                     |  ovpn_peer_hash_vpn_ip()  [old]      |
     |                                     |  ovpn_peer_hash_transp_addr() [new]  |
     |                                     |-------------------------------------->|
     |                                     |         bind->remote (IPv6 scope_id)|
     |                                     |                                       |
     |                                     |  ovpn_peer_get_by_transp_addr()       |
     |                                     |<-------------------------------------| RX
     |                                     |  key: family + addr + port (no scope) |
     |                                     | -> lookup bucket |
     |                                     |                                       |
     |     __ovpn_peer_hash_transp_addr |                                       |
     |     hlist_nulls_del_init_rcu()      |                                       |
     |     hlist_nulls_add_head_rcu()      |-------------------------------------->|
     |                                     |         bucket == lookup bucket OK |
              cgroup hierarchy
              +-----------+
              |   root    |
              +-----+-----+
                    |
        +-----------+-----------+
        |                       |
   +----+-----+           +-----+----+
   |  sub-A   |           |  sub-B   |
   | linked |           | never |
   | (sibling)|           | linked   |
   +----+-----+           +-----+----+
        |                       |
   drain_descendants       scx_sub_disable()
   waits for sub-B sibling empty -> dump (no walk, no re-home)
       ops.init_task() state machine
       ---------------------------- fork = true ? --yes--> scx_error("during fork")
                |
                no |
        scx_enable_state() == SCX_ENABLING ? --no--> scx_error("outside enable path")
                |
 yes
                |
        accept disallow, revert policy to SCHED_NORMAL

类比

可以把 sched_ext 想象成一家俱乐部门口的"VIP通道"。ops.init_task() 就像门口的安检员,他有权把某个客人(task)从 VIP 名单里划掉(设置 disallow)。

  • 旧逻辑:安检员不管是谁来了都能改名单(fork路径、sub-sched 重新初始化路径都允许),结果名单被改了,但内部排班系统没有读这个变更,VIP 客人继续进场。
  • patch 013/014/015 相当于:只有"营业开始前的早班"或"已上锁的会议室"才能动名单;其他时段改名单,安检员必须上报;而且只是"挂名"但还没真正上线的 VIP 包厢,重新分配时不真的去清场。
  • patch 022(NUMA 测试)相当于:客人睡着了被挪到别的楼层,醒来时系统允许安检员按"上次房间"继续处理,而不是粗暴拒绝。
  • rqspinlock patch 115 类似停车场闸机检测到死锁后保留队列,但需要把"队尾指针"清零,否则下一辆车永远以为前面还有人。

Highlight:风险与注意点

  1. scx_cgroup_lock 锁顺序变更(patch 260):从 cgroup_lock() → percpu_down_write() 改成相反顺序,会与已经按旧顺序拿锁的代码路径产生锁顺序反转;升级前应确认没有调用方在持 cgroup_lock 的同时反向触发 scx_cgroup_lock,否则会触发 lockdep splat。
  2. sched_ext 拒绝路径(patch 013):旧代码把 task 改回 SCHED_NORMAL 后不报错,新代码会触发 scx_error() 调度器自杀,BPF 子调度器作者需要重新审视 .init_task 实现,否则可能在新内核上"突然挂掉"。
  3. numa test(patch 022):判定 task_cpu 是否在 cpus_ptr 之外依赖的是"affinity 改变且未重新选 runqueue"的窗口,对超低延迟负载可能仍存在 -EBUSY,需要持续观察。
  4. psi psimon 移出 cgroup_mutex(patch 259):kthread 创建路径变更后,cgroup 卸载路径若还在等 psimon,可能出现新的依赖关系,需要重新跑 cgroup stress test。
  5. stable 回滚风险:本系列 271 枚 patch 中部分来自多版本 queued fixes(如 sched_ext 一连串),与其他 stable patch 叠加可能造成 bisect 跳点;建议在测试报告中按子系统分块标注是否覆盖。
  6. ovpn 相关 patch(025–034):不在 scheduler 子树,但 thread 内大量修补了 netns/sk_user_data/RCU 与哈希 bucket 同步问题,可与 scheduler 的 RCU 使用模式横向参考。

版本变化

7.1.9-rc1 是 7.1 stable 分支的一个累积 release,无独立 vN→vN+1 的拆分;不过 sched_ext 相关 patch 在主线已经过若干轮迭代:

  • 013 在上游 commit 477869b...,属 root-vs-sub启停生命周期 bug 的最后一公里。
  • 014/015 来自同一 commit group(8c13364d.../5cdc9285...),先修 sibling sentinel,再修 task-class 判定。
  • 022 上游 d4a00d61...,针对 selftests/numa。
  • 115 rqspinlock 单独 patch,不依赖前面 patch。
  • 258/259/260 是 psi/Sched_ext 与 cgroup_lock 联动的稳定版本。

一句话总结

7.1.9-rc1 是 Greg KH 发布的 stable 合订,scheduler 子树里集中加固了 sched_ext 在 sub-sched 启停、disallow 拒绝、cgroup 锁顺序上的正确性,并修复了 psi kthread 创建点与 rqspinlock deadlock 路径,是7.1 长期支持用户必须评估的稳定基线。