0/37 已展开

LLM 分析

sched/steal_monitor:cpu_preferred_mask 与 steal 驱动的 vCPU 退避

系列概况

  • 标题:[PATCH v7 00/12] sched, steal_monitor: Introduce cpu_preferred_mask and steal-driven vCPU backoff
  • 作者:Shrikanth Hegde sshegde@linux.ibm.com,审阅者 Yury Norov,文档审阅 Randy Dunlap
  • 版本:v7(12 个补丁,base = tip/sched/core 04998aa54848 "sched/eevdf: Delayed dequeue task can't preempt")
  • 规模:21 个文件,+737 / -5;新增 drivers/virt/steal_monitor/{sm_core.c, defaults.c, sm_core.h, Kconfig, Makefile} 与 Documentation/driver-api/steal-monitor.rst
  • Message-ID(首封)20260709215648.1246821-1-sshegde@linux.ibm.com
  • 完整性:37封,含 13 个 patch 与 24 封回复/审阅意见,覆盖 v7 整套 12 个 patch 的全部讨论

补丁目的

在虚拟化超分配场景下,guest 内核只能看到 steal time 这一被动信号,但旧调度器没有任何机制能据此把负载折叠到更小的 vCPU 集合上。本系列引入 cpu_preferred_mask(设计上 preferred ⊆ active ⊆ online ⊆ present ⊆ possible),并配套 drivers/virt/steal_monitor 周期性采样 steal 比例:跨过 high_threshold 就收缩一个核心的 preferred CPU,落到 low_threshold 就再扩张一个核心;调度器侧 wakeup、tick、load balance 全部把任务优先放到 preferred CPU 上,从而降低 vCPU preemption 与锁持有者抢占开销。

旧流程的问题

  • guest 只能"被动接受" hypervisor 的调度,被抢占 vCPU 时只能挨打,无法主动让出。
  • 调度器只认 active_mask,load balance 仍会把任务拉进正在被宿主抢的 vCPU,下一个 tick 又被赶走,徒增迁移。
  • 想解决只能靠 CPU hotplug 或 cpuset 隔离,这两条都会破坏用户亲和性(cpuset 还破坏全局调度视图),不适合做动态微调。
  • irqbalance 等用户态工具无法感知"现在哪几颗 vCPU 还能放心用"。

新流程

   +-----------------------+
   | steal_monitor module  |
   |  load -> delayed_work |
   +----------+------------+
              | every interval_ms
   +----------v------------+
   | compute steal_ratio   |
   | = delta_steal / |
   |   (delta_ns * npcpu   |
   |    / 10000)           |
   +----------+------------+
              |
   +----------v------------+ consecutive hi -> DECREASE
   | prev_dir==DECREASE ?  | -- ratio > high_thr  --> decrease_preferred_cpus()
   | prev_dir==INCREASE ?  | -- ratio <= low_thr  --> increase_preferred_cpus()
   +----------+------------+
              |
   +----------v------------+
   | set_cpu_preferred()   |  (per SMT sibling group)
   +----------+------------+
              |
   +----------v------------+
   | sched wakeup / LB /   |
   | tick stopper use      |
   | cpu_preferred_mask    |
   +-----------------------+

decrease_preferred_cpus() 总是把"受保护 housekeeping 核心"之外最末尾的核心及其 SMT sibling 一起标 non-preferred,保证至少一个 housekeeping core 始终在 preferred 内。

Patch 概览

  • 1/12 Documentation:把 preferred CPU 概念写进 Documentation/scheduler/sched-arch.rst
  • 2/12 cpumask:定义 __cpu_preferred_maskset_cpu_preferred()cpu_preferred()!CONFIG_PREFERRED_CPU 下退化为空操作 + cpu_active()
  • 3/12 sysfs:/sys/devices/system/cpu/preferred 只读 cpulist。
  • 4/12 is_cpu_allowed:新增 task_can_sched_on_preferred(),让 wakeup/select_fallback_rq 倾向 preferred。
  • 5/12 sched/fair:load balance 仅在 preferred CPU 之间进行,IDLE/NEWIDLE 早退。
  • 6/12 sched_tick:在 non-preferred CPU 上用 stopper 把当前任务迁出(新增 push_task_work_done 防重入)。
  • 7/12 schedstat:新增 nr_migrations_cpu_non_preferred,便于验证。
  • 8/12 drivers/virt/steal_monitor:新建模块、Kconfig、Makefile、sm_core.c 入口、MAINTAINERS。
  • 9/12 模块参数:interval_mslow_thresholdhigh_thresholdmodule_param_cb 在加载时校验。
  • 10/12 defaults.c:实现 get_system_steal_time / decrease/increase_preferred_cpus / get_num_cpus_steal_ratio
  • 11/12 周期 work:compute_preferred_cpus_work,连续两次越界才动作,方向记忆防 ping-pong,结束时 WARN_ON_ONCE 检查不变量。
  • 12/12 nohz_full:把 non-preferred nohz_full CPU 的 tick 保留到任务出清(作者宣布 v8 删除)。

关键实现

/* kernel/sched/sched.h */
static inline bool task_can_sched_on_preferred(int cpu, struct task_struct *p)
{
    if (cpu_preferred(cpu))
        return false;
    if (unlikely(p->sched_class != &fair_sched_class))
        return false;
    return cpumask_intersects(p->cpus_ptr, cpu_preferred_mask);
}

/* kernel/sched/core.c */
void sched_push_current_non_preferred_cpu(struct rq *rq)
{
    struct task_struct *push_task = rq->curr;

    if (!task_can_sched_on_preferred(rq->cpu, push_task))
        return;
    if (rq->push_task_work_done)
        return;

    get_task_struct(push_task);
    scoped_guard(rq_lock, rq)
        rq->push_task_work_done = true;

    stop_one_cpu_nowait(rq->cpu, sched_non_preferred_cpu_push_stop,
                        push_task, this_cpu_ptr(&npc_push_task_work));
}

/* drivers/virt/steal_monitor/sm_core.c */
static void compute_preferred_cpus_work(struct work_struct *work)
{
    u64 curr_steal = get_system_steal_time();
    u64 delta_steal = curr_steal > sm_core_ctx.prev_steal ?
                       curr_steal - sm_core_ctx.prev_steal : 0;
    sm_core_ctx.prev_steal = curr_steal;

    u64 delta_ns = ktime_to_ns(ktime_sub(ktime_get(),
                                         sm_core_ctx.prev_time));
    /* avoid overflow: divide denominator first */
    delta_ns = div_u64(delta_ns * get_num_cpus_steal_ratio(), 100 * 100);
    u64 steal_ratio = div64_u64(delta_steal, delta_ns);

    if (sm_core_ctx.prev_direction == SM_DIR_DECREASE &&
        steal_ratio > sm_core_ctx.high_threshold)
        decrease_preferred_cpus(&sm_core_ctx);
    if (sm_core_ctx.prev_direction == SM_DIR_INCREASE &&
        steal_ratio <= sm_core_ctx.low_threshold)
        increase_preferred_cpus(&sm_core_ctx);
    /* ... direction + requeue ... */
}

设计不变量通过 cpumask 自身结构 + set_cpu_preferred(cpu, true) 仅在 sched_cpu_activate 时调用来保证;!CONFIG_PREFERRED_CPU 编译路径上 set_cpu_preferreddo { } while (0)cpu_preferred()cpu_active()

类比

想象 guest是一家共享办公室,整栋楼(possible)有 720 个工位,今天开放的(active)有 720 个。宿主超分配就像走廊突然塞满人(pCPU 抢占),空调不够用了:steal time 是"走廊热度计"。前台(steal_monitor)一旦发现温度超过 high_threshold,就把靠走廊尽头那几排的灯关掉、让大家集中到靠窗的几排(preferred),等人流过了再把灯一排排重新打开。这样既没裁员(没碰 affinity),又没有占着茅坑(没人在被抢占的 vCPU 上空跑)。

Highlight:风险与注意点

  • Invariant 恢复:Yury 指出 WARN_ON_ONCE 只是打印,破坏后必须主动 rmmod 或恢复 cpu_preferred_mask;目前只 printk 风险较大,VM 可能带着空 preferred mask 撑到深夜。
  • Kconfig 依赖CONFIG_PREFERRED_CPU 当前是裸 bool,Yury 要求加 depends on SMP && PARAVIRT,否则可能造成 STEAL_MONITOR=y && PREFERRED_CPU=n 的坏组合。
  • steal 用 possible 而非 active:CPU 下线瞬间要靠 curr > prev 钳位保护,否则会注入伪下降。
  • 跨 VM 协作:单 VM 开启会让自己"吃亏",需要部署文档明确全员开启。
  • 非 FAIR class:RT/DL 不走 preferred 路径,文档/sysfs 需写清。
  • Patch 12 删除:作者确认 v8 移除 nohz_full tick 守卫,pinned 任务与 TICK_DEP_BIT_SCHED 清理难处理。
  • 文档:Randy 触发 docutils 警告,需在 .. SPDX-License-Identifier: GPL-2.0 后加空行。
  • 模块参数只读:阈值/间隔只能 rmmod 后改,部署脚本需注意。
  • Config 位置:Yury 建议把 STEAL_MONITOR Kconfig 移到 series 末尾,避免 bisect 中段坏掉。
  • decrease/increase 状态返回:Yury 建议返回状态并记忆在 struct steal_monitor,作者认为 stateless 已足够;v8 待定。
  • sysfs gating:Yury 主张 /sys/devices/system/cpu/preferredCONFIG_PREFERRED_CPU ifdef 包住,作者已认可在 v8 加上。

版本变化

v6 → v7(已合并):

  • steal_monitor 拆成 4–5 块;新增 CONFIG_STEAL_MONITOR select PREFERRED_CPU
  • 模块参数改 module_param_cb,加载期校验,运行期只读。
  • is_cpu_allowed 简化,删冗余注释。
  • nohz_full tick 守卫拆成独立 patch 12;两个 load balance 补丁合并。
  • steal 计算由 activepossible;移除 __weak
  • scoped_guard 去多余空格;sched_push_current_non_preferred_cpu 去 class 检查。
  • 修复 push_task_work_done 与 early-return 的 requeue;WARN_ONWARN_ON_ONCE
  • MAINTAINERS 增加条目。

预计 v8:

  • 文档:补 steal-monitor.rst 空行。
  • Kconfig:PREFERRED_CPUdepends on SMP && PARAVIRTSTEAL_MONITOR 移至系列末尾。
  • 不变量破坏时主动恢复/卸载驱动。
  • sysfs preferred 文件用 #ifdef CONFIG_PREFERRED_CPU 包起来。
  • 删除 patch 12。
  • 清理 increase/decrease_preferred_cpus 的 unused ctx 形参。

一句话总结

v7 已把 cpu_preferred_mask 的 cpumask/sysfs/sched 接口与 drivers/virt/steal_monitor 周期调参机制完整打通;下一轮 v8 重点是修复 invariant 破坏时的恢复策略、Kconfig 依赖与 patch 12 的去留。