0/27 已展开

LLM 分析

sched, steal_governor:基于 steal time 的 preferred CPU 与 vCPU 动态回缩

系列概况

  • 标题:[PATCH v10 00/12] sched, steal_governor: Introduce preferred CPUs and steal-driven vCPU backoff
  • 作者:Shrikanth Hegde sshegde@linux.ibm.com(IBM)
  • 版本:v10(此前已迭代 v1–v9;本版基于 next-20260812,base commit f2c2ba7219e5)
  • 规模:12 个 patch,覆盖核心调度器、cpumask 基础设施、sysfs ABI、文档,以及一个全新的 drivers/virt/steal_governor.c 模块
  • 修改文件:kernel/sched/{core,fair,debug}.c、kernel/sched/sched.h、kernel/cpu.c、kernel/Kconfig.preempt、include/linux/{cpumask,kernel_stat,sched}.h、drivers/base/cpu.c、fs/proc/uptime.c、arch/s390/kernel/hiperdispatch.c、drivers/virt/{Kconfig,Makefile,steal_governor.c}、Documentation/scheduler/sched-paravirt.rst、Documentation/driver-api/steal-governor.rst、Documentation/ABI/testing/sysfs-devices-system-cpu、MAINTAINERS
  • 代码统计:约 +390 行(drivers/virt/steal_governor.c ~290 行 + 调度器核心 ~100 行 + 文档/ABI/header)
  • Message-ID20260812054033.95658-1-sshegde@linux.ibm.com
  • 完整性:完整线程;包含 cover letter、12 个 patch,以及 Yury Norov、Mete Durlu、Ionut Nechita、Dietmar Eggemann 多轮 review

补丁目的

针对 paravirt 重载场景(PowerVM SPLPAR、KVM、Xen 等)下多 VM 同时高负载导致的 pCPU 竞争与 vCPU 抢占问题,传统手段(CPU hotplug、isolated cpuset、显式 affinity)成本高且会破坏 userspace affinity 契约。

本系列的目标:

  1. 在调度器侧引入一个新的 CPU 状态 preferred(cpu_preferred_mask ⊆ cpu_active_mask),区分"可安全使用"与"仍在 active 但建议折叠"的 CPU。
  2. 在 wakeup、tick push、load balance 三处利用该 hint,让任务优先落在 preferred CPU 上,并主动把任务从非 preferred CPU 上推走。
  3. 在 virt 子系统新增可选模块 steal_governor(CONFIG_STEAL_GOVERNOR=m),按 steal time 高低阈值动态放大或缩小 preferred mask。

强约束:严格不破坏用户 affinity;pin 在 non-preferred 的任务不会被强制迁移;driver 是 policy 层、scheduler 是 mechanism 层,二者解耦。

旧流程的问题

  • vCPU 抢占代价高:hypervisor 在 pCPU 紧张时抢占一个 vCPU,可能打断锁持有者或关中断区,整体 forward progress 退化。
  • CPU hotplug / isolated cpuset 太重:要 topology rebuild,破坏 userspace affinity;显式 affinity 又难管理。
  • 没有一个对用户透明、动态、可在 kernel 内完成的"voluntary folding"机制。

新流程

                    steal time samples
                            |
                            v
 +------------------------------+
               | steal_governor_loop        | delayed work, default1s
               |   ratio > high / <= low      |
               +------------------------------+
                  |                     |
        ratio>high|                     |ratio<=low
                  v                     v
     +----------------------+   +-----------------------+
 | decrease_preferred_  |   | increase_preferred_   |
     | cpus()               |   | cpus()                |
     +----------------------+   +-----------------------+
                  |                     |
                  v                     v
          set_cpu_preferred(cpu, false)  set_cpu_preferred(cpu, true)
                            |
                            v
 cpu_preferred_mask updated
                            |
       +--------------------+--------------------+
       |                    |                    |
       v                    v                    v is_cpu_allowed sched_tick push    sched_balance_rq span
   (wakeup path)      (stopper thread)   (span &= pref mask)

三条路径都用 cpu_preferred(cpu) 做 hint,并保留 select_fallback_rq() 兜底,保证 affinity 契约不被破坏。

Patch 概览

#patch关键改动
01sched/cputime: kcpustat_field_total helper抽取 cpumask 内 cpustat 求和 helper;改 s390 hiperdispatch、fs/proc/uptime
02sched/docs: sched-paravirt.rst新增 paravirt 子系统文档,描述 preferred 概念与设计契约
03cpumask: Introduce cpu_preferred_maskCONFIG_PREFERRED_CPU + __cpu_preferred_mask + cpu_preferred()/set_cpu_preferred(),CPU activate/deactivate 时同步 preferred
04sysfs: /sys/devices/system/cpu/preferredsysfs 只读 cpulist,供用户/irqbalance 使用
05sched/core: is_cpu_allowed 用 preferred任务 wakeup 时优先选 preferred;O(N) 主路径,O(N²) 仅对 pin 在纯 non-preferred 的罕见任务
06sched/fair: LB 仅在 preferred 间sched_balance_rq span 改为 sched_domain ∩ cpu_preferred_mask;sched_balance_newidle 用 cpu_preferred() bail
07sched/core: tick 推走 non-preferred 上的任务stopper thread + select_fallback_rq;rq->push_task_work_done 防重入
08sched/debug: non-preferred 迁移统计新增 nr_migrations_cpu_non_preferred 计数
09virt: Introduce steal_governor新建 drivers/virt/steal_governor.c + 文档 + MAINTAINERS 条目
10virt/steal_governor: 三个 module 参数interval_ms / high_threshold / low_threshold,带范围校验
11virt/steal_governor: policy loopget_system_steal_time、steal_ratio、decrease/increase_preferred_cpus、preferred_cpus_valid 自检
12virt/steal_governor: Kconfig / MakefileCONFIG_STEAL_GOVERNOR tristate default m,select PREFERRED_CPU

关键实现

1. preferred CPU 基础设施(patch 03)

#ifdef CONFIG_PREFERRED_CPU
extern struct cpumask __cpu_preferred_mask;
#else
#define __cpu_preferred_mask __cpu_active_mask
#endif

#define cpu_preferred_mask ((const struct cpumask *)&__cpu_preferred_mask)

static __always_inline bool cpu_preferred(unsigned int cpu)
{
    return cpumask_test_cpu(cpu, cpu_preferred_mask);
}

不变量:preferred ⊆ active ⊆ online ⊆ present ⊆ possible。CONFIG_PREFERRED_CPU=n 时 preferred 退化为 active,set_cpu_preferred 是 nop;driver 不加载时完全无开销。

2. wakeup 路径(patch 05)

static inline bool task_can_sched_on_preferred(int cpu, struct task_struct *p)
{
    if (cpu_preferred(cpu))
        return false;

    /* Only FAIR tasks honor preferred CPU state */
    if (unlikely(p->sched_class != &fair_sched_class))
        return false;

    return cpumask_intersects(p->cpus_ptr, cpu_preferred_mask);
}

is_cpu_allowed() 在用户任务和 unbound kthread 上调用:只要任务还有 preferred CPU 可用,就把当前 non-preferred 标为不可用,让 select_fallback_rq() 选 preferred。

3. tick push(patch 07)

sched_tick()!cpu_preferred(cpu) 时调用 sched_push_current_non_preferred_cpu(rq);后者用 stop_one_cpu_nowait 起 stopper thread,调 __migrate_task() 把任务搬到 select_fallback_rq 选出的 preferred。rq->push_task_work_done 防止重入;首次 rq->curr 检测时 task_can_sched_on_preferred() 仍允许跳过 pin 在纯 non-preferred 的任务。

4. load balance(patch 06)

cpumask_and(cpus, sched_domain_span(sd), cpu_preferred_mask);
...
/* NEWIDLE: bail if this CPU is non-preferred */
if (!cpu_preferred(this_cpu))
    return;

让 LB 与 tick push 朝同一方向,避免 LB 把任务拉回 non-preferred 又被 tick 推回去。

5. steal_governor policy loop(patch 11)

delta_steal = curr_steal - sg_ctx.steal; /* monotonic */
delta_ns    = ktime_to_ns(ktime_sub(now, sg_ctx.time));
delta_ns    = max_t(u64, div_u64(delta_ns * num_possible_cpus(), 10000), 1);
steal_ratio = div64_u64(delta_steal, delta_ns); /* unit: 0.01% */

if (steal_ratio > sg_ctx.high_threshold)
 decrease_preferred_cpus();
else if (steal_ratio <= sg_ctx.low_threshold)
    increase_preferred_cpus();

if (!preferred_cpus_valid())
    restore_preferred_to_active();

decrease/increase 按 core(topology_sibling_cpumask)粒度操作,保证 SMT 兄弟线程同步;decrease 还保护第一个 housekeeping core 永远为 preferred。preferred_cpus_valid() 自检发现 preferred 空或不再是 active 子集时,自动 restore 并停 work。

类比

把这一套机制想成 酒店宴会厅的桌号管理

  • 客人(vCPU 任务)来到宴会厅想坐下。

  • 服务员(scheduler)手里有两份名单:active 是"今天营业的桌子"(满厅),preferred 是"现在还能上菜的桌子"(其中一部分)。

  • 经理(steal_governor)盯着厨房出菜速度(steal time):

    • 出菜变慢(steal 超过 high_threshold)→ 经理把最远那张桌从 preferred 名单里划掉(policy 缩),并让 waiter 把还坐在那张桌的客人礼貌地请到 preferred 桌(tick push)。
    • 出菜恢复(steal 低于 low_threshold)→ 经理把刚才那张桌重新加回 preferred,客人自然回归。

桌号调整是"软"的:如果某位客人是 VIP 写明要坐那张桌(affinity),服务员不会硬拉,他继续在那张桌吃饭,宴会照常进行——只是他那桌暂时不出新菜。这正对应 preferred ⊆ active 和"不破坏 affinity 契约"的设计。

再具体一点:preferred mask 就是"现在可以坐的桌子清单"。wakeup 是"问一句还坐得下",tick push 是"巡场把跑错桌的客人请回",load balance 是"不要再把新客人往那张桌引"。三者同时动作,桌号才能稳定收敛。

Highlight:风险与注意点

  1. x86/KVM/Xen 上默认阈值被稀释。QEMU -smp 4,maxcpus=32 会让 num_possible_cpus() 远大于 num_online_cpus()steal_ratio 分母放大 8×,默认 5%/2% 实际成 40%/16%,driver 进入"什么都不做"区间。Ionut 建议 init 时按 possible/online 比打 warning,或自动按 online 缩放,v11 已规划。
  2. Xen dom0 加载会拖垮整台 host。dom0 的 blkback/netback 服务所有 guest,shrink dom0 preferred 等于让 host 自我降速,建议加 xen_initial_domain() 拒绝加载或 loud warning,并在文档声明"driver 只面向 guest"。
  3. 32-bit 任务在 ARM64 上可能误判(Dietmar 指出)。cpus_ptr 可能与 64-bit-only preferred 重叠,is_cpu_allowed() 返回 false;同时 64-bit CPU 又被 task_allowed_on_cpu() 拒掉,所有 CPU 全拒,select_fallback_rq 陷入死循环。需要在 intersection 检查里叠加 task_cpu_possible_mask()
  4. core 粒度在 KVM/Xen 退化为单 vCPUtopology_sibling_cpumask() 在这些平台等于自身,每轮只动一颗 vCPU,policy 收敛变慢。需要在文档说明 core 步进依赖 guest 拓扑反映 host 调度粒度。
  5. module 参数 init 后只读。阈值不匹配现场 VM 配置时只能 rmmod/modprobe 重载;CONFIG_STEAL_GOVERNOR=y 时失去灵活性,所以推荐 m。
  6. cooperative 假设:若只有部分 VM 启用,禁用 driver 的 VM 会"白占"更多 pCPU;doc 要求 all VMs opt-in。
  7. CPU hotplug 边界:preferred mask 可能因 offline CPU 变空;preferred_cpus_valid() 自检发现后自动 restore 并停 work。
  8. schbench 性能数据退化:PowerVM 上 hackbench 高负载提升 10–44%,但 schbench 部分配置几乎不变甚至略退化;Yury 要求 v11 补充解读。
  9. s390 计划另写 governor(Mete):后续 s390 可能引入自己的 governor 模块,希望未来有 cpuidle 式框架挂多 driver;Shrikanth 暂保持单文件形态,留待后续演化。

版本变化(v9 → v10)

  • 新增 kcpustat_field_total() helper(Yury 建议)。
  • 设计检查改为无条件执行,避免把 driver 约束塞进核心 hotplug 路径。
  • 去掉 idle balance 中的 cpu_preferred 检查,让 nohz.next_balance 自然更新。
  • 推迟 find_new_ilb() 修改(普通场景用不到)。
  • 把 scheduler 文档迁到独立 sched-paravirt.rst(Yury)。
  • 文档补充"默认值不一定适合所有配置"的限制(Yury)。
  • task_can_sched_on_preferred 从 sched.h 移到 core.c(Mete)。
  • 补全多处 Suggested-by tag;统一 polish changelog。
  • 顺手修复 hd_calculate_steal_percentage 中遗留未使用的 cpu 局部变量。

一句话总结

通过在 scheduler 引入 cpu_preferred_mask 提示,并在 virt/ 下新增 steal_governor 模块按 steal time 阈值动态折叠,本系列让 paravirt guest 在高竞争时主动收敛到 preferred CPU 子集以缓解 vCPU 抢占,同时严格保留用户 affinity 契约。