Skip to content

Latest commit

 

History

History
3546 lines (2714 loc) · 144 KB

File metadata and controls

3546 lines (2714 loc) · 144 KB

Linux 内核 EAS 与 AutoNUMA 深度分析

基于 Linux kernel 源码(commit 8a30aeb0d),主要文件:

  • kernel/sched/fair.c(EAS 与 AutoNUMA 核心实现)
  • include/linux/energy_model.h(能量模型数据结构与内联函数)
  • kernel/power/energy_model.c(EM 框架注册与管理)
  • mm/memory.c(NUMA 缺页处理路径)
  • mm/migrate.c(NUMA 内存迁移实现)
  • include/linux/memory-tiers.h(内存分层数据结构)
  • include/linux/sched/topology.h(CPU 容量接口定义)
  • kernel/sched/sched.h(调度器内部数据结构)

目录

  1. 概述与背景
  2. EAS:能效感知调度
  3. AutoNUMA:自动 NUMA 平衡
  4. EAS 与 AutoNUMA 的协同分析
  5. 调试与观测手段
  6. 性能影响与调优建议
  7. 附录:关键函数速查表

1. 概述与背景

EAS(Energy-Aware Scheduling)

EAS 是 Linux 4.14 引入的调度优化机制,目标是在保证性能的前提下,将任务调度到最节能的 CPU 上执行。它对 big.LITTLE 这类异构多核架构(如 ARM Cortex-A78 + Cortex-A55 组合)尤为重要。大核高性能高功耗,小核低性能低功耗,传统的负载均衡算法完全忽略了 CPU 间功耗的差异,导致在轻负载场景下将任务跑在大核上浪费大量能量。

EAS 的核心思路:在任务唤醒时,遍历所有性能域(Performance Domain),利用能量模型(Energy Model,EM)估算将任务放到每个候选 CPU 的额外能耗,选择能耗增量最小的 CPU。这个决策发生在 find_energy_efficient_cpu()kernel/sched/fair.c:8388)中。

AutoNUMA(Automatic NUMA Balancing)

AutoNUMA 是 Linux 3.8 引入的 NUMA 自动平衡机制。在多 NUMA 节点系统(如双路或四路 x86 服务器、多芯片 ARM 服务器)上,进程可能运行在 node0,但其内存分配在 node1,造成高延迟的跨节点内存访问(remote memory access)。根据 NUMA 拓扑,跨节点访问的延迟通常是本地访问的 1.5x 到 3x。

AutoNUMA 通过一种优雅的机制探测访问模式:周期性地将正在运行任务的页表项设置为不可访问(PROT_NONE 即缺页状态),强迫后续访问触发缺页中断,从而统计每个 NUMA 节点上的访问频率,然后将任务或内存迁移到更合适的节点。

                    异构系统中的两大调度优化

  +---------------------------+   +---------------------------+
  |   EAS (能效感知调度)       |   |  AutoNUMA (NUMA 自动平衡)  |
  |                           |   |                           |
  |  目标: 最小化 CPU 能耗    |   |  目标: 最小化 NUMA 内存   |
  |  场景: big.LITTLE, SoC   |   |  访问延迟                 |
  |  维度: 频率 x 功耗        |   |  场景: 多路服务器          |
  |  粒度: 单次任务唤醒       |   |  维度: 内存局部性          |
  |  机制: 能量模型估算       |   |  机制: PROT_NONE 缺页      |
  |                           |   |                           |
  |  主要平台: ARM mobile/SoC |   |  主要平台: x86/ARM server  |
  +---------------------------+   +---------------------------+
             |                                |
             +-------------+  +--------------+
                           v  v
               +----------------------+
               |  kernel/sched/fair.c  |
               |    (核心实现文件,     |
               |     两者均在此实现)   |
               +----------------------+

两者的发展历史

  • Linux 3.8(2013):AutoNUMA 首次合并,基本 PROT_NONE 扫描框架
  • Linux 4.14(2017):EAS 合并,基于能量模型的任务放置
  • Linux 5.2(2019):AutoNUMA 引入 numa_group 聚合优化
  • Linux 5.16(2022):Memory Tiering 支持,AutoNUMA 扩展到慢速内存提升
  • Linux 6.1(2022):EAS 引入 em_perf_table RCU 保护,支持运行时能量模型更新(芯片 binning)
  • Linux 6.6(2023):EAS 的 em_perf_domain 新增 min_perf_state/max_perf_state 支持热限制下的 OPP 范围收窄

2. EAS:能效感知调度

2.1 能量模型框架(Energy Model)

能量模型(EM)是 EAS 的数据基础,定义在 include/linux/energy_model.h。整个框架由三层数据结构构成。

em_perf_state:单个 OPP 点的描述

include/linux/energy_model.h:24-30

struct em_perf_state {
    unsigned long performance;  // CPU 性能容量,归一化到 SCHED_CAPACITY_SCALE (1024)
    unsigned long frequency;    // 该 OPP 的频率,单位 kHz
    unsigned long power;        // 该频率下单颗 CPU 的功耗(uW 或抽象单位)
    unsigned long cost;         // 预计算的能耗系数,热路径中直接使用
    unsigned long flags;        // EM_PERF_STATE_INEFFICIENT 等标志
};

cost 字段的推导来自 include/linux/energy_model.h:282-314 的注释,是 EAS 的关键优化:

   cpu_nrg = ps->power * cpu_util / ps->performance
           = (ps->power * cpu_max_freq / ps->freq / scale_cpu) * cpu_util
           = ps->cost * cpu_util

通过预计算 cost = power * max_freq / freq / scale_cpu(精度放大 10 倍),将热路径中的能耗计算从除法变为乘法,显著降低计算开销。

EM_PERF_STATE_INEFFICIENT(行 40):若某 OPP 的 cost 不低于更高频率 OPP 的 cost,则标记为低效状态——意味着提升频率反而更省能,保持在该频率点没有意义。低效状态可通过 EM_PERF_DOMAIN_SKIP_INEFFICIENCIES 标志在 EAS 计算时跳过。

em_perf_table:性能状态表(RCU 保护)

include/linux/energy_model.h:48-52

struct em_perf_table {
    struct rcu_head rcu;          // RCU 回收头,支持无锁读取
    struct kref kref;             // 引用计数,多用户共享同一表
    struct em_perf_state state[]; // 变长数组:按频率升序排列的 OPP 列表
};

使用 RCU 保护是为了支持运行时动态更新能量模型——例如芯片 binning 特性(根据实测功耗修正出厂 OPP 表)通过 em_dev_update_chip_binning() 触发,不需要停机。

em_perf_domain:性能域

include/linux/energy_model.h:74-83

struct em_perf_domain {
    struct em_perf_table __rcu *em_table; // 当前 OPP 表(RCU 保护,可热更新)
    struct list_head node;         // 链入全局 em_pd_list
    int id;                        // 唯一标识号
    int nr_perf_states;            // OPP 总数
    int min_perf_state;            // 最小允许 OPP 索引(热限制时由 em_update_performance_limits 调整)
    int max_perf_state;            // 最大允许 OPP 索引
    unsigned long flags;           // EM_PERF_DOMAIN_MICROWATTS | EM_PERF_DOMAIN_SKIP_INEFFICIENCIES 等
    unsigned long cpus[];          // 该域包含的 CPU 掩码(变长末尾,节省内存)
};

性能域的概念:共享同一调频策略(CPUfreq policy)的一组 CPU 构成一个性能域。在 big.LITTLE 系统中通常有两个性能域:小核集群和大核集群。所有域内 CPU 的频率同时变化,因此 EM 以域为单位估算能耗。

能量模型三层结构示意图:

  系统级
  +------------------------------------------------------------------+
  |  rd->pd(root domain 的性能域链表)                               |
  |  +-------------------+       +-------------------+               |
  |  |  em_perf_domain   |  ->   |  em_perf_domain   |  -> NULL      |
  |  |  (小核集群 A55)   |       |  (大核集群 A78)   |               |
  |  |  cpus: 0-3        |       |  cpus: 4-7        |               |
  |  +-------------------+       +-------------------+               |
  |          |                           |                           |
  |          v                           v                           |
  |  +-------------------+       +-------------------+               |
  |  |  em_perf_table    |       |  em_perf_table    |               |
  |  |  (RCU 保护)        |       |  (RCU 保护)        |               |
  |  +-------------------+       +-------------------+               |
  |          |                           |                           |
  |          v                           v                           |
  |  state[0]: 500MHz/100mW    state[0]: 1.0GHz/500mW               |
  |  state[1]: 800MHz/200mW    state[1]: 1.8GHz/1.2W                |
  |  state[2]: 1.2GHz/400mW   state[2]: 2.6GHz/2.8W                |
  +------------------------------------------------------------------+

em_cpu_energy():性能域能耗估算内联函数

include/linux/energy_model.h:243-316 是调度热路径频繁调用的核心:

static inline unsigned long em_cpu_energy(struct em_perf_domain *pd,
                unsigned long max_util, unsigned long sum_util,
                unsigned long allowed_cpu_cap)

参数语义:

  • max_util:该性能域中利用率最高的 CPU 的利用率(决定频率请求,模拟 schedutil 的行为)
  • sum_util:该域所有 CPU 利用率之和(决定总能耗分摊)
  • allowed_cpu_cap:因热限制等原因实际可用的最大容量(来自 get_actual_cpu_capacity()

执行步骤(行 264-315):

  1. max_util = min(max_util, allowed_cpu_cap) — 受热限制夹住,避免估算超出实际能力
  2. 调用 em_pd_get_efficient_state() 找到满足 max_util 需求的最低有效 OPP 索引
  3. 返回 ps->cost * sum_util — 整个性能域的能耗估算值

em_pd_get_efficient_state():查找有效 OPP

include/linux/energy_model.h:204-225

static inline int
em_pd_get_efficient_state(struct em_perf_state *table,
                          struct em_perf_domain *pd, unsigned long max_util)
{
    int min_ps = pd->min_perf_state;
    int max_ps = pd->max_perf_state;

    for (i = min_ps; i <= max_ps; i++) {
        ps = &table[i];
        if (ps->performance >= max_util) {
            if (pd_flags & EM_PERF_DOMAIN_SKIP_INEFFICIENCIES &&
                ps->flags & EM_PERF_STATE_INEFFICIENT)
                continue;   // 跳过低效状态
            return i;
        }
    }
    return max_ps;  // 利用率超出最高 OPP,返回最高档
}

min_perf_state 开始向上遍历(低频到高频),找到第一个 performance >= max_util 的有效状态。若启用了 SKIP_INEFFICIENCIES,则自动跳过低效 OPP 继续向上查找。


2.2 OPP 表到能量模型的映射

驱动程序通过 dev_pm_opp 框架提供 OPP 表,能量模型框架将其转换为内部的 em_perf_state 数组。关键路径在 kernel/power/energy_model.cem_init_performance() 函数(约 229-251 行):

fmax = table[nr_states - 1].frequency;       // 最高频率点
max_cap = arch_scale_cpu_capacity(cpu);       // 架构最大容量(1024 或异构值)

for (i = 0; i < nr_states; i++) {
    // performance 与频率成线性比例
    table[i].performance = div64_u64(
        (u64)max_cap * table[i].frequency, fmax);
}

该线性比例假设 CPU 性能与频率成正比——对同质 CPU 这是合理近似,但对具有 IPC(每周期指令数)差异的异构核心则需要通过 capacity-dmips-mhz 设备树属性修正。

OPP 表提供的功耗数据有两种来源:

  1. 设备树 operating-points-v2 节点的 opp-microwatt 属性(精确值)
  2. 驱动程序通过 em_data_callback.active_power() 回调提供(如通过在线功耗测量)
OPP 表 → 能量模型映射流程:

  设备树 OPP 表              em_data_callback
  (频率 + 功耗)              (.active_power 回调)
         |                         |
         +----------+  +-----------+
                    v  v
          em_dev_register_perf_domain()
                    |
                    v
          em_init_performance()
          (计算 performance = max_cap * freq / fmax)
                    |
                    v
          em_compute_costs()
          (计算 cost = power * 10 / performance)
          (标记 EM_PERF_STATE_INEFFICIENT)
                    |
                    v
          em_perf_domain.em_table
          (完成的性能域,注册到 rd->pd 链表)

2.3 em_dev_register_perf_domain 注册流程

include/linux/energy_model.h:175-180 声明了两个注册接口:

int em_dev_register_perf_domain(struct device *dev, unsigned int nr_states,
                                const struct em_data_callback *cb,
                                const cpumask_t *cpus, bool microwatts);

int em_dev_register_pd_no_update(struct device *dev, unsigned int nr_states,
                                 const struct em_data_callback *cb,
                                 const cpumask_t *cpus, bool microwatts);

两者的区别:前者注册后立即重建调度域(触发 EAS 使能检查);后者延迟重建,适合批量注册多个性能域时避免频繁重建开销。

microwatts 参数:若为 true,功耗值以微瓦(µW)为单位,同时设置 EM_PERF_DOMAIN_MICROWATTS 标志;若为 false,则为抽象单位(允许不同功率测量方式)。

注册完成后,性能域通过 perf_domain 封装链接到 root_domain.pd 链表,供 find_energy_efficient_cpu() 遍历。

em_data_callback 结构(include/linux/energy_model.h:124-163)提供了两个可选回调:

  • active_power(dev, power, freq):查询给定频率以上最近 OPP 的功耗
  • get_cost(dev, freq, cost):直接提供 cost 值(适合功耗曲线非线性的设备)

2.4 CPU Capacity 概念与异构系统

CPU capacity(CPU 容量)是 Linux 调度器描述异构 CPU 计算能力的核心抽象,归一化到 SCHED_CAPACITY_SCALE = 1024

整个 capacity 系统有三层值:

CPU Capacity 三层体系:

  arch_scale_cpu_capacity(cpu)        <- 架构静态容量
  (来自设备树 capacity-dmips-mhz 或 ACPI PPTT)
  (big 核: 1024, LITTLE 核: ~400)
            |
            | 减去热压力(hw_load_avg)
            | 减去 cpufreq 压力
            v
  get_actual_cpu_capacity(cpu)        <- 实际可用容量(用于 EAS 估算)
            |
            | 经调度器周期性更新
            v
  cpu_rq(cpu)->cpu_capacity            <- 调度器用容量(capacity_of(cpu))
  (用于负载均衡的 fits_capacity 判断)

arch_scale_cpu_capacity()include/linux/sched/topology.h:201-216):

// 默认实现(对称 CPU 系统)
#ifndef arch_scale_cpu_capacity
static __always_inline
unsigned long arch_scale_cpu_capacity(int cpu)
{
    return SCHED_CAPACITY_SCALE;  // 所有 CPU 返回 1024
}
#endif

ARM 架构会覆盖此函数(arch/arm64/kernel/topology.c),从设备树读取 capacity-dmips-mhz 并归一化,使得异构 CPU 间的容量比例反映真实性能差异。

get_actual_cpu_capacity()kernel/sched/fair.c:4981-4988):

static inline unsigned long get_actual_cpu_capacity(int cpu)
{
    unsigned long capacity = arch_scale_cpu_capacity(cpu);
    capacity -= max(hw_load_avg(cpu_rq(cpu)), cpufreq_get_pressure(cpu));
    return capacity;
}

扣除两种压力来源:

  1. hw_load_avg():硬件层面报告的负载(如 ARM AMU 寄存器,反映 CPU stall 等开销)
  2. cpufreq_get_pressure():调频驱动上报的容量压力(如热限频时报告已损失的容量)

fits_capacity()capacity_greater()kernel/sched/fair.c:104-112):

// 任务利用率是否适配 CPU 容量(留 20% 余量,即 1024/1280 = 0.8 阈值)
#define fits_capacity(cap, max)  ((cap) * 1280 < (max) * 1024)

// cap1 是否比 cap2 显著更大(约 5% 阈值,1078/1024 ≈ 1.053)
#define capacity_greater(cap1, cap2) ((cap1) * 1024 > (cap2) * 1078)

fits_capacity 的 20% 余量(headroom)设计是为了给 CPU 频率响应留时间:当利用率上涨后,schedutil 需要一段时间才能提升频率,这段时间内 CPU 已经超载。20% 余量预留了这个反应时间。


2.5 arch_scale_cpu_capacity 与 arch_scale_freq_capacity

两者共同描述了 CPU 在某时刻的实际计算能力:

capacity_orig = arch_scale_cpu_capacity(cpu)   // 静态架构容量上限

              频率因子: arch_scale_freq_capacity(cpu)
              (= 当前频率 / 最大频率,范围 0-1024)
              * capacity_orig
              = 当前频率对应的实际容量

arch_scale_freq_capacity()include/linux/arch_topology.h 中定义(ARM 实现读取 AMU 频率计数器或 CPUfreq 信息),代表频率不变性(frequency invariance)校正因子。频率不变性是 EAS 的前提条件之一:只有当 PELT 利用率已经被频率校正,EAS 的能耗估算才准确。

在 big.LITTLE 系统上的典型数值示例:

ARM Cortex-A78 + A55(假设值):

  CPU 类型    arch_scale_cpu_capacity   当前频率   arch_scale_freq_capacity
  A55-0       410                       600MHz/1.2GHz  341
  A55-1       410                       1.2GHz/1.2GHz  1024
  A78-4       1024                      1.0GHz/2.6GHz  394
  A78-5       1024                      2.6GHz/2.6GHz  1024

2.6 EAS 启用条件:sched_energy_enabled

EAS 的开关是一个静态 key:DECLARE_STATIC_KEY_FALSE(sched_energy_present)kernel/sched/sched.h:3700)。

// kernel/sched/sched.h:3702-3705
static inline bool sched_energy_enabled(void)
{
    return static_branch_unlikely(&sched_energy_present);
}

使用 static_branch_unlikely(而非 static_branch_likely)表示设计上认为 EAS 是特殊情况(仅限异构系统),普通对称多核 x86 系统默认不启用。

EAS 启用需要同时满足的条件(在 kernel/sched/topology.csched_energy_setup_pd() 和相关检查中验证):

EAS 启用条件检查流程:

  rebuild_sched_domains()
       |
       v
  sched_energy_setup_pd()  [为每个 root domain 配置 perf_domain]
       |
       v
  检查 1: CONFIG_ENERGY_MODEL && CONFIG_CPU_FREQ_GOV_SCHEDUTIL
         -- 编译时条件,缺一不可
       |
       v
  检查 2: rd->pd != NULL
         -- 至少有一个性能域注册了 EM
       |
       v
  检查 3: sched_domain 层次存在 SD_ASYM_CPUCAPACITY 标志
         -- 系统有异构容量(big.LITTLE)
       |
       v
  检查 4: cpufreq_ready_for_eas()
         -- 所有在线 CPU 的 cpufreq 驱动为 schedutil
         -- 且 arch_scale_freq_invariant() == true
       |
       v
  检查 5: !sched_smt_active()
         -- 未启用 SMT(超线程会破坏 EAS 的性能域假设)
       |
       v
  全部满足 -> static_branch_enable_cpuslocked(&sched_energy_present)

用户层控制:/proc/sys/kernel/sched_energy_aware(默认值 1)可以临时关闭 EAS,对应变量 sysctl_sched_energy_aware


2.7 schedutil governor 与 EAS 协作

EAS 与 schedutil 的协作是设计上的深度耦合,不是偶然的。两者共享同一套利用率信号(PELT,Per-Entity Load Tracking):

PELT 利用率信号的双重使用:

  调度器 tick / 唤醒事件
          |
          v
  update_load_avg()
  (更新 se.util_avg, se.load_avg)
          |
          +----------------+------------------+
          |                |
          v                v
  schedutil governor    EAS 能耗估算
  (决定目标频率)        (compute_energy 中
  sugov_update_shared   eenv_pd_max_util 调用
  -> cpufreq_driver     sugov_effective_cpu_perf)
     ->target()

eenv_pd_max_util() 内部调用 sugov_effective_cpu_perf()kernel/sched/fair.c:8319),这使得 EAS 的最大利用率估算与 schedutil 的频率请求逻辑高度一致:EAS 预测的 OPP 就是 schedutil 实际会请求的 OPP,从而确保能耗估算的准确性。

这种耦合也解释了为什么 EAS 要求使用 schedutil governor:如果使用 ondemand 或 performance governor,频率请求逻辑不同,EAS 的能耗预测将失准。


2.8 energy_env 能耗估算环境

kernel/sched/fair.c:8213-8218 定义了 EAS 计算的临时状态结构:

struct energy_env {
    unsigned long task_busy_time;  // 目标任务的忙碌时间贡献(已做 IRQ 缩放)
    unsigned long pd_busy_time;    // 性能域(不含目标任务)的总忙碌时间
    unsigned long cpu_cap;         // 单个 CPU 的实际容量上限
    unsigned long pd_cap;          // 性能域总容量 (nr_cpus * cpu_cap)
};

energy_envfind_energy_efficient_cpu() 的每次调用中栈上分配,包含了单次 EAS 决策所需的所有中间状态。


2.9 eenv_pd_busy_time 与 eenv_pd_max_util

eenv_task_busy_time()kernel/sched/fair.c:8226-8238):

static inline void eenv_task_busy_time(struct energy_env *eenv,
                                       struct task_struct *p, int prev_cpu)
{
    unsigned long busy_time, max_cap = arch_scale_cpu_capacity(prev_cpu);
    unsigned long irq = cpu_util_irq(cpu_rq(prev_cpu));

    if (unlikely(irq >= max_cap))
        busy_time = max_cap;
    else
        busy_time = scale_irq_capacity(task_util_est(p), irq, max_cap);

    eenv->task_busy_time = busy_time;
}

IRQ 缩放的必要性:在高 IRQ 负载下,CPU 被中断抢占,任务实际能获得的时间片减少。scale_irq_capacity() 根据 IRQ 占用比例缩减任务的有效 busy_time,使能耗估算更准确。注意这里使用 prev_cpu(任务上次运行的 CPU)的 IRQ 数据,因为只有在任务已经运行的 CPU 上,IRQ 缩放才有意义(行 8224 注释说明)。

eenv_pd_busy_time()kernel/sched/fair.c:8261-8275):

static inline void eenv_pd_busy_time(struct energy_env *eenv,
                                     struct cpumask *pd_cpus,
                                     struct task_struct *p)
{
    unsigned long busy_time = 0;
    int cpu;

    for_each_cpu(cpu, pd_cpus) {
        unsigned long util = cpu_util(cpu, p, -1, 0);  // -1 表示排除任务 p 的贡献
        busy_time += effective_cpu_util(cpu, util, NULL, NULL);
    }

    eenv->pd_busy_time = min(eenv->pd_cap, busy_time);
}

通过 cpu_util(cpu, p, -1, 0) 将任务 p 从 CPU 利用率中移除,得到"基线"利用率——即使把任务 p 放到其他 CPU,该域的基础负载也保持稳定,从而实现公平的跨 CPU 比较。

eenv_pd_max_util()kernel/sched/fair.c:8284-8324):计算放置任务 p 到 dst_cpu 后,整个性能域的最大有效利用率(决定频率档位)。关键代码:

eff_util = sugov_effective_cpu_perf(cpu, eff_util, min, max);
max_util = max(max_util, eff_util);

对于 dst_cpu,加入任务 p 的利用率;对其他 CPU,保持原有利用率。取所有 CPU 中的最大值——因为整个性能域由单一 OPP 运行,最高利用率决定所有 CPU 的频率。


2.10 compute_energy 能耗计算

kernel/sched/fair.c:8331-8347

static inline unsigned long
compute_energy(struct energy_env *eenv, struct perf_domain *pd,
               struct cpumask *pd_cpus, struct task_struct *p, int dst_cpu)
{
    unsigned long max_util = eenv_pd_max_util(eenv, pd_cpus, p, dst_cpu);
    unsigned long busy_time = eenv->pd_busy_time;

    if (dst_cpu >= 0)
        busy_time = min(eenv->pd_cap, busy_time + eenv->task_busy_time);

    energy = em_cpu_energy(pd->em_pd, max_util, busy_time, eenv->cpu_cap);

    trace_sched_compute_energy_tp(p, dst_cpu, energy, max_util, busy_time);

    return energy;
}

dst_cpu 的语义:

  • dst_cpu >= 0:模拟将任务放到 dst_cpu,busy_time 加上任务自身贡献
  • dst_cpu = -1:计算不含目标任务的"基线"能耗(用于差分计算)

最终的 energy_delta = compute_energy(..., dst_cpu) - compute_energy(..., -1) 代表将任务放到该 CPU 带来的额外能耗增量,EAS 选择 energy_delta 最小的 CPU。

compute_energy 工作流程:

  eenv_pd_busy_time()           eenv_pd_max_util()
       |                               |
       v                               v
  pd_busy_time                    max_util
  (所有 CPU 利用率之和,           (触发调频的最大利用率,
   不含目标任务 p)                  含目标任务放到 dst_cpu)
       |                               |
       +------------+  +---------------+
                    v  v
        em_cpu_energy(pd, max_util, busy_time, cpu_cap)
                    |
                    v
        找到满足 max_util 的最低有效 OPP (i)
                    |
                    v
        ps->cost * busy_time   (ps = em_table->state[i])
                    |
                    v
           能耗估算值(相对值,仅用于比较)

2.11 find_energy_efficient_cpu 唤醒路径详解

kernel/sched/fair.c:8388-8571 是 EAS 的主入口,在 select_task_rq_fair() 中(行 8605-8610)被调用:

// kernel/sched/fair.c:8605-8610
if (!is_rd_overutilized(this_rq()->rd)) {
    new_cpu = find_energy_efficient_cpu(p, prev_cpu);
    if (new_cpu >= 0)
        return new_cpu;
    new_cpu = prev_cpu;
}

即:只有在 root domain 未过载时,才进入 EAS 路径;一旦过载则退回传统负载均衡(sched_balance_find_dst_cpu())。

函数签名及关键局部变量(行 8388-8401):

static int find_energy_efficient_cpu(struct task_struct *p, int prev_cpu)
{
    struct cpumask *cpus = this_cpu_cpumask_var_ptr(select_rq_mask);
    unsigned long prev_delta = ULONG_MAX, best_delta = ULONG_MAX;
    unsigned long p_util_min = uclamp_is_used() ? uclamp_eff_value(p, UCLAMP_MIN) : 0;
    unsigned long p_util_max = uclamp_is_used() ? uclamp_eff_value(p, UCLAMP_MAX) : 1024;
    int prev_fits = -1, best_fits = -1;    // -1: 不满足 uclamp_min, 0: 满足但有余量, 1: 完全满足
    unsigned long best_actual_cap = 0;
    unsigned long prev_actual_cap = 0;
    ...

fits 的三值语义:

  • -1util_fits_cpu() 返回负值,CPU 无法满足 uclamp_min 约束(CPU 容量不足)
  • 0:满足容量但不满足 uclamp_min(任务需要更高性能档)
  • 1 (正值):完全满足,CPU 既有容量又满足性能约束

完整算法步骤注解(行 8403-8571):

find_energy_efficient_cpu(p, prev_cpu)
  |
  +-- [1] 锁定 RCU,获取 rd->pd 性能域链表
  |       获取 sd_asym_cpucapacity(跨越 this_cpu 和 prev_cpu 的最低非对称容量域)
  |
  +-- [2] 快速路径:任务利用率为 0 且无 uclamp_min → 直接留在 prev_cpu
  |       (行 8421: if (!task_util_est(p) && p_util_min == 0) goto unlock)
  |
  +-- [3] eenv_task_busy_time():预计算任务忙碌时间(含 IRQ 缩放)
  |
  +-- [4] 遍历每个 perf_domain (pd):
  |
  |     [4.1] 跳过不在线或不在 sd 范围内的 CPU
  |           计算 cpu_actual_cap, eenv.cpu_cap, eenv.pd_cap
  |
  |     [4.2] 遍历 pd 内每个 CPU:
  |           - cpu_util() 计算去除任务 p 后的利用率
  |           - util_fits_cpu() 检查容量约束(含 uclamp)
  |           - 不满足 (fits == 0) 则跳过
  |           - 计算 spare_cap = cpu_cap - util
  |           - 分别记录 prev_cpu 的 spare_cap/fits
  |             和其他 CPU 中 spare_cap 最大的 (max_spare_cap_cpu)
  |
  |     [4.3] 若该 pd 内无候选 CPU → continue(跳过该 pd 的能耗计算)
  |
  |     [4.4] eenv_pd_busy_time():计算该 pd 基线忙碌时间
  |
  |     [4.5] base_energy = compute_energy(eenv, pd, cpus, p, -1)
  |           (基线能耗,不含目标任务)
  |
  |     [4.6] 若 prev_cpu 在此 pd:
  |           prev_delta = compute_energy(..., prev_cpu) - base_energy
  |           (将任务放到 prev_cpu 的能耗增量)
  |
  |     [4.7] 若 max_spare_cap_cpu 有效且余量优于 prev_cpu:
  |           若 fits 不如 best_fits → 跳过(不降级)
  |           若均无法满足 uclamp_min 但容量不够 → 跳过
  |           cur_delta = compute_energy(..., max_spare_cap_cpu) - base_energy
  |           若均能满足且 cur_delta >= best_delta → 跳过(能耗不优)
  |           否则更新 best_delta, best_energy_cpu, best_fits
  |
  +-- [5] 最终决策(行 8560-8563):
        if (best_fits > prev_fits)          → target = best_energy_cpu  [性能更好]
        elif (best_fits > 0 &&
              best_delta < prev_delta)       → target = best_energy_cpu  [能耗更低]
        elif (best_fits < 0 &&
              best_actual_cap > prev_actual_cap) → target = best_energy_cpu [容量更大]
        else                                 → target = prev_cpu         [保持原位]

关键设计决策:

  1. 每个 pd 只选一个候选 CPU(最大空闲容量者):避免 O(n^2) 比较,减少热路径开销
  2. prev_cpu 始终参与比较:体现调度器的"惰性"偏好,减少不必要迁移
  3. 三值 fits 优先级:性能约束(uclamp_min)优先于能耗优化,保证 RT/延迟敏感任务得到满足
  4. 早退机制goto unlock):若检测到 CPU 利用率已变化(prev_delta < base_energy),说明数据不一致,直接退回 prev_cpu 而非做出错误决策

2.12 overutilized 保护机制

kernel/sched/sched.h:3702-3705

static inline bool sched_energy_enabled(void)
{
    return static_branch_unlikely(&sched_energy_present);
}

is_rd_overutilized()kernel/sched/fair.c 附近):

static inline bool is_rd_overutilized(struct root_domain *rd)
{
    return !sched_energy_enabled() || READ_ONCE(rd->overutilized);
}

rd->overutilizedcheck_update_overutilized_status() 维护:当任意 CPU 的利用率超过其容量(超过 fits_capacity 阈值,即利用率 > 80% 容量),该标志被设置。一旦置位,所有唤醒路径绕过 EAS,退回传统的最空闲 CPU 策略(sched_balance_find_dst_cpu()),以保证吞吐量而非能效。

设计思路:EAS 的能量模型只包含活跃功耗,未建模闲置功耗和深睡眠。在高负载时 CPU 已无法进入深睡眠,EAS 的节能假设不再成立,传统负载均衡反而能保证更好的吞吐量。

EAS 与传统负载均衡的切换逻辑:

               任务唤醒
                  |
                  v
     is_rd_overutilized(this_rq()->rd)?
          /                \
        YES                 NO
         |                   |
         v                   v
    传统负载均衡         EAS 路径
    sched_balance_       find_energy_efficient_cpu()
    find_dst_cpu()       (最节能 CPU)
    (最空闲 CPU)

2.13 uclamp 与 EAS 的交互

uclamp(utilization clamping)是 Linux 5.3 引入的机制,允许用户空间对任务的有效利用率设置上下限:

  • UCLAMP_MIN:最小有效利用率(保证性能下限)
  • UCLAMP_MAX:最大有效利用率(限制 CPU 频率上限)

EAS 中 uclamp 通过 util_fits_cpu() 判断影响候选 CPU 的筛选:

// 在 find_energy_efficient_cpu 行 8481:
fits = util_fits_cpu(util, util_min, util_max, cpu);
if (!fits)
    continue;  // 不满足容量约束,跳过该 CPU

util_min/util_max 取的是 CPU 上正在运行任务的 uclamp 聚合值与目标任务 uclamp 值的最大值(行 8474-8479):

rq_util_min = uclamp_rq_get(rq, UCLAMP_MIN);
rq_util_max = uclamp_rq_get(rq, UCLAMP_MAX);

util_min = max(rq_util_min, p_util_min);
util_max = max(rq_util_max, p_util_max);

这确保了 uclamp_min 约束在 EAS 路径中被严格执行:若用户对某任务设置了较高的 uclamp_min(要求高性能),EAS 会优先将其放到有足够容量的大核,而非小核。


3. AutoNUMA:自动 NUMA 平衡

3.1 问题背景:NUMA 远端内存访问

在 NUMA(Non-Uniform Memory Access)系统中,每个处理器节点(node)都有其本地内存控制器,访问本地内存的延迟远低于访问远端节点内存。典型的延迟差异:

NUMA 访问延迟示意(典型双路 AMD EPYC):

  Node 0 (CPU 0-63)                Node 1 (CPU 64-127)
  +-----------------------+         +-----------------------+
  |  本地内存              |         |  本地内存              |
  |  延迟: ~80ns           |  NUMA   |  延迟: ~80ns           |
  |                       | <-----> |                       |
  |  远端内存访问延迟       |   QPI   |  远端内存访问延迟       |
  |  ~140ns (约 1.75x)    |  /xGMI  |  ~140ns (约 1.75x)    |
  +-----------------------+         +-----------------------+

Linux 在内存分配时倾向于在当前 CPU 所在节点分配(首次接触策略,first-touch)。但进程在运行过程中可能被调度到不同节点的 CPU,或者子进程继承了父进程的内存布局,导致内存与 CPU 不在同一节点。AutoNUMA 的使命就是检测并修复这种错位。


3.2 核心机制概览

AutoNUMA 的整体工作流程是一个闭环的反馈控制系统:

AutoNUMA 工作闭环:

  ┌─────────────────────────────────────────────────────────────┐
  │                                                             │
  │  ① 周期扫描(task_numa_work)                              │
  │     将运行中任务的 PTE 标记为 PROT_NONE                    │
  │                    │                                        │
  │                    v                                        │
  │  ② 缺页中断触发(do_numa_page)                            │
  │     应用访问被标记的页 → 触发 NUMA fault                   │
  │                    │                                        │
  │                    v                                        │
  │  ③ 统计记录(task_numa_fault)                             │
  │     记录:发生在哪个节点 / 哪个 CPU / 是否私有访问          │
  │                    │                                        │
  │                    v                                        │
  │  ④ 偏好节点计算(task_numa_placement)                     │
  │     基于累积缺页统计,确定最优节点(含衰减)               │
  │                    │                                        │
  │                    v                                        │
  │  ⑤ 迁移决策(numa_migrate_preferred)                     │
  │     内存迁移:migrate_misplaced_folio                      │
  │     任务迁移:task_numa_migrate → migrate_task_to          │
  │                    │                                        │
  │                    └──────────────── 回到 ①               │
  └─────────────────────────────────────────────────────────────┘

3.3 NUMA 缺页统计数据结构

kernel/sched/fair.c:1525-1544struct numa_group 是 AutoNUMA 的核心数据结构:

struct numa_group {
    refcount_t refcount;           // 引用计数

    spinlock_t lock;               // 保护 nr_tasks, tasks
    int nr_tasks;                  // 组内活跃任务数
    pid_t gid;                     // 组 ID(第一个加入任务的 PID)
    int active_nodes;              // 活跃 NUMA 节点数(CPU 侧缺页超过阈值的节点)

    struct rcu_head rcu;
    unsigned long total_faults;    // 组内总缺页次数(内存侧,用于归一化)
    unsigned long max_faults_cpu;  // CPU 侧最大节点缺页数(用于 active_nodes 判定)

    // faults[] 分为四区:
    //   [0, NR_NUMA_HINT_FAULT_STATS * nr_node_ids)    <- 平滑统计值
    //   [NR_NUMA_HINT_FAULT_STATS * nr_node_ids, ...)  <- 当前窗口缓冲区
    unsigned long faults[];
};

active_nodes 的作用:记录有显著 CPU 访问活动的节点数。当 active_nodes > 1 时,说明该工作组跨节点运行,此时 AutoNUMA 会更积极地搜索其他候选节点(task_numa_migrate 中行 2629)。

task_struct 中相关字段:

  • numa_faults[]:按节点、访问类型统计的缺页数组(延迟分配)
  • total_numa_faults:总缺页次数(内存侧,用于 task_weight 归一化)
  • numa_preferred_nid:当前推断的偏好 NUMA 节点(NUMA_NO_NODE 表示未知)
  • numa_scan_period:当前扫描周期(ms),在 task_scan_mintask_scan_max 范围内动态调整
  • numa_scan_seq:与 mm->numa_scan_seq 对比,判断是否需要重新 placement 评估
  • numa_faults_locality[3]:[0]=远端缺页数, [1]=本地缺页数, [2]=迁移失败次数
  • numa_pages_migrated:累计迁移成功的页数(用于统计)
  • numa_migrate_retry:下次允许重试迁移的 jiffies 时间戳

3.4 numa_faults 数组布局

kernel/sched/fair.c:1660-1692 定义了统计维度的常量:

// kernel/sched/fair.c:1660-1667
#define NR_NUMA_HINT_FAULT_TYPES   2   // private(1) 或 shared(0)
#define NR_NUMA_HINT_FAULT_STATS   4   // 2维度: (MEM/CPU) x (private/shared)
#define NR_NUMA_HINT_FAULT_BUCKETS 8   // 统计值 + 缓冲区(各 4 个)

static inline int task_faults_idx(enum numa_faults_stats s, int nid, int priv)
{
    return NR_NUMA_HINT_FAULT_TYPES * (s * nr_node_ids + nid) + priv;
}

numa_faults_stats 枚举:

  • NUMA_MEM(0):内存侧平滑统计——缺页发生时内存所在节点的统计
  • NUMA_CPU(1):CPU 侧平滑统计——缺页发生时 CPU 所在节点的统计(按运行时归一化)
  • NUMA_MEMBUF(2):内存侧当前窗口缓冲区
  • NUMA_CPUBUF(3):CPU 侧当前窗口缓冲区
numa_faults 数组布局示意(假设 2 节点系统,nr_node_ids = 2):

  索引  类型                              含义
  ----  --------------------------------  -------------------------
   0    NUMA_MEM * 2 * 2 + node0 * 2 + 0  内存侧/node0/shared 平滑值
   1    NUMA_MEM * 2 * 2 + node0 * 2 + 1  内存侧/node0/private 平滑值
   2    NUMA_MEM * 2 * 2 + node1 * 2 + 0  内存侧/node1/shared 平滑值
   3    NUMA_MEM * 2 * 2 + node1 * 2 + 1  内存侧/node1/private 平滑值
   4    NUMA_CPU * ... + node0 * 2 + 0     CPU 侧/node0/shared 平滑值
   5    NUMA_CPU * ... + node0 * 2 + 1     CPU 侧/node0/private 平滑值
   6    NUMA_CPU * ... + node1 * 2 + 0     CPU 侧/node1/shared 平滑值
   7    NUMA_CPU * ... + node1 * 2 + 1     CPU 侧/node1/private 平滑值
  ---- (以上 8 项为平滑统计值,以下 8 项为当前窗口缓冲区)
   8    NUMA_MEMBUF ...                    内存侧/node0/shared 缓冲区
   ...
  15    NUMA_CPUBUF ...                    CPU 侧/node1/private 缓冲区

每次 task_numa_fault() 触发时写缓冲区(BUF 区),task_numa_placement() 每个扫描轮次将缓冲区通过指数衰减合并到平滑统计区。


3.5 NUMA 扫描:task_numa_work

kernel/sched/fair.c:3362-3598 是 AutoNUMA 的扫描核心,通过 task_work 机制在每个扫描周期、任务返回用户空间时执行。

触发链(完整路径):

scheduler_tick()  [kernel/sched/core.c]
    -> task_tick()
        -> task_tick_fair(rq, curr, queued)
            -> task_tick_numa(rq, curr)  [fair.c:3668]
                |
                +-- 检查: curr->mm 存在 &&
                |         非退出/内核线程 &&
                |         work->next == work(未pending)
                |
                +-- 使用 CPU 运行时(sum_exec_runtime)而非挂钟时间判断间隔
                |   目的:只有真正运行的任务才触发,避免休眠任务浪费扫描
                |
                +-- now = curr->se.sum_exec_runtime
                    period = curr->numa_scan_period * NSEC_PER_MSEC
                    if (now > node_stamp + period):
                        task_work_add(curr, &curr->numa_work, TWA_RESUME)
                                     |
                                     v (syscall/中断返回用户空间前)
                         task_numa_work(work)  [fair.c:3362]

task_numa_work() 核心逻辑(行 3362-3598):

static void task_numa_work(struct callback_head *work)
{
    // [1] 退出检查:PF_EXITING 时直接返回
    if (p->flags & PF_EXITING)
        return;

    // [2] cpuset 检查:若内存被固定到单节点,无需扫描
    if (cpusets_enabled() &&
        nodes_weight(cpuset_current_mems_allowed) == 1)
        return;   // trace_sched_skip_cpuset_numa

    // [3] 首次扫描:设置 numa_next_scan 延迟(scan_delay ms)
    if (!mm->numa_next_scan)
        mm->numa_next_scan = now + msecs_to_jiffies(
            sysctl_numa_balancing_scan_delay);

    // [4] 全局速率限制:mm 级别,防止多线程过频扫描
    migrate = mm->numa_next_scan;
    if (time_before(now, migrate)) return;

    // [5] 原子 CAS 更新 numa_next_scan,同一个 mm 的多线程竞争,只有一个胜出
    if (!try_cmpxchg(&mm->numa_next_scan, &migrate, next_scan))
        return;

    // [6] 计算本次扫描量
    pages = sysctl_numa_balancing_scan_size;
    pages <<= 20 - PAGE_SHIFT;   // MB 转 pages
    virtpages = pages * 8;        // 虚拟地址空间扫描上限

    // [7] 非阻塞锁:mmap_read_trylock,失败直接返回
    if (!mmap_read_trylock(mm)) return;

    // [8] VMA 遍历(见 3.6 节 VMA 过滤规则)
    vma_iter_init(&vmi, mm, mm->numa_scan_offset);
    for (; vma; vma = vma_next(&vmi)) {
        // 过滤不适合的 VMA ...
        // 调用 change_prot_numa(vma, start, end)
        // 累计扫描的 pages 和 virtpages
    }

    // [9] 多线程场景:若所有 VMA 都因 PID 不活跃被跳过,强制扫描一轮
    if (vma_pids_skipped && !vma_pids_forced) {
        vma_pids_forced = true;
        goto retry_pids;
    }

    mmap_read_unlock(mm);
}

3.6 VMA 过滤规则详解

task_numa_work() 在遍历 VMA 时应用多层过滤(行 3454-3547),每种跳过原因都有对应的 tracepoint(sched_skip_vma_numa):

跳过原因 条件 Trace 标签
不可迁移 VMA !vma_migratable(vma)!vma_policy_mof(vma) NUMAB_SKIP_UNSUITABLE
巨页 / 混合映射 is_vm_hugetlb_page(vma)VM_MIXEDMAP NUMAB_SKIP_UNSUITABLE
只读文件映射 共享库等,vm_file && (VM_READ 但无 VM_WRITE) NUMAB_SKIP_SHARED_RO
不可访问 VMA !vma_is_accessible(vma)(PROT_NONE VMA) NUMAB_SKIP_INACCESSIBLE
扫描延迟未到 time_before(now, vma->numab_state->next_scan) NUMAB_SKIP_SCAN_DELAY
同轮次已扫描 prev_scan_seq == mm->numa_scan_seq NUMAB_SKIP_SEQ_COMPLETED
PID 未访问 !vma_is_accessed(mm, vma) NUMAB_SKIP_PID_INACTIVE

其中 PID 活跃度检查(vma_is_accessed(),行 3319-3343)是一项重要优化:

static bool vma_is_accessed(struct mm_struct *mm, struct vm_area_struct *vma)
{
    // 前两轮扫描无条件扫描所有 VMA(确保首次建立基线)
    if ((READ_ONCE(mm->numa_scan_seq) -
         vma->numab_state->start_scan_seq) < 2)
        return true;

    // 之后只扫描当前 PID 近期有访问的 VMA
    pids = vma->numab_state->pids_active[0] | vma->numab_state->pids_active[1];
    if (test_bit(hash_32(current->pid, ilog2(BITS_PER_LONG)), &pids))
        return true;

    // 正在进行中的扫描不中断(避免 VMA 永远扫不到)
    if (mm->numa_scan_offset > vma->vm_start)
        return true;

    return false;
}

pids_active[] 是一个双 slot 的滑动窗口(两个 unsigned long 位图):

  • pids_active[1]:当前活跃 PID 集合(近期访问记录)
  • pids_active[0]:上一个时间窗口的活跃 PID
  • 每隔 VMA_PID_RESET_PERIOD 时间轮换:pids_active[0] = pids_active[1]; pids_active[1] = 0

这个机制避免了对大量"冷"VMA(如动态链接库的只读段、长期未访问的 mmap 区域)进行无谓的 PROT_NONE 扫描,显著减少了 AutoNUMA 的开销。


3.7 change_prot_numa 与 PROT_NONE 标记

change_prot_numa(vma, start, end) 遍历指定范围内的 PTE,将已存在的映射(非空 PTE)修改为 NUMA hint 状态(_PAGE_NUMA,在 x86 上复用 _PAGE_PROTNONE 位)。

只有已映射的页才会被标记——未映射(尚未分配)的区域没有 PTE,不会触发 NUMA fault,也不会被迁移。

被标记后的 PTE 行为:

  • 内核访问:允许,不触发缺页
  • 用户态读/写:触发缺页中断,进入 do_numa_page()mm/memory.c:6048
  • 具体实现:在 ARM64 上,通过清除 PTE 的 PTE_AF(Access Flag)位或设置 PROT_NONE 实现

3.8 扫描速率动态调整

task_scan_min()task_scan_start()task_scan_max() 三个函数(kernel/sched/fair.c:1586-1646)共同控制扫描周期的动态范围。

task_scan_min()(行 1586-1598):计算最小扫描周期,确保扫描速率不超过 MAX_SCAN_WINDOW(2560 MB/s):

static unsigned int task_scan_min(struct task_struct *p)
{
    unsigned int scan_size = READ_ONCE(sysctl_numa_balancing_scan_size);
    unsigned int windows = 1;

    if (scan_size < MAX_SCAN_WINDOW)
        windows = MAX_SCAN_WINDOW / scan_size;
    floor = 1000 / windows;  // 确保不会低于合理下限

    scan = sysctl_numa_balancing_scan_period_min / task_nr_scan_windows(p);
    return max_t(unsigned int, floor, scan);
}

task_nr_scan_windows(p):任务的 RSS 除以 scan_size,得到扫描完整 RSS 需要的轮次数。RSS 越大,每轮覆盖越少,因此最小周期相应缩短(确保 RSS 可以被完整扫描)。

task_scan_start()(行 1600-1620):在 task_scan_min() 基础上,根据 numa_group 的共享内存比例调整起始周期:

static unsigned int task_scan_start(struct task_struct *p)
{
    unsigned long smin = task_scan_min(p);
    unsigned long period = smin;
    struct numa_group *ng = ...;

    if (ng) {
        unsigned long shared = group_faults_shared(ng);
        unsigned long private = group_faults_priv(ng);

        period *= refcount_read(&ng->refcount);  // 组越大,周期越长
        period *= shared + 1;
        period /= private + shared + 1;           // 共享比例越高,周期越长
    }
    return max(smin, period);
}

设计逻辑:共享内存多的任务组,每次扫描都会对组内所有成员产生影响,频繁扫描会放大开销,因此通过增大周期来降低扫描频率。

update_task_scan_period()(行 3062 处调用):根据本次扫描周期内本地与远端缺页的比例调整下次周期:

  • 本地缺页多(内存已在本地)→ 减小周期,减少不必要扫描(收敛后降频)
  • 远端缺页多(仍有优化空间)→ 增大周期,让迁移有时间稳定

3.9 NUMA fault 处理路径:do_numa_page

当用户态访问被标记为 PROT_NONE 的 NUMA hint 页时,触发缺页中断,最终进入 do_numa_page()mm/memory.c:6048)。

缺页处理路径(行 6310-6323 附近):

// mm/memory.c:6322-6323
if (pte_protnone(vmf->orig_pte) && vma_is_accessible(vmf->vma))
    return do_numa_page(vmf);

do_numa_page() 的完整流程(行 6048-6137):

static vm_fault_t do_numa_page(struct vm_fault *vmf)
{
    spin_lock(vmf->ptl);                    // 获取 PTE 锁
    old_pte = ptep_get(vmf->pte);           // 读取当前 PTE

    // 并发检查:若 PTE 已被其他路径修改,放弃
    if (!pte_same(old_pte, vmf->orig_pte)) {
        pte_unmap_unlock(vmf->pte, vmf->ptl);
        return 0;
    }

    pte = pte_modify(old_pte, vma->vm_page_prot);  // 恢复完整权限(但保持 NUMA hint)
    writable = pte_write(pte);
    folio = vm_normal_folio(vma, vmf->address, pte);  // 找到对应 folio

    nid = folio_nid(folio);               // 当前内存所在节点
    nr_pages = folio_nr_pages(folio);

    // 调用迁移检查(含 memory tiering 的热页判断)
    target_nid = numa_migrate_check(folio, vmf, vmf->address,
                                    &flags, writable, &last_cpupid);
    if (target_nid == NUMA_NO_NODE)
        goto out_map;  // 不需要迁移

    // 迁移准备(隔离 folio)
    if (migrate_misplaced_folio_prepare(folio, vma, target_nid)) {
        flags |= TNF_MIGRATE_FAIL;
        goto out_map;
    }

    pte_unmap_unlock(vmf->pte, vmf->ptl);  // 释放锁,迁移是异步的

    // 实际迁移
    if (!migrate_misplaced_folio(folio, target_nid)) {
        nid = target_nid;
        flags |= TNF_MIGRATED;
        task_numa_fault(last_cpupid, nid, nr_pages, flags);
        return 0;
    }

    flags |= TNF_MIGRATE_FAIL;
    // ... 恢复 PTE 为可访问状态
out_map:
    // 重建页表项(单页或大页)
    if (folio && folio_test_large(folio))
        numa_rebuild_large_mapping(...);
    else
        numa_rebuild_single_mapping(...);

    if (nid != NUMA_NO_NODE)
        task_numa_fault(last_cpupid, nid, nr_pages, flags);
    return 0;
}

无论迁移是否成功,do_numa_page() 都在返回前调用 task_numa_fault() 记录统计——迁移成功时 nid 为目标节点,失败时为原节点。这确保了统计的完整性。


3.10 task_numa_fault:缺页记录与统计

kernel/sched/fair.c:3224-3303,这是从缺页处理路径回调到调度器的接口:

void task_numa_fault(int last_cpupid, int mem_node, int pages, int flags)
{
    struct task_struct *p = current;
    bool migrated = flags & TNF_MIGRATED;
    int cpu_node = task_node(current);    // 当前 CPU 所在 NUMA 节点
    int local = !!(flags & TNF_FAULT_LOCAL);

    // [1] 全局开关:CONFIG_NUMA_BALANCING 且 sched_numa_balancing 静态 key
    if (!static_branch_likely(&sched_numa_balancing))
        return;

    // [2] Memory Tiering 模式下,只处理顶层(快速)内存节点的缺页
    //     慢速内存(CXL/PMEM)的缺页用 cpupid 记录扫描时间而非 CPU/PID
    if (!node_is_toptier(mem_node) &&
        (sysctl_numa_balancing_mode & NUMA_BALANCING_MEMORY_TIERING ||
         !cpupid_valid(last_cpupid)))
        return;

    // [3] 延迟分配统计数组(首次缺页时分配)
    if (unlikely(!p->numa_faults)) {
        int size = sizeof(*p->numa_faults) *
                   NR_NUMA_HINT_FAULT_BUCKETS * nr_node_ids;
        p->numa_faults = kzalloc(size, GFP_KERNEL|__GFP_NOWARN);
    }

    // [4] 判断访问类型:私有(priv=1) 或 共享(priv=0)
    if (unlikely(last_cpupid == (-1 & LAST_CPUPID_MASK))) {
        priv = 1;   // 页从未被访问过,视为私有
    } else {
        priv = cpupid_match_pid(p, last_cpupid);  // PID 匹配 = 私有
        if (!priv && !(flags & TNF_NO_GROUP))
            task_numa_group(p, last_cpupid, flags, &priv);  // 尝试建立/加入 group
    }

    // [5] 多节点工作组中,两个活跃节点间的共享缺页计为"本地"
    ng = deref_curr_numa_group(p);
    if (!priv && !local && ng && ng->active_nodes > 1 &&
        numa_is_active_node(cpu_node, ng) &&
        numa_is_active_node(mem_node, ng))
        local = 1;   // 组内跨节点访问不算"远端"

    // [6] 周期性重新评估:若超过 retry 时间,触发 placement 和迁移
    if (time_after(jiffies, p->numa_migrate_retry)) {
        task_numa_placement(p);
        numa_migrate_preferred(p);
    }

    // [7] 更新统计计数器
    if (migrated) p->numa_pages_migrated += pages;
    if (flags & TNF_MIGRATE_FAIL) p->numa_faults_locality[2] += pages;

    p->numa_faults[task_faults_idx(NUMA_MEMBUF, mem_node, priv)] += pages;
    p->numa_faults[task_faults_idx(NUMA_CPUBUF, cpu_node, priv)] += pages;
    p->numa_faults_locality[local] += pages;
}

TNF_* 标志集合:

  • TNF_MIGRATED:迁移成功
  • TNF_MIGRATE_FAIL:迁移失败
  • TNF_FAULT_LOCAL:缺页发生在本地节点
  • TNF_SHARED:页被多进程共享
  • TNF_NO_GROUP:禁止建立 numa_group(如 khugepaged 路径)

3.11 cpupid 字段:CPU/PID 编码机制

last_cpupid 是存储在 struct page(或 folio)中的一个紧凑编码字段,记录上次访问该页的 CPU 编号和 PID 的片段。具体位宽取决于架构和配置:

cpupid 字段编码(典型 64 位 ARM/x86):

  +------------------+------------------+
  |    CPU 编号      |    PID 片段      |
  |   (CPUPID_SHF    |  (CPUPID_MASK    |
  |    位以上)       |    位以下)       |
  +------------------+------------------+

  cpupid_match_pid(p, cpupid):
    return (cpupid >> CPUPID_SHF == p->pid & CPUPID_MASK) &&
           (cpupid_to_cpu(cpupid) < nr_cpu_ids)

当多个进程(或线程)访问同一页时,cpupid 会记录最后一个访问者的信息。若下一次访问者的 PID 与 cpupid 不匹配,说明该页被多任务共享,priv = 0,进而触发 task_numa_group() 尝试建立分组。

Memory Tiering 模式下,cpupid 字段被复用来存储页的扫描时间戳(numa_hint_fault_latency() 中读取),而非 CPU/PID。当 cpupid_valid() 为 false 时,说明当前处于 tiering 模式,AutoNUMA 的普通 NUMA 均衡逻辑应跳过(行 3244-3247)。


3.12 task_numa_placement:偏好节点计算

kernel/sched/fair.c:2951-3063 是 AutoNUMA 的"决策脑",根据累积缺页统计确定任务的偏好 NUMA 节点:

static void task_numa_placement(struct task_struct *p)
    __context_unsafe(/* conditional locking */)
{
    // [1] 幂等检查:同一扫描轮次(numa_scan_seq)只计算一次
    seq = READ_ONCE(p->mm->numa_scan_seq);
    if (p->numa_scan_seq == seq)
        return;
    p->numa_scan_seq = seq;

    // [2] 获取运行时信息(用于 CPU 侧统计归一化)
    total_faults = p->numa_faults_locality[0] + p->numa_faults_locality[1];
    runtime = numa_get_avg_runtime(p, &period);

    // [3] 若属于 numa_group,加锁保护组统计(spin_lock_irq)
    ng = deref_curr_numa_group(p);
    if (ng) { group_lock = &ng->lock; spin_lock_irq(group_lock); }

    // [4] 遍历所有在线节点,对每个节点计算加权缺页数
    for_each_online_node(nid) {
        for (priv = 0; priv < NR_NUMA_HINT_FAULT_TYPES; priv++) {
            // 内存侧:指数衰减合并(旧值减半 + 新缓冲区增量)
            diff = p->numa_faults[membuf_idx] -
                   p->numa_faults[mem_idx] / 2;
            p->numa_faults[membuf_idx] = 0;  // 清空缓冲区
            p->numa_faults[mem_idx] += diff;

            // CPU 侧:按运行时占比归一化
            f_weight = div64_u64(runtime << 16, period + 1);
            f_weight = (f_weight * p->numa_faults[cpubuf_idx]) /
                       (total_faults + 1);
            f_diff = f_weight - p->numa_faults[cpu_idx] / 2;
            p->numa_faults[cpubuf_idx] = 0;
            p->numa_faults[cpu_idx] += f_diff;

            faults += p->numa_faults[mem_idx];
            if (ng) {
                ng->faults[mem_idx] += diff;   // 同步更新 group 统计
                ng->faults[cpu_idx] += f_diff;
                ng->total_faults += diff;
                group_faults += ng->faults[mem_idx];
            }
        }

        // 找内存侧缺页最多的节点 = 偏好节点候选
        if (!ng && faults > max_faults) { max_faults = faults; max_nid = nid; }
        else if (group_faults > max_faults) { max_faults = group_faults; max_nid = nid; }
    }

    // [5] 修正:目标节点必须有 CPU(排除纯内存节点)
    max_nid = numa_nearest_node(max_nid, N_CPU);

    // [6] 若属于 group,用组级别统计确定偏好节点
    if (ng) {
        numa_group_count_active_nodes(ng);
        spin_unlock_irq(group_lock);
        max_nid = preferred_group_nid(p, max_nid);
    }

    // [7] 更新偏好节点
    if (max_faults && max_nid != p->numa_preferred_nid)
        sched_setnuma(p, max_nid);

    // [8] 根据本地/远端缺页比例调整下次扫描周期
    update_task_scan_period(p, fault_types[0], fault_types[1]);
}

3.13 指数衰减与滑动窗口统计

task_numa_placement() 中使用的指数衰减公式(行 3000-3004):

new_smoothed = old_smoothed / 2 + new_sample

即每个扫描轮次,旧值减半,加上新一轮的缓冲区增量。这实现了一个半衰期为 1 轮扫描的指数移动平均(EMA):

时间维度示意:

  轮次 t       t+1       t+2       t+3
  缺页数 100   50        80        30

  平滑值:
  t:   100
  t+1: 100/2 + 50 = 100
  t+2: 100/2 + 80 = 130
  t+3: 130/2 + 30 = 95

  效果:近期访问权重更高,但历史访问不被完全遗忘。
  若某个时期任务迁移到了 node1,统计会快速反映新位置的访问模式。

CPU 侧的统计还额外做了运行时归一化:

// 归一化公式(行 3011-3014):
f_weight = (runtime / period) * cpubuf_faults / total_faults

// 目的:运行时短的任务(如刚被唤醒)影响力小;
//       长期运行的任务的 CPU 亲和性更可靠

3.14 task_weight 与 group_weight:亲和性评分

kernel/sched/fair.c:1828-1865

static inline unsigned long task_weight(struct task_struct *p, int nid, int dist)
{
    unsigned long faults, total_faults;

    if (!p->numa_faults) return 0;
    total_faults = p->total_numa_faults;
    if (!total_faults) return 0;

    faults = task_faults(p, nid);
    faults += score_nearby_nodes(p, nid, dist, true);  // 邻近节点加成

    return 1000 * faults / total_faults;  // 千分比(0-1000)
}

static inline unsigned long group_weight(struct task_struct *p, int nid, int dist)
{
    struct numa_group *ng = deref_task_numa_group(p);
    unsigned long faults, total_faults;

    if (!ng) return 0;
    total_faults = ng->total_faults;
    if (!total_faults) return 0;

    faults = group_faults(p, nid);
    faults += score_nearby_nodes(p, nid, dist, false);

    return 1000 * faults / total_faults;
}

返回值含义:该任务(或组)在节点 nid 上的内存访问比例(千分比)。

这两个权重在 task_numa_migrate() 中用于计算迁移收益:

迁移到 dst_nid 的净收益(行 2614-2615):

  taskimp  = task_weight(p, dst_nid, dist) - task_weight(p, src_nid, dist)
  groupimp = group_weight(p, dst_nid, dist) - group_weight(p, src_nid, dist)
  imp = taskimp + groupimp

  若 imp > 0 → 迁移有利
  若 taskimp < 0 && groupimp < 0 → 迁移对任务和组均不利 → 跳过

3.15 score_nearby_nodes:非直连拓扑处理

kernel/sched/fair.c:1756-1820,处理 NUMA 拓扑不是全互联(fully-connected)的情况:

static unsigned long score_nearby_nodes(struct task_struct *p, int nid,
                                        int lim_dist, bool task)
{
    // NUMA_DIRECT(全直连):无需特殊处理,返回 0
    if (sched_numa_topology_type == NUMA_DIRECT)
        return 0;

    // 遍历其他节点
    for_each_online_node(node) {
        if (dist >= max_dist || node == nid) continue;

        // NUMA_BACKPLANE(背板互联):只考虑同一群组内的节点
        if (sched_numa_topology_type == NUMA_BACKPLANE && dist >= lim_dist)
            continue;

        faults = task ? task_faults(p, node) : group_faults(p, node);

        // NUMA_GLUELESS_MESH(无胶水 mesh):距离越远,权重越低
        if (sched_numa_topology_type == NUMA_GLUELESS_MESH) {
            faults *= (max_dist - dist);
            faults /= (max_dist - LOCAL_DISTANCE);
        }

        score += faults;
    }
    return score;
}

三种 NUMA 拓扑类型:

NUMA_DIRECT(全互联,如 2S x86):
  node0 -------- node1
  所有节点直接互联,距离相等,无需邻近分析

NUMA_GLUELESS_MESH(无胶水 mesh,如 4S x86):
  node0 -- node1 -- node3
            |
          node2
  节点通过中间节点转发,距离不等,邻近节点有折算加成

NUMA_BACKPLANE(背板,如某些多路 ARM 服务器):
  [group0: node0, node1] <---背板---> [group1: node2, node3]
  节点按组聚集,组内低延迟,跨组高延迟
  迁移时应考虑整个组的内存亲和性

3.16 task_numa_migrate:任务迁移决策

kernel/sched/fair.c:2561-2694,这是 AutoNUMA 中决定是否移动任务 CPU 亲和性的核心函数:

static int task_numa_migrate(struct task_struct *p)
{
    struct task_numa_env env = {
        .p = p,
        .src_cpu = task_cpu(p),
        .src_nid = task_node(p),
        .imbalance_pct = 112,          // 初始容忍 12% 的负载不平衡
        .best_task = NULL,             // 候选交换任务(task swap)
        .best_imp = 0,                 // 最佳迁移收益
        .best_cpu = -1,                // 最佳目标 CPU
    };

    // 获取 SD_NUMA 调度域,修正 imbalance_pct
    sd = rcu_dereference_all(per_cpu(sd_numa, env.src_cpu));
    if (sd)
        env.imbalance_pct = 100 + (sd->imbalance_pct - 100) / 2;
    // 使用调度域的 imbalance_pct 的一半,避免过激迁移

    env.dst_nid = p->numa_preferred_nid;  // 目标:偏好节点

    // 计算当前节点与偏好节点的迁移收益
    taskweight  = task_weight(p, env.src_nid, dist);
    groupweight = group_weight(p, env.src_nid, dist);
    taskimp  = task_weight(p, env.dst_nid, dist) - taskweight;
    groupimp = group_weight(p, env.dst_nid, dist) - groupweight;

    // 在偏好节点上寻找合适的目标 CPU
    task_numa_find_cpu(&env, taskimp, groupimp);

    // 若组跨多节点活跃,也搜索其他节点
    ng = deref_curr_numa_group(p);
    if (env.best_cpu == -1 || (ng && ng->active_nodes > 1)) {
        for_each_node_state(nid, N_CPU) {
            if (nid == env.src_nid || nid == p->numa_preferred_nid)
                continue;
            taskimp  = task_weight(p, nid, dist) - taskweight;
            groupimp = group_weight(p, nid, dist) - groupweight;
            if (taskimp < 0 && groupimp < 0)
                continue;  // 对任务和组均无益,跳过
            task_numa_find_cpu(&env, taskimp, groupimp);
        }
    }

    // 执行迁移
    if (env.best_cpu == -1) {
        trace_sched_stick_numa(p, env.src_cpu, NULL, -1);
        return -EAGAIN;     // 无合适目标
    }

    if (env.best_task == NULL)
        ret = migrate_task_to(p, env.best_cpu);      // 单向迁移
    else
        ret = migrate_swap(p, env.best_task,         // 双向交换
                          env.best_cpu, env.src_cpu);
    return ret;
}

双向任务交换(task swap)是 AutoNUMA 的一个巧妙设计:若偏好节点上的某个 CPU 正运行着一个其偏好节点恰好是 p 的当前节点的任务,则双向交换对双方都有利,避免了 CPU 负载不均的问题。


3.17 numa_migrate_preferred:迁移入口

kernel/sched/fair.c:2697-2715,周期性尝试将任务迁移到偏好节点:

static void numa_migrate_preferred(struct task_struct *p)
{
    unsigned long interval = HZ;

    // 无统计数据或无偏好节点时跳过
    if (unlikely(p->numa_preferred_nid == NUMA_NO_NODE || !p->numa_faults))
        return;

    // 设置下次重试时间:最多 1 秒,最少 scan_period/16
    interval = min(interval, msecs_to_jiffies(p->numa_scan_period) / 16);
    p->numa_migrate_retry = jiffies + interval;

    // 若已在偏好节点上运行,无需迁移
    if (task_node(p) == p->numa_preferred_nid)
        return;

    task_numa_migrate(p);
}

interval = scan_period / 16 的设计:使迁移重试频率与扫描频率挂钩——扫描周期越短(访问模式变化越快),重试越频繁;扫描周期越长(已稳定),重试越少。


3.18 migrate_misplaced_folio:内存页迁移

内存迁移通过 mm/migrate.c 实现,分为准备和执行两个阶段。

准备阶段 migrate_misplaced_folio_prepare()mm/migrate.c:2657-2713):

int migrate_misplaced_folio_prepare(struct folio *folio,
        struct vm_area_struct *vma, int node)
{
    // [1] 共享可执行页不迁移(如 .so 共享库,各进程 cache 拷贝收益大于迁移成本)
    if ((vma->vm_flags & VM_EXEC) && folio_maybe_mapped_shared(folio))
        return -EACCES;

    // [2] 脏文件页不迁移(需要先写回,异步迁移不支持)
    if (folio_test_dirty(folio))
        return -EAGAIN;

    // [3] 目标节点内存水位检查,不足时唤醒 kswapd 并返回
    pgdat = NODE_DATA(node);
    if (!migrate_balanced_pgdat(pgdat, folio_nr_pages(folio)))
        return -EAGAIN;

    // [4] 从 LRU 链表中隔离 folio(隔离后持有 folio 引用)
    if (!folio_isolate_lru(folio))
        return -EAGAIN;

    return 0;  // 成功隔离,可以迁移
}

迁移阶段 migrate_misplaced_folio()mm/migrate.c:2722-2748):

int migrate_misplaced_folio(struct folio *folio, int node)
{
    LIST_HEAD(migratepages);
    list_add(&folio->lru, &migratepages);

    nr_remaining = migrate_pages(&migratepages,
                                 alloc_misplaced_dst_folio,  // 在目标节点分配新 folio
                                 NULL,
                                 node,
                                 MIGRATE_ASYNC,    // 异步,不阻塞应用
                                 MR_NUMA_MISPLACED,
                                 &nr_succeeded);

    if (nr_succeeded) {
        count_vm_numa_events(NUMA_PAGE_MIGRATE, nr_succeeded);
        // Memory tiering 模式下,记录页提升成功(慢速 -> 快速内存)
        if (!list_empty(&migratepages) && nr_remaining) {
            count_vm_events(PGMIGRATE_FAIL, nr_remaining);
            folio_putback_lru(folio);
        }
    }
    return nr_remaining ? -EAGAIN : 0;
}

MIGRATE_ASYNC 模式:迁移过程中若遇到锁竞争,不等待而是跳过,避免阻塞应用。迁移的实际搬运通过 copy_folio_to() 完成(memcpy),然后更新页表指向新地址。

NUMA 内存迁移完整流程:

  用户态访问 PROT_NONE 页
         |
         v
  缺页中断 -> do_fault() -> do_numa_page()  [mm/memory.c:6048]
         |
         v
  numa_migrate_check(folio, ...)
  (确定目标节点 target_nid,含 memory tiering 热页判断)
         |
         v
  migrate_misplaced_folio_prepare()
  + 检查共享/脏页/内存水位
  + folio_isolate_lru()(从 LRU 移除,持有引用)
         |
         v
  pte_unmap_unlock() -- 释放 PTE 锁
         |
         v
  migrate_misplaced_folio(folio, target_nid)
  + alloc_misplaced_dst_folio()(目标节点分配内存)
  + copy_folio_to()(内容复制)
  + 更新所有 PTE 指向新 folio
  + 释放旧 folio
         |
         v
  task_numa_fault(last_cpupid, target_nid, nr_pages, TNF_MIGRATED)
  + 更新 p->numa_faults 统计
  + count_vm_numa_events(NUMA_PAGE_MIGRATE)

3.19 numa_group:共享内存分组机制

struct numa_groupkernel/sched/fair.c:1525-1544)是 AutoNUMA 处理共享内存场景的关键抽象。当多个任务(进程或线程)共同访问同一内存页时,它们的 NUMA 亲和性决策应该协同进行——孤立地优化单个任务可能导致组内成员分散到不同节点,反而增加共享内存的跨节点访问。

numa_group 合并了组内所有成员的 numa_faults 统计,使得:

  • 偏好节点计算基于组的整体访问模式
  • 迁移决策考虑对组整体的影响
  • 多线程程序的各线程倾向于聚合到同一节点

numa_is_active_node()(行 1751-1753):

static bool numa_is_active_node(int nid, struct numa_group *ng)
{
    // CPU 侧缺页超过最大值的 1/ACTIVE_NODE_FRACTION (1/3)
    return group_faults_cpu(ng, nid) * ACTIVE_NODE_FRACTION > ng->max_faults_cpu;
}

active_nodes 的更新在 numa_group_count_active_nodes()(行 2723-2741)中:

static void numa_group_count_active_nodes(struct numa_group *numa_group)
{
    // 找出 CPU 侧缺页最多的节点
    for_each_node_state(nid, N_CPU) {
        faults = group_faults_cpu(numa_group, nid);
        if (faults > max_faults) max_faults = faults;
    }
    // 计算超过最大值 1/3 的节点数
    for_each_node_state(nid, N_CPU) {
        faults = group_faults_cpu(numa_group, nid);
        if (faults * ACTIVE_NODE_FRACTION > max_faults)
            active_nodes++;
    }
    numa_group->max_faults_cpu = max_faults;
    numa_group->active_nodes = active_nodes;
}

active_nodes 的实际影响:

  • active_nodes == 1:组集中在一个节点,处于收敛状态,迁移趋于稳定
  • active_nodes > 1:组分散在多节点,继续主动搜索更好的目标节点(task_numa_migrate 行 2629)

3.20 task_numa_group:分组触发与合并

kernel/sched/fair.c:3076-3170,当缺页处理发现页被不同任务访问(!cpupid_match_pid())时调用:

分组逻辑分三种情况:

情况 1:任务尚未属于任何组 → 新建 numa_group,将自己的统计数据复制进去。

情况 2:发现对方任务也已有组,且两组不同 → 考虑合并。合并规则(行 3127-3134):

  • 小组加入大组(my_grp->nr_tasks <= grp->nr_tasks
  • 同等规模时,地址较小的组被合并(my_grp > grp 则自己加入对方)

这个规则确保了合并的单向性,避免死锁。

// 合并时统计迁移(行 3158-3164):
for (i = 0; i < NR_NUMA_HINT_FAULT_STATS * nr_node_ids; i++) {
    my_grp->faults[i] -= p->numa_faults[i];  // 从旧组移出
    grp->faults[i] += p->numa_faults[i];      // 加入新组
}
my_grp->total_faults -= p->total_numa_faults;
grp->total_faults += p->total_numa_faults;

情况 3:对方任务不在任何组 → 不加入(等待对方先建组或下次触发)。

numa_group 形成过程示意(双线程共享内存场景):

  时刻 T0:线程 A (pid=100) 首次访问 page_X(位于 node0)
  page_X.cpupid = encode(cpu=0, pid=100)

  时刻 T1:线程 B (pid=101) 访问同一 page_X(位于 node1 的 CPU 上)
  cpupid_match_pid(B, page_X.cpupid) → false(PID 不匹配)
  task_numa_group(B, cpupid, TNF_SHARED, &priv):
    B 无 group → 新建 group_B {gid=101, nr_tasks=1}
    查找 cpu=0 上当前任务 A:
    A 无 group → A 也尝试新建(在下次缺页时)

  时刻 T2:线程 A 再次访问 page_X
  A 新建 group_A {gid=100, nr_tasks=1}
  发现 B 已有 group_B,且 group_A.nr_tasks == group_B.nr_tasks
  若 group_A > group_B(地址比较)→ A 加入 group_B
  → 合并:A 的统计转移到 group_B,group_B.nr_tasks = 2

3.21 内存分层与 AutoNUMA 的交互

Linux 5.16 引入了 Memory Tiering(内存分层),通过 include/linux/memory-tiers.h 框架将 NUMA 节点按访问速度分层管理:

// include/linux/memory-tiers.h:21
#define MEMTIER_ADISTANCE_DRAM  ((4L * MEMTIER_CHUNK_SIZE) + (MEMTIER_CHUNK_SIZE >> 1))
// DRAM 的"抽象距离"基准值(≈ 576),adistance 越小表示速度越快

struct memory_dev_type {
    int adistance;          // 抽象距离,DRAM < CXL < PMEM
    nodemask_t nodes;       // 属于此类型的节点集合
};

node_is_toptier()include/linux/memory-tiers.h:58):判断节点是否属于最顶层(最快)内存:

bool node_is_toptier(int node);  // CONFIG_NUMA && CONFIG_MIGRATION 时为实现版本
static inline bool node_is_toptier(int node) { return true; }  // 降级版本

Memory Tiering 下 AutoNUMA 的行为变化:

普通 NUMA 模式(NUMA_BALANCING_NORMAL):
  节点 0 (DRAM)  <-->  节点 1 (DRAM)
  AutoNUMA 在所有节点间均衡

Memory Tiering 模式(NUMA_BALANCING_MEMORY_TIERING):
  节点 0 (DRAM,顶层)  <-->  节点 2 (CXL/PMEM,慢层)
                              |
                              v
  AutoNUMA 的额外任务:
  - 将慢层上的热页"提升"(promote)到 DRAM
  - 普通 NUMA 均衡只在顶层 DRAM 节点间进行
  - cpupid 字段复用为扫描时间戳

在 Memory Tiering 模式下(task_numa_fault() 行 3244-3247):

if (!node_is_toptier(mem_node) &&
    (sysctl_numa_balancing_mode & NUMA_BALANCING_MEMORY_TIERING ||
     !cpupid_valid(last_cpupid)))
    return;  // 慢层内存的缺页不做普通 NUMA 均衡统计

慢层内存(如 CXL 内存节点)的缺页由 numa_hint_fault_latency() 处理:通过 cpupid 字段记录的扫描时间戳计算"缺页延迟",延迟小的页被认为是热页,优先提升到 DRAM。

pgdat_free_space_enough()kernel/sched/fair.c:1886-1905):检查 DRAM 节点是否有足够空间,若剩余超过总容量的 1/16 或 1GB(取较大值),则激进地提升所有近期访问的慢层页,不依赖热页判定阈值。


3.22 Memory Tiering 下的 cpupid 重用

在 Memory Tiering 模式下,folio_last_cpupid 字段被复用来存储扫描时间戳:

普通 NUMA 模式:
  page._last_cpupid = encode(cpu_id, pid_fragment)
  用于区分私有/共享访问,触发 numa_group 建立

Memory Tiering 模式(慢层页):
  page._last_cpupid = scan_time_encoded
  用于 numa_hint_fault_latency() 计算热页
  hint_fault_latency = access_time - scan_time
  latency < hot_threshold → 热页 → 提升到 DRAM

numa_hint_fault_latency()kernel/sched/fair.c:1919 附近)通过读取 folio 中存储的扫描时间与当前时间的差值,判断页的"热度"。只有热度足够的页才会被提升(promote),避免频繁的冷热页抖动。


3.23 sysctl 参数调优参考

AutoNUMA 参数/proc/sys/kernel/ 路径下):

sysctl 名称 默认值 单位 含义
numa_balancing 1 0/1/2 全局开关(0=关, 1=普通 NUMA, 2=普通+内存分层)
numa_balancing_scan_period_min_ms 1000 ms 最小扫描周期
numa_balancing_scan_period_max_ms 60000 ms 最大扫描周期
numa_balancing_scan_size_mb 256 MB 每次扫描的物理内存量
numa_balancing_scan_delay_ms 1000 ms 新 VMA 首次扫描前的延迟
numa_balancing_hot_threshold_ms 1000 ms 热页判定阈值(memory tiering 模式)
numa_balancing_promote_rate_limit_MBps 65536 MB/s 慢层→快速内存的提升速率上限

EAS 参数

sysctl 名称 默认值 含义
sched_energy_aware 1 EAS 开关(0=强制关闭,1=自动使能)

numa_balancing 取值说明

// 来自 kernel/sched/core.c 中的宏定义(或等效):
// 0: NUMA_BALANCING_DISABLED        -- 完全关闭
// 1: NUMA_BALANCING_NORMAL          -- 传统 NUMA 均衡(任务 + 内存迁移)
// 2: NUMA_BALANCING_NORMAL |
//    NUMA_BALANCING_MEMORY_TIERING  -- 同时启用内存分层提升
//                                      (CXL/PMEM 的热页自动提升到 DRAM)

4. EAS 与 AutoNUMA 的协同分析

4.1 两者的边界与正交性

EAS 和 AutoNUMA 在设计上是互补但相互正交的机制,分别针对不同维度的调度问题:

两种机制的关注维度对比:

  维度          EAS                    AutoNUMA
  ─────────────────────────────────────────────────────
  优化目标      CPU 能耗               内存访问延迟
  决策时机      任务唤醒时             NUMA fault 触发时
  决策粒度      单次 CPU 选择          统计窗口内的访问模式
  内存感知      无(不感知 NUMA 拓扑)  核心功能
  CPU 感知      核心功能               任务迁移方向
  生效机制      影响 select_task_rq    直接触发任务/内存迁移
  典型平台      ARM mobile SoC         x86/ARM server
  启用条件      SD_ASYM_CPUCAPACITY    CONFIG_NUMA_BALANCING
  ─────────────────────────────────────────────────────

  注:两者均可在同一系统上同时启用(如多路 ARM 服务器)

两者均通过 kernel/sched/fair.c 实现,共享的数据结构包括:

  • task_struct.se(sched_entity,PELT 利用率跟踪)
  • rq(run queue,per-CPU 队列)
  • sched_domain 层次(决定搜索范围)

4.2 CPU 迁移路径的潜在干扰

AutoNUMA 通过 migrate_task_to() 直接移动任务到偏好节点的 CPU,这个操作绕过 EAS 的 select_task_rq 路径

// AutoNUMA 任务迁移路径(不走 EAS):
task_numa_migrate()
    -> migrate_task_to(p, env.best_cpu)   // 直接到目标 CPU
        -> stop_one_cpu(task_cpu(p), migration_cpu_stop, ...)

这可能导致一个有趣的场景:EAS 在唤醒时将任务放到了小核(节能选择),但之后 AutoNUMA 发现该任务的内存主要在另一节点的大核集群所在的 NUMA 节点,于是将任务迁移到大核上运行。从能效角度看,AutoNUMA 可能"撤销"了 EAS 的节能决策。

然而在大多数实际场景中,这种冲突不严重:

  1. big.LITTLE 系统通常是单 NUMA 节点(单芯片),AutoNUMA 不活跃
  2. 多路服务器通常是对称 CPU 容量(x86 或同类 ARM),EAS 不活跃
  3. 只有在多路异构 ARM 服务器(如 Ampere 的多 socket 配置)上两者才会同时活跃

4.3 update_scan_period:跨节点迁移后的扫描复位

kernel/sched/fair.c:3699-3731 提供了一个重要的协同机制:

static void update_scan_period(struct task_struct *p, int new_cpu)
{
    int src_nid = cpu_to_node(task_cpu(p));
    int dst_nid = cpu_to_node(new_cpu);

    if (!static_branch_likely(&sched_numa_balancing)) return;
    if (!p->mm || !p->numa_faults || (p->flags & PF_EXITING)) return;
    if (src_nid == dst_nid) return;   // 同节点内的 CPU 迁移不影响

    if (p->numa_scan_seq) {
        // 若迁移到偏好节点,或任务本就不在偏好节点,不重置
        if (dst_nid == p->numa_preferred_nid ||
            (p->numa_preferred_nid != NUMA_NO_NODE &&
             src_nid != p->numa_preferred_nid))
            return;
    }

    // 重置为最短扫描周期:迁移后快速重新建立内存访问基线
    p->numa_scan_period = task_scan_start(p);
}

这个函数在任务迁移(包括 EAS 引发的 CPU 迁移、负载均衡迁移)时被调用。当迁移跨越 NUMA 节点时,旧的缺页统计已不再准确,因此重置扫描周期,让 AutoNUMA 快速重新探测新节点上的内存亲和性。

这是 EAS 与 AutoNUMA 之间最重要的协同点:EAS 的调度决策会触发 AutoNUMA 的自适应响应。


4.4 ARM DSU 架构下的实践场景

ARM DynamIQ Shared Unit(DSU)是 ARMv8.2 引入的集群架构,允许在同一集群内混合不同类型的核心并共享 L3 缓存。

ARM DSU (DynamIQ) 架构示意:

  ┌────────────────────────────────────┐
  │         DSU Cluster                │
  │  ┌───────┐ ┌───────┐ ┌─────────┐  │
  │  │ A55×4 │ │ A78×3 │ │ A78-X×1 │  │
  │  │小核   │ │中核   │ │大核     │  │
  │  │cap≈400│ │cap≈900│ │cap≈1024 │  │
  │  └───┬───┘ └───┬───┘ └────┬────┘  │
  │      └─────────┴──────────┘        │
  │              L3 Cache              │
  │         (Shared, ~4-16MB)          │
  └────────────────────────────────────┘

  EAS 视角(三个 PerfDomain):
  PD0: A55×4   OPP: 500M-1.2GHz
  PD1: A78×3   OPP: 1.0G-2.4GHz
  PD2: A78-X×1 OPP: 1.5G-3.0GHz

  NUMA 视角:单 NUMA 节点(单 SoC),AutoNUMA 基本不活跃

在 DSU 架构下,EAS 特别受益于:

  1. 共享 L3 缓存:任务在小核/大核间迁移不会失去 L3 缓存(热数据依然可用),降低了迁移代价
  2. 精细的三层能量模型:A55/A78/A78-X 三个 PD 各有独立 EM,EAS 可以精细区分三档能效
  3. EM_PERF_DOMAIN_SKIP_INEFFICIENCIES:跳过功耗曲线上的低效 OPP 点,进一步降低能耗

4.5 异构 CPU + 异构内存的组合调度

在未来的系统中(例如多 socket ARM 服务器,每个 socket 内有 big.LITTLE,socket 间有 NUMA 距离,且部分 socket 附加了 CXL 内存),EAS 和 AutoNUMA 将同时面对三个维度的优化:CPU 类型(大/小核)、CPU 位置(NUMA 节点)、内存类型(DRAM/CXL)。

组合异构系统场景(未来方向):

  Socket 0 (NUMA node 0)          Socket 1 (NUMA node 1)
  ┌──────────────────────┐         ┌──────────────────────┐
  │ Big Core × 8         │  XGMI   │ Big Core × 8         │
  │ Little Core × 4      │ <-----> │ Little Core × 4      │
  │ Local DRAM: 64GB     │         │ Local DRAM: 64GB     │
  │ CXL Memory: 256GB    │         │ CXL Memory: 256GB    │
  │ (NUMA node 2)        │         │ (NUMA node 3)        │
  └──────────────────────┘         └──────────────────────┘

  调度优化层次:
  1. EAS:在 socket 内,倾向将轻负载任务放小核(节能)
  2. AutoNUMA:在两 socket 间,将任务及其内存拉向同一 socket(降延迟)
  3. Memory Tiering:将热数据从 CXL 提升到 DRAM(降带宽延迟)

  能效 vs NUMA 亲和性的权衡:
  若任务在 socket0 的小核运行,但内存在 socket1,
  AutoNUMA 会将任务迁移到 socket1 的 CPU(可能是大核)。
  EAS 在迁移后通过 update_scan_period 感知变化,下次唤醒时重新评估。

当前内核(6.x)尚未实现感知 NUMA 拓扑的 EAS 或感知 CPU 容量的 AutoNUMA。两者的协同优化仍是调度子系统的研究课题。


5. 调试与观测手段

EAS 调试

energy_model debugfs 接口

# 查看已注册的性能域列表
ls /sys/kernel/debug/energy_model/
# 典型输出:cpu0  cpu4  (对应两个 PD)

# 查看具体 OPP 的功耗和 cost(CPU 频率以 Hz 为单位)
cat /sys/kernel/debug/energy_model/cpu0/ps:500000/power   # 功耗(uW)
cat /sys/kernel/debug/energy_model/cpu0/ps:500000/cost    # cost 系数
cat /sys/kernel/debug/energy_model/cpu0/ps:500000/performance  # 性能容量

# 确认 EAS 是否启用
cat /proc/sys/kernel/sched_energy_aware        # 用户层开关
# 确认 schedutil 是当前 governor
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor

关键 tracepoint

# 追踪 EAS 能耗计算(find_energy_efficient_cpu 中每次 compute_energy 调用)
echo 1 > /sys/kernel/tracing/events/sched/sched_compute_energy_tp/enable

# 追踪 overutilized 状态变化(EAS 进入/退出)
echo 1 > /sys/kernel/tracing/events/sched/sched_overutilized_tp/enable

# 查看任务放置结果(需要 sched_wakeup + sched_migrate_task)
echo 1 > /sys/kernel/tracing/events/sched/sched_wakeup/enable
echo 1 > /sys/kernel/tracing/events/sched/sched_migrate_task/enable

通过 perf sched 分析 EAS 效果

# 记录调度事件
perf sched record -a -- sleep 10

# 分析 CPU 迁移模式
perf sched map | grep -E "cpu[0-3]|cpu[4-7]"
# 观察轻负载任务是否集中在小核(EAS 有效时应如此)

通过 powertop 验证功耗改善

# 对比 EAS 开启/关闭的包级功耗
echo 0 > /proc/sys/kernel/sched_energy_aware
powertop --time=10 > powertop_eas_off.txt

echo 1 > /proc/sys/kernel/sched_energy_aware
powertop --time=10 > powertop_eas_on.txt

AutoNUMA 调试

进程级 NUMA 统计

# 查看进程的 NUMA 内存分布(各 VMA 在哪个节点上)
cat /proc/<pid>/numa_maps
# 输出示例(每行一个 VMA):
# 7f1234560000 default anon=1024 dirty=256 N0=800 N1=224 kernelpagesize_kB=4

# 查看进程级 NUMA 统计
numastat -p <pid>
# 输出:
# Per-node process memory usage (in MBs) for PID 1234 (myapp)
#                  Node 0    Node 1    Total
#                 ------    ------    -----
# Private              345       12      357
# Shared                56      156      212
# Total                401      168      569

系统级 NUMA 事件统计

# 查看 NUMA 相关的 vmstat 计数器
grep -E "^numa" /proc/vmstat
# 关键字段:
# numa_hit           3456789   <- 分配在本地节点(理想状态)
# numa_miss          12345     <- 分配在远端节点
# numa_foreign       456       <- 预期本地但分配到远端
# numa_interleave    0         <- 交织分配的页数
# numa_local         3456789   <- 本地访问次数(等同 numa_hit)
# numa_other         12345     <- 远端访问次数

# Memory Tiering 相关
grep -E "pgpromote|pgdemote" /proc/vmstat
# pgpromote_success  1234      <- 慢层 -> DRAM 提升成功
# pgdemote_kswapd    567       <- DRAM -> 慢层 降级(kswapd 发起)

AutoNUMA 扫描与迁移的 tracepoint

# 追踪 VMA 扫描跳过原因
echo 1 > /sys/kernel/tracing/events/sched/sched_skip_vma_numa/enable
# 输出字段包括:vma_start, vma_end, skip_reason (NUMAB_SKIP_*)

# 追踪任务无法迁移("粘住"在错误节点)
echo 1 > /sys/kernel/tracing/events/sched/sched_stick_numa/enable

# 追踪 cpuset 限制导致的跳过
echo 1 > /sys/kernel/tracing/events/sched/sched_skip_cpuset_numa/enable

# 追踪 NUMA 页迁移
echo 1 > /sys/kernel/tracing/events/migrate/mm_migrate_pages/enable

通过 perf stat 观察 NUMA 访问率

# 需要 PMU 支持 NUMA 访问事件(AMD EPYC / Intel 服务器)
perf stat -e \
  cpu/event=0xE9,umask=0x1,name=local_mem_access/,   \
  cpu/event=0xEA,umask=0x1,name=remote_mem_access/   \
  -p <pid> sleep 10

# 计算 NUMA 远端访问比例:
# remote_ratio = remote_mem_access / (local_mem_access + remote_mem_access)
# 理想值应 < 5%

debugfs NUMA 均衡参数实时查看kernel/sched/debug.c:618-626):

# 通过 debugfs 查看当前 NUMA 均衡配置(比 sysctl 更详细)
ls /sys/kernel/debug/sched/numa_balancing/
# 包含 scan_period_min, scan_period_max, scan_size, hot_threshold_ms 等

6. 性能影响与调优建议

EAS 调优

场景 1:延迟敏感型应用(音频、交互 UI)

EAS 在系统轻负载时倾向将小任务打包到小核,可能导致大核进入深 C 状态,引入唤醒延迟(几十到几百微秒)。若延迟影响用户体验:

# 方案 A:通过 uclamp_min 强制任务跑在高性能 CPU 上
# cgroup v2 方式
echo "512" > /sys/fs/cgroup/<group>/cpu.uclamp.min  # 50% 容量下限

# 方案 B:直接 CPU 亲和性绑定(绕过 EAS)
taskset -c 4-7 <your_app>

# 方案 C:调整 schedutil 的 rate_limit_us(减少频率响应延迟)
echo "500" > /sys/devices/system/cpu/cpufreq/policy0/schedutil/rate_limit_us

场景 2:吞吐型应用(视频编码、编译)

此类应用通常使系统进入 overutilized 状态,EAS 自动退出(rd->overutilized = 1),切换到传统负载均衡,无需特别干预。确认方式:

# 通过 tracepoint 确认 overutilized 状态
echo 1 > /sys/kernel/tracing/events/sched/sched_overutilized_tp/enable
cat /sys/kernel/tracing/trace | grep sched_overutilized
# 输出:sched_overutilized: rd=... overutilized=1

场景 3:功耗优化(移动/IoT 设备)

确认 EAS 完整工作:

# 步骤 1:验证 EM 已注册
ls /sys/kernel/debug/energy_model/
# 步骤 2:验证 schedutil 生效
cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor | sort -u
# 应全部为 schedutil
# 步骤 3:验证频率不变性
# (无直接 sysfs 接口,通过 dmesg 查找 "frequency-invariant load tracking" 日志)
# 步骤 4:验证 EAS 已启用
cat /proc/sys/kernel/sched_energy_aware  # 应为 1

场景 4:芯片功耗数据不准确(EM_PERF_DOMAIN_ARTIFICIAL)

# 检查 EM 是否使用人工功耗数据
# 若标志中含 EM_PERF_DOMAIN_ARTIFICIAL,说明平台未提供真实功耗信息
# 此时 EAS 的能耗估算可能不准,考虑关闭
echo 0 > /proc/sys/kernel/sched_energy_aware

AutoNUMA 调优

场景 1:大型 HPC / MPI 应用

MPI 进程通常已通过 numactl --membind 手动绑定内存,AutoNUMA 的扫描是额外开销:

# 对已手动 NUMA 绑定的进程,直接关闭 AutoNUMA
echo 0 > /proc/sys/kernel/numa_balancing

# 或针对特定进程关闭(Linux 5.7+)
# 通过 madvise(MADV_NOHUGEPAGE) 或 mbind(MPOL_BIND) 提示内核不迁移

# 若只想减少干扰而不完全关闭,增大扫描粒度:
echo 512 > /proc/sys/kernel/numa_balancing_scan_size_mb
echo 5000 > /proc/sys/kernel/numa_balancing_scan_period_min_ms
echo 120000 > /proc/sys/kernel/numa_balancing_scan_period_max_ms

场景 2:数据库(PostgreSQL / MySQL / Redis)

共享内存密集型,AutoNUMA 的 numa_group 机制对多线程数据库特别有效:

# 保持 AutoNUMA 启用(默认)
# 若有 CXL/PMEM,启用 memory tiering 提升热数据
echo 2 > /proc/sys/kernel/numa_balancing

# 确认 numa_group 已在多线程间建立(通过 /proc 查看)
cat /proc/<db_pid>/status | grep Numa
# 输出:Numa_pages_migrated: 12345
# NumaGroup: <non-zero> 表示已有 group

场景 3:虚拟化宿主机(KVM / QEMU)

每个 VM 的 vCPU 线程之间共享内存有限,AutoNUMA 的 numa_group 可能把不相关的 VM 的 vCPU 错误聚合:

# 增大扫描粒度以减少误判
echo 1024 > /proc/sys/kernel/numa_balancing_scan_size_mb
echo 3000 > /proc/sys/kernel/numa_balancing_scan_period_min_ms

# 对于 NUMA 感知的 VM(通过 QEMU 的 -numa 参数配置),
# 建议结合 numactl + CPU pinning 确保 VM 的 vCPU 和 memory 在同一 node

场景 4:CXL 内存系统(Memory Tiering)

# 启用 memory tiering 模式
echo 2 > /proc/sys/kernel/numa_balancing

# 调整热页阈值(较小值 = 更激进的提升,更多 DRAM 压力)
echo 500 > /proc/sys/kernel/numa_balancing_hot_threshold_ms  # 500ms

# 控制提升速率(避免提升流量冲击 DRAM 带宽)
echo 2048 > /proc/sys/kernel/numa_balancing_promote_rate_limit_MBps

# 监控提升效果
watch -n 1 'grep -E "pgpromote|pgdemote" /proc/vmstat'

两者同时调优

在同时需要 EAS 和 AutoNUMA 的系统(如多路 ARM 服务器,每 socket 内有 big.LITTLE)上:

调优优先级建议:

  步骤 1:确认 NUMA 拓扑和距离
    numactl --hardware
    关注:node distances 矩阵,远端/本地延迟比

  步骤 2:建立基线
    perf stat -e cache-misses,node-load-misses,instructions -a sleep 30
    记录 NUMA miss 率

  步骤 3:若 node-load-misses / node-loads > 10%
    → AutoNUMA 优先:降低 scan_period_min,增加 scan_size
    → 用 numastat 确认迁移效果

  步骤 4:若电池 / 功耗是瓶颈
    → EAS 调优:确认 schedutil + EM 注册正确
    → 通过 powertop 量化节能效果

  步骤 5:周期性评估
    不同工作负载对两者的需求不同,
    生产环境建议针对代表性 workload 做 A/B 测试

7. 附录:关键函数速查表

函数名 文件:行号 功能简述
em_cpu_energy() energy_model.h:243 估算性能域能耗(内联,热路径)
em_pd_get_efficient_state() energy_model.h:204 找满足利用率的最低有效 OPP
em_dev_register_perf_domain() energy_model.h:175 注册设备能量模型(驱动入口)
em_dev_update_chip_binning() energy_model.h:186 运行时更新芯片功耗数据
find_energy_efficient_cpu() fair.c:8388 EAS 唤醒路径主函数
compute_energy() fair.c:8331 估算放置到特定 CPU 的能耗增量
eenv_task_busy_time() fair.c:8226 计算任务忙碌时间(含 IRQ 缩放)
eenv_pd_busy_time() fair.c:8261 计算性能域基线忙碌时间
eenv_pd_max_util() fair.c:8284 计算放置任务后的最大域利用率
get_actual_cpu_capacity() fair.c:4981 获取扣除压力后的实际 CPU 容量
is_rd_overutilized() fair.c:~6871 判断 root domain 是否过载
arch_scale_cpu_capacity() sched/topology.h:213 获取 CPU 架构静态容量
sched_energy_enabled() sched/sched.h:3702 检查 EAS 静态 key 是否使能
task_numa_fault() fair.c:3224 NUMA 缺页时记录访问统计(被 mm 层回调)
task_numa_work() fair.c:3362 周期性 PROT_NONE 扫描主函数
task_tick_numa() fair.c:3668 scheduler tick 中触发 NUMA 扫描判断
task_numa_placement() fair.c:2951 根据缺页统计确定偏好 NUMA 节点
task_numa_migrate() fair.c:2561 迁移决策(单向迁移或双向交换)
numa_migrate_preferred() fair.c:2697 周期性尝试迁移到偏好节点
task_numa_group() fair.c:3076 检测共享内存并建立/合并 numa_group
numa_group_count_active_nodes() fair.c:2723 更新 group 的活跃节点计数
task_weight() fair.c:1828 任务对节点的亲和性权重(千分比)
group_weight() fair.c:1847 任务组对节点的亲和性权重
score_nearby_nodes() fair.c:1757 非直连拓扑的邻近节点加权
task_scan_min() fair.c:1586 计算最小扫描周期
task_scan_start() fair.c:1600 计算初始扫描周期(含共享加权)
task_scan_max() fair.c:1622 计算最大扫描周期
update_scan_period() fair.c:3699 跨 NUMA 迁移后重置扫描周期
vma_is_accessed() fair.c:3319 检查 VMA 是否被当前 PID 近期访问
numa_is_active_node() fair.c:1751 判断节点是否为 group 活跃节点
do_numa_page() mm/memory.c:6048 NUMA hint 缺页处理主函数
migrate_misplaced_folio() mm/migrate.c:~2722 将 folio 迁移到目标 NUMA 节点
migrate_misplaced_folio_prepare() mm/migrate.c:~2657 迁移前检查与 LRU 隔离
node_is_toptier() memory-tiers.h:58 判断节点是否为顶层(快速)内存
pgdat_free_space_enough() fair.c:1886 检查 DRAM 节点空间是否充足

由 Claude Code 分析生成


8. EAS 与热管理的深度协作

8.1 热压力(Thermal Pressure)传导链

EAS 能效估算的准确性依赖 CPU 容量的精确度。当系统过热发生热限频(thermal throttling)时,CPU 的实际可用容量低于架构额定值,EAS 必须感知这一变化,否则会将任务放到已经限频的 CPU 上,导致能耗估算错误甚至性能下降。

热压力的传导链(从硬件到调度器):

硬件温度传感器上报温度超限
          |
          v
thermal framework 触发 thermal zone 轮询
(drivers/thermal/thermal_core.c)
          |
          v
IPA governor (gov_power_allocator.c:406)
  allocate_power() 计算可用功率预算
          |
          v
cpufreq cooling device 收到功率约束
  cpufreq_power2state()  [cpufreq_cooling.c:314]
  -> 将功率限制转换为目标频率
  -> cpufreq_set_policy() 降低 policy->max
          |
          v
cpufreq_update_pressure()  [drivers/cpufreq/cpufreq.c:2586]
  pressure = max_cap - (max_cap * capped_freq / max_freq)
  WRITE_ONCE(per_cpu(cpufreq_pressure, cpu), pressure)
          |
          v
调度器读取 cpufreq_get_pressure(cpu)  [include/linux/cpufreq.h:258]
  = READ_ONCE(per_cpu(cpufreq_pressure, cpu))
          |
          v
get_actual_cpu_capacity(cpu)  [kernel/sched/fair.c:4981]
  capacity -= max(hw_load_avg(rq), cpufreq_get_pressure(cpu))
          |
          v
EAS compute_energy() 使用修正后容量做能耗估算

cpufreq_update_pressure() 的计算逻辑(drivers/cpufreq/cpufreq.c:2586-2610):

static void cpufreq_update_pressure(struct cpufreq_policy *policy)
{
    unsigned long max_capacity, capped_freq, pressure;
    u32 max_freq;
    int cpu;

    cpu = cpumask_first(policy->related_cpus);
    max_freq = arch_scale_freq_ref(cpu);   // 架构最大频率(EM 基准频率)
    capped_freq = policy->max;             // 当前被限制的最大频率

    if (max_freq <= capped_freq) {
        pressure = 0;  // 未限频,无压力
    } else {
        max_capacity = arch_scale_cpu_capacity(cpu);
        // 压力 = 损失的容量 = max_cap * (1 - capped_freq/max_freq)
        pressure = max_capacity -
                   mult_frac(max_capacity, capped_freq, max_freq);
    }

    for_each_cpu(cpu, policy->related_cpus)
        WRITE_ONCE(per_cpu(cpufreq_pressure, cpu), pressure);
}

这个压力值反映了"因热限频损失的容量",例如大核最高 2.6GHz 被限到 1.3GHz,损失 50% 容量,cpufreq_pressure = 512(满容量 1024 的一半)。


8.2 IPA(Intelligent Power Allocator)工作机制

IPA 是 Linux 热管理框架中用于"功率感知调温"的核心 governor,定义在 drivers/thermal/gov_power_allocator.c。与简单的 step_wise governor(直接切 cooling state)不同,IPA 以功率为单位进行分配,能够在多个 cooling device 之间智能权衡。

IPA 数据结构

struct power_allocator_paramsgov_power_allocator.c:86-98):

struct power_allocator_params {
    bool allocated_tzp;         // 是否由 IPA 自动分配了 tzp(热区参数)
    bool update_cdevs;          // 下次运行时是否需要更新 cooling devices
    s64  err_integral;          // PID 积分项累积值
    s32  prev_err;              // 上一次误差(用于微分项计算)
    u32  sustainable_power;     // 热区可持续散热功率(mW)
    const struct thermal_trip *trip_switch_on;  // 第一个被动触发点
    const struct thermal_trip *trip_max;        // 控温目标触发点
    int  total_weight;          // 所有 cooling device 实例权重之和
    unsigned int num_actors;    // 支持 IPA 回调的 cooling device 数量
    unsigned int buffer_size;   // 功率缓冲区大小
    struct power_actor *power;  // 各 actor 功率信息缓冲区
};

sustainable_power 是 IPA 的核心参数,代表热区在稳定状态下可以持续散出的最大热量(即 CPU 可以持续运行的最大功率)。它既可由 DTS 中 sustainable-power 属性配置,也可通过 estimate_sustainable_power() 自动估算(gov_power_allocator.c:116)。

IPA PID 控制器

pid_controller()gov_power_allocator.c:239-299)是 IPA 的核心算法:

PID 控制目标:维持温度 = control_temp(最高被动触发点温度)

误差:err = control_temp - tz->temperature
      > 0: 温度低于目标,可给予更多功率
      < 0: 温度超过目标,需降低功率

PID 输出:
  P 项 = k_po * err  (降温时)  或  k_pu * err  (升温时)
  I 项 = k_i * err_integral     (消除稳态误差)
  D 项 = k_d * (err - prev_err) / passive_delay  (抑制震荡)

最终功率预算:
  power_range = sustainable_power + P + I + D
  power_range = clamp(power_range, 0, max_allocatable_power)

使用两套比例系数(k_po 超调、k_pu 欠调)的设计原因:超温时需要快速响应(更激进降频),欠温时可以更保守地恢复(避免频繁升降频导致的性能抖动)。estimate_pid_constants() 的默认估算(gov_power_allocator.c:149-178):

tz->tzp->k_po = int_to_frac(sustainable_power) / temperature_threshold;
tz->tzp->k_pu = int_to_frac(2 * sustainable_power) / temperature_threshold;
tz->tzp->k_i  = k_pu / 10;
// k_pu = 2 * k_po,意味着升温响应比降温更激进

IPA 功率分配:divvy_up_power

divvy_up_power()gov_power_allocator.c:350-404)将总功率预算按比例分配给各 cooling actor:

各 actor 分配逻辑(比例分配 + 剩余再分配):

  total_req_power = sum(actor.weighted_req_power)
  for each actor:
    actor.granted_power = power_range * actor.weighted_req_power
                         / total_req_power

  # 若某 actor granted > max_power(其最低冷却态功率),
  # 则 capped 到 max_power,多余部分再按比例分给其他 actor
  # 这个过程迭代直到没有溢出

weighted_req_power = weight * req_power,权重由 DTS 中 contribution 属性(cooling-device 的 weight 参数)配置,默认所有 actor 权重相等(1 << FRAC_BITS)。

IPA 与 EAS 的功率模型对齐

IPA 的 cpufreq_power2state() 通过能量模型的 em_perf_state 将功率约束转换为频率限制(cpufreq_cooling.c:314-329):

static int cpufreq_power2state(struct thermal_cooling_device *cdev,
                               u32 power, unsigned long *state)
{
    u32 last_load, normalised_power;
    struct cpufreq_cooling_device *cpufreq_cdev = cdev->devdata;

    last_load = cpufreq_cdev->last_load ?: 1;
    // 将"总功率"还原为"100%负载时的功率"(消除负载影响)
    normalised_power = (power * 100) / last_load;
    // 查 EM 表找对应频率
    target_freq = cpu_power_to_freq(cpufreq_cdev, normalised_power);
    *state = get_level(cpufreq_cdev, target_freq);
    return 0;
}

cpufreq_state2power() 实现反向转换(cpufreq_cooling.c:274-297):通过 RCU 读取 em_perf_state 表,将冷却 state(实际是 OPP 索引的倒序,state=0 对应最高频率)转换为对应功率。

两者共用同一份 em_perf_domain 数据,这使得 IPA 的功率预算单位与 EAS 的能耗估算单位完全一致——都基于同一套 OPP 功耗数据,保证了系统整体行为的一致性。

IPA 与 EAS 的共同 EM 基础:

  em_perf_domain (大核集群)
  +------------------------------------------+
  |  state[0]: 500MHz  power=200mW  cost=...  |
  |  state[1]: 1.0GHz  power=500mW  cost=...  |
  |  state[2]: 1.8GHz  power=1200mW cost=...  |
  |  state[3]: 2.6GHz  power=2800mW cost=...  |
  +------------------------------------------+
           |                    |
    IPA 使用                 EAS 使用
  cpufreq_power2state()    em_cpu_energy()
  cpufreq_state2power()    compute_energy()
  (功率 <-> 频率状态)       (利用率 -> 能耗)

8.3 热管理对 EAS 的影响路径分析

热管理通过两条路径影响 EAS 的决策:

路径 1:通过 cpufreq_pressure 调整候选 CPU 的有效容量

当大核被热限频后,cpufreq_pressure 增大,get_actual_cpu_capacity() 返回的有效容量降低。在 find_energy_efficient_cpu() 中(kernel/sched/fair.c:8440):

cpu_actual_cap = get_actual_cpu_capacity(cpu);  // 含热压力修正
// ...
eenv.cpu_cap = cpu_actual_cap;
eenv.pd_cap  = cpu_actual_cap * nr_cpus_pd;

这会影响两方面:

  • em_cpu_energy() 中的 allowed_cpu_cap 参数被替换为热压力修正后的容量
  • util_fits_cpu() 判断任务是否适合该 CPU 时,使用修正后的容量做比较

结果:热限频的大核在 EAS 眼中"变小"了,轻任务会被引导到小核,减少大核的负载,有助于大核降温。

路径 2:通过 em_update_performance_limits 收窄 OPP 范围

em_update_performance_limits()include/linux/energy_model.h:187)允许在运行时动态调整 min_perf_state / max_perf_state,将热限范围反映到 EAS 的 OPP 搜索范围:

int em_update_performance_limits(struct em_perf_domain *pd,
                                 unsigned long freq_min,
                                 unsigned long freq_max);
// 当热管理将频率限制在 [freq_min, freq_max] 时,
// min/max_perf_state 被更新,EAS 不会再考虑范围外的 OPP

em_pd_get_efficient_state() 使用 min_perf_state 作为起始搜索点(energy_model.h:209-210),确保 EAS 在超温时不会"虚假地"估算使用高频 OPP 的能耗。


8.4 热节流下 EAS 的降级行为

当系统深度过热时,热管理可能将所有 CPU 限到同一低频,此时:

  1. 所有 CPU 的 cpufreq_pressure 均大幅增加
  2. 大核和小核的有效容量差距缩小
  3. SD_ASYM_CPUCAPACITY 标志在容量趋同后可能失去意义
  4. sched_is_eas_possible() 重新检查 any_asym_capacity

在极端情况下(如所有核心被限到相同频率),EAS 的非对称容量前提不再成立,build_perf_domains() 可能返回 false,sched_energy_set(false) 禁用 EAS,系统回退到传统对称负载均衡。

EAS 在热节流下的退化路径:

  正常状态: A55 cap=410, A78 cap=1024 (非对称)
       |
       | 温度升高 → 热限频
       v
  限频状态: A55 cap=400 (轻限), A78 cap=500 (重限)
  仍非对称 → EAS 继续工作,只是能耗估算基于更低容量
       |
       | 温度继续上升 → 深度限频
       v
  极限状态: A55 cap=300, A78 cap=320 (近乎对称)
  capacity_greater(A78, A55) 判断: 320*1024 > 300*1078? → 约等于 → false
  sched_is_eas_possible() 可能返回 false
  → EAS 被禁用,退回 sched_balance_find_dst_cpu()

9. EAS 能量模型的运行时更新

9.1 芯片 binning 与 em_dev_update_chip_binning

ARM SoC 在出厂时通过"binning"(芯片测试分级)确定每颗芯片的实际功耗特性。同一批次的芯片功耗可能差异 10-20%,但 DTS 中的 OPP 表通常给出最保守(最高功耗)的值。Linux 6.1 引入了运行时更新机制允许修正这一偏差。

em_dev_update_chip_binning()include/linux/energy_model.h:186):

// 通过读取平台特定的功耗特征数据(如 OPP 表中的 opp-microwatt)
// 重新构建 em_perf_table,并通过 RCU 热更新
int em_dev_update_chip_binning(struct device *dev);

更新流程(kernel/power/energy_model.cem_dev_update_chip_binning 实现):

1. 从设备 OPP 表读取各频率点的实测功耗
2. 分配新的 em_perf_table(包含 em_perf_state 数组)
3. 重新计算 cost = power * max_freq / freq / scale_cpu
4. 重新标记 EM_PERF_STATE_INEFFICIENT
5. rcu_assign_pointer(pd->em_table, new_table)
   - 旧表通过 kref_put 延迟释放(等所有读者离开 RCU 临界区)
6. 通知调度器重建性能域:rebuild_sched_domains_energy()

RCU 保护的必要性:调度器热路径(compute_energy() -> em_cpu_energy())在持有 RCU 读锁时访问 em_table,更新时不需要停机。

// em_cpu_energy 中的安全访问(energy_model.h:270):
rcu_read_lock();
table = rcu_dereference(pd->em_table)->state;
// ... 使用 table 计算能耗
rcu_read_unlock();

9.2 em_perf_state 的 inefficiency 标记机制

em_compute_costs()kernel/power/energy_model.c 中)在构建 EM 时检测低效 OPP:

低效 OPP 判断规则:
  若 state[i].cost >= state[j].cost(j > i,更高频率 OPP)
  则 state[i] 被标记为 EM_PERF_STATE_INEFFICIENT

示意(某平台真实 OPP 曲线):
  频率(MHz)  功耗(mW)  性能  cost = power/perf
  500        100       400   0.250  ← 正常
  800        200       640   0.313  ← 正常
  1000       400       800   0.500  ← 正常
  1100       600       880   0.682  ← 低效!cost > 1200MHz 的 0.583
  1200       700       960   0.729  ← 低效!cost > 1400MHz 的 0.600
  1400       840       1024  0.820  ← 若跳过 1100/1200,从 1000 直接到此

  标记为 INEFFICIENT 的 OPP 在启用 SKIP_INEFFICIENCIES 后被跳过:
  EAS 不会在这些频率点运行,CPU 会在更高频率(更低 cost)下运行,
  实际上反而更省能(避免了低效工作点)。

这种情况在 ARM GPU 或某些 SoC 的中频段尤为常见,是物理约束(如电源轨共享、电压域耦合)造成的功耗曲线非单调性。


10. schedutil 与 EAS 协作的深层机制

10.1 sugov_effective_cpu_perf 详解

sugov_effective_cpu_perf()kernel/sched/cpufreq_schedutil.c:209-224)是 schedutil 和 EAS 的共享频率预测函数:

unsigned long sugov_effective_cpu_perf(int cpu, unsigned long actual,
                                unsigned long min, unsigned long max)
{
    // 1. 添加 DVFS 响应余量(map_util_perf 通常加 25% 余量)
    actual = map_util_perf(actual);

    // 2. 若带余量的值仍低于 uclamp_max,限制到 uclamp_max
    if (actual < max)
        max = actual;

    // 3. 至少保证 uclamp_min
    return max(min, max);
}

map_util_perf() 的余量设计:将利用率放大(通常是 util * 5/4,即 25% 余量),确保在频率响应滞后期间 CPU 不会过载。这个余量与 fits_capacity() 的 20% headroom 来自同一设计考虑,但方向相反:一个是放大利用率请求,一个是收缩容量判断阈值。

eenv_pd_max_util()kernel/sched/fair.c:8319)中:

eff_util = sugov_effective_cpu_perf(cpu, eff_util, min, max);

EAS 使用与 schedutil 完全相同的 sugov_effective_cpu_perf 函数预测最终请求的性能点,确保 EAS 选核预测的 OPP 就是 schedutil 实际会请求的 OPP,使能耗估算的误差最小化。

10.2 IO 等待提升(iowait boost)对 EAS 的影响

schedutil 通过 sugov_iowait_apply()cpufreq_schedutil.c:326-340)为 IO 密集型任务提供额外的频率提升(iowait boost),防止 IO 唤醒后的 CPU 响应延迟:

static unsigned long sugov_iowait_apply(struct sugov_cpu *sg_cpu, u64 time,
                                        unsigned long max_cap)
{
    // iowait_boost 从 IOWAIT_BOOST_MIN 开始,每次 IO 唤醒倍增
    // 直到 iowait_boost_max(= sg_policy->freq_util_max)
    unsigned long boost = sg_cpu->iowait_boost;
    if (!boost)
        return 0;
    // boost 每个 tick 衰减(若超过 tick 未有 IO 事件则清零)
    if (sg_cpu->iowait_boost_pending) {
        sg_cpu->iowait_boost_pending = false;
    } else {
        boost >>= 1;  // 指数衰减
        if (boost < IOWAIT_BOOST_MIN) {
            sg_cpu->iowait_boost = 0;
            return 0;
        }
        sg_cpu->iowait_boost = boost;
    }
    return boost;
}

iowait boost 通过 sugov_get_util() 中的 boost 参数传入利用率计算,会暂时提升 CPU 频率请求。这对 EAS 的影响:EAS 在 eenv_pd_max_util() 中也考虑了 iowait boost(通过 sugov_effective_cpu_perf),使得对 IO 密集任务的能耗估算更接近实际调频行为。

10.3 schedutil 的 rate_limit_us 与 EAS 精度

rate_limit_us(默认 2 * MSEC_PER_SEC / CONFIG_HZ,约 4ms on 250Hz 内核)是 schedutil 对频率更新的限频参数,防止频率过于频繁变化影响硬件寿命和功耗。

该参数通过 sysfs 暴露:

/sys/devices/system/cpu/cpufreq/policy<N>/schedutil/rate_limit_us

当 rate_limit_us 较大时,schedutil 对负载变化的响应滞后,EAS 的能耗预测与实际行为的偏差可能增大(EAS 基于当前瞬时利用率做选核,但 schedutil 可能延迟变频)。调优建议:

  • 延迟敏感场景:降低 rate_limit_us(如 500us),提高频率响应速度,EAS 预测更准
  • 功耗优先场景:保持默认或适当增大,减少频率切换损耗

11. NUMA Balancing 深度机制分析

11.1 numa_type 与节点分类

task_numa_find_cpu() 搜索目标 CPU 时,AutoNUMA 首先对目标节点进行分类,决定搜索策略(kernel/sched/fair.c:2093-2156):

enum numa_type {
    node_has_spare = 0,   // 节点有空闲容量,任务可以直接迁入
    node_fully_busy,      // 节点满负荷但不过载,迁入需要置换
    node_overloaded       // 节点过载,不建议迁入
};

numa_classify() 函数(fair.c:2142-2156)基于节点负载判断分类:

static inline enum numa_type numa_classify(unsigned int imbalance_pct,
                                            struct numa_stats *ns)
{
    // 过载条件:任务数 > CPU 数 且 (util > capacity*100/pct 或 runnable > ...)
    if ((ns->nr_running > ns->weight) &&
        (((ns->compute_capacity * 100) < (ns->util * imbalance_pct)) ||
         ((ns->compute_capacity * imbalance_pct) < (ns->runnable * 100))))
        return node_overloaded;

    // 空闲条件:任务数 < CPU 数 且利用率低
    if ((ns->nr_running < ns->weight) ||
        (((ns->compute_capacity * 100) > (ns->util * imbalance_pct)) &&
         ((ns->compute_capacity * imbalance_pct) > (ns->runnable * 100))))
        return node_has_spare;

    return node_fully_busy;
}

imbalance_pct = 112(初始值)意味着允许 12% 的负载不均,避免因轻微不平衡触发不必要的迁移。

11.2 numa_stats:节点负载快照

struct numa_statsfair.c:2109-2119)是 AutoNUMA 在迁移决策时对节点状态的快照:

struct numa_stats {
    unsigned long load;             // 节点总负载(load_avg 之和)
    unsigned long runnable;         // 可运行任务利用率之和
    unsigned long util;             // CPU 利用率之和
    unsigned long compute_capacity; // 节点总计算容量(cpu_cap * nr_cpus)
    unsigned int  nr_running;       // 节点上正在运行的任务数
    unsigned int  weight;           // 节点 CPU 数(调度 weight)
    enum numa_type node_type;       // 节点分类(spare/busy/overloaded)
    int idle_cpu;                   // 节点上的空闲 CPU(-1 = 无)
};

task_numa_find_cpu() 中对目标节点的 CPUs 遍历(fair.c:2506),对每个 CPU 计算:

  • 若是空闲 CPU:直接视为最优候选(idle_cpu 字段记录)
  • 若非空闲:评估当前运行任务与目标任务的双向收益(任务交换逻辑)

11.3 SMALLIMP 阈值与迁移代价控制

SMALLIMP = 30fair.c:2309)是迁移收益的最小有效门槛:

// fair.c:2309
#define SMALLIMP 30

// 迁移过滤逻辑(fair.c:2445):
if (imp < SMALLIMP || imp <= env->best_imp + SMALLIMP / 2)
    goto unlock;

imp 的来源:task_weight(p, dst_nid, dist) - task_weight(p, src_nid, dist),单位是千分比。SMALLIMP = 30 意味着至少要有 3% 的迁移收益才值得迁移。这个阈值防止了因微小统计波动导致的任务频繁抖动。

当且仅当满足以下条件时才更新候选:

  1. imp >= SMALLIMP(绝对收益足够大)
  2. imp > env->best_imp + SMALLIMP/2(比当前最优候选好至少 1.5%)

11.4 任务交换(task swap)的决策逻辑

AutoNUMA 的任务交换机制(fair.c:2360-2445):当目标节点上有正在运行的任务,AutoNUMA 尝试双向交换而非单向迁移。

任务交换收益计算:

  任务 p 在 src_nid 运行,mem 主要在 dst_nid
  目标 CPU 上任务 cur 在 dst_nid 运行,mem 主要在 src_nid

  移动 p 到 dst_nid 的收益:
    moveimp = task_weight(p, dst_nid) - task_weight(p, src_nid)

  交换(p <-> cur)的综合收益:
    swapimp  = moveimp
    swapimp += task_weight(cur, src_nid) - task_weight(cur, dst_nid)
    // cur 从 dst_nid 移到 src_nid,cur 的收益也加进来

  若 swapimp > moveimp(交换比单向迁移收益更大)
    且 swapimp > env->best_imp
  则优先选择交换(env->best_task = cur)

优先使用交换的原因:单向迁移会破坏 dst_nid 的负载平衡(多了一个任务),而交换是等量交换,不改变各节点的任务数量。

特殊处理(fair.c:2363-2365):若当前最优是交换,但新候选只是单向迁移且收益更高,则切换到单向迁移:

if (env->best_task &&
    env->best_task->numa_preferred_nid == env->src_nid &&
    env->best_task->numa_preferred_nid != env->src_nid) {
    // 当前 best 是交换,新候选 cur 不适合交换,退回单向
}

11.5 preferred_group_nid:NUMA_BACKPLANE 拓扑下的组偏好节点

preferred_group_nid()fair.c:2868-2950)在非直连拓扑下递归搜索最优节点:

NUMA_BACKPLANE 拓扑下的层次搜索算法:

  dist = sched_max_numa_distance(最大 NUMA 距离)
  nodes = 所有有 CPU 的节点

  for dist 从 max_dist 递减到 LOCAL_DISTANCE:
    将节点按"dist 内可达"分组
    计算每组内所有节点的 group_faults 总和
    找出总 faults 最高的组(max_group)
    nodes = max_group  // 下次迭代只在最优组内搜索

  最终收敛到单个节点(最内层组的最优节点)

示意(4 节点 NUMA_BACKPLANE 系统):
  节点0, 节点1 在同一 die(近)
  节点2, 节点3 在另一 die(远)

  第一轮(dist=200):组A={0,1} faults=800, 组B={2,3} faults=200
  → 选组A,nodes={0,1}
  第二轮(dist=100):{0} faults=600, {1} faults=200
  → 选节点0
  → preferred_nid = 0

12. AutoNUMA 扫描机制的细节

12.1 mm->numa_scan_seq 的作用

mm->numa_scan_seq 是进程地址空间级别的扫描序号,每完成一次完整扫描(遍历完整个 VMA 列表后绕回)递增一次。它的核心用途:

  1. 幂等化 task_numa_placementfair.c:2957-2960):
seq = READ_ONCE(p->mm->numa_scan_seq);
if (p->numa_scan_seq == seq)
    return;  // 本轮已处理过,跳过
p->numa_scan_seq = seq;

多线程程序的所有线程共享同一个 mm,但每个线程都有独立的 task_numa_work。若多个线程在同一轮内触发 task_numa_placement,只有第一个执行(通过 seq 去重),避免重复计算。

  1. VMA 扫描防重入fair.c:1048):
// VMA 的 numab_state 记录上次完成扫描的 seq
if (vma->numab_state->prev_scan_seq == mm->numa_scan_seq)
    skip_reason = NUMAB_SKIP_SEQ_COMPLETED;

确保同一 VMA 在一个扫描轮次内不会被重复标记 PROT_NONE。

  1. VMA 建立基线期豁免fair.c:1047-1049):
// 前两轮扫描无条件扫描(不检查 PID 活跃度)
if ((mm->numa_scan_seq - vma->numab_state->start_scan_seq) < 2)
    return true;

新建的 VMA 在前两轮内强制扫描,确保建立足够的统计基线。

12.2 numa_scan_offset 与增量扫描

mm->numa_scan_offset 记录上次扫描结束的地址,实现跨多次 task_numa_work 调用的增量扫描(fair.c:3479):

// 从上次停止的位置继续扫描
vma_iter_init(&vmi, mm, mm->numa_scan_offset);

扫描完整个地址空间后复位:

if (mm->numa_scan_offset >= end_of_address_space) {
    mm->numa_scan_offset = 0;
    atomic_inc(&mm->numa_scan_seq);  // 完成一轮,seq++
}

这种增量设计的好处:scan_size 每次扫描固定量的物理页(默认 256MB),对大内存进程(如数据库的几十 GB RSS)不会一次性扫描完整内存,避免长时间持有 mmap_read_lock 影响其他线程。

12.3 VMA numab_state 详解

struct vma_numab_stateinclude/linux/mm_types.h 中)是 VMA 级别的 NUMA 均衡状态:

struct vma_numab_state {
    unsigned long next_scan;         // 下次允许扫描的绝对 jiffies 时间
    unsigned long next_pid_reset;    // 下次重置 pids_active 的 jiffies 时间
    unsigned long access_pids[2];    // PID 哈希位图双缓冲(滑动窗口)
    unsigned int  start_scan_seq;    // VMA 创建时的 mm->numa_scan_seq
    unsigned int  prev_scan_seq;     // 上次完成扫描时的 mm->numa_scan_seq
};

access_pids[] 双缓冲机制(fair.c:1064-1068):

  时间轴:
  |-- period T ---|-- period T+1 --|-- period T+2 --|
  access_pids[1]  = 当前活跃 PID 集合(位图)
  access_pids[0]  = 上一周期活跃 PID

  每隔 VMA_PID_RESET_PERIOD:
    access_pids[0] = access_pids[1]  // 归档
    access_pids[1] = 0               // 清空,开始新周期

  当任务访问该 VMA 时,将 hash(pid) 写入 access_pids[1]
  扫描时检查:hash(current->pid) in (pids[0] | pids[1])

这个机制用 O(1) 的位图操作替代了 O(n) 的 PID 链表查找,在进程数量多时性能优势显著。

12.4 大页(THP)与 AutoNUMA

透明大页(THP,2MB hugepage)对 AutoNUMA 有特殊处理:

  1. 跳过 hugetlb 页fair.c:1032):
if (is_vm_hugetlb_page(vma)) {
    skip_reason = NUMAB_SKIP_UNSUITABLE;
    continue;
}

hugetlb(固定大页)因为 NUMA hint PTE 机制与普通页不同(ARM64 上 Block descriptor 格式差异),暂不支持 AutoNUMA。

  1. THP(透明大页)支持:THP 通过 numa_rebuild_large_mapping()folio_test_large() 特殊路径处理,do_numa_page() 中会以 folio 为单位处理整个 2MB 页:
nr_pages = folio_nr_pages(folio);  // THP: nr_pages = 512 (2MB/4KB)
// task_numa_fault 计数也以 nr_pages 为单位
task_numa_fault(last_cpupid, nid, nr_pages, flags);

THP 迁移时,migrate_misplaced_folio() 在目标节点分配同样大小的 THP,若分配失败则降级为普通页迁移(fallback_huge_page_size 机制)。

12.5 migrate_pages 的异步模式

AutoNUMA 使用 MIGRATE_ASYNC 模式(mm/migrate.c),这意味着:

MIGRATE_SYNC:   等待页迁移完成,可能阻塞 ms 级别
MIGRATE_ASYNC:  遇到锁竞争立即放弃,最小化影响

AutoNUMA 选择 MIGRATE_ASYNC 的原因:
1. 缺页处理路径需要快速返回(不能长时间持有 PTE 锁)
2. 迁移失败时会在下次缺页时重试(无需强制成功)
3. 避免阻塞当前进程影响响应延迟

迁移成功率追踪:count_vm_numa_events(NUMA_PAGE_MIGRATE, nr_succeeded)count_vm_events(PGMIGRATE_FAIL, nr_remaining) 统计成功和失败的页数,可通过 /proc/vmstat 监控。


13. 调试接口深度分析

13.1 /proc/PID/sched 的 NUMA 输出

proc_sched_show_task() 通过 sched_show_numa() 函数(kernel/sched/debug.c:1236-1249)输出每个任务的 NUMA 统计:

cat /proc/$(pgrep myapp)/sched

NUMA 相关输出字段(kernel/sched/debug.c:1225-1248):

# NUMA Balancing 字段解析:
numa_faults node=0   task_private=1200 task_shared=300 group_private=2000 group_shared=500
numa_faults node=1   task_private=50   task_shared=20  group_private=100  group_shared=40

mm.numa_scan_seq:                            42   # 已完成的扫描轮次
numa_pages_migrated:                       3456   # 累计迁移成功的页数
numa_preferred_nid:                           0   # 当前偏好 NUMA 节点
total_numa_faults:                         1570   # 累计 NUMA 缺页次数
current_node=0, numa_group_id=1234              # 当前 CPU 节点和所属组 GID

# EAS/调度相关字段:
se.avg.util_avg:                            256   # PELT 利用率(0-1024)
se.avg.load_avg:                            256   # PELT 负载
uclamp.min:                                   0   # uclamp 最小值
uclamp.max:                                1024   # uclamp 最大值
effective uclamp.min:                         0   # 实际生效的 uclamp_min
effective uclamp.max:                      1024   # 实际生效的 uclamp_max

# schedstats(需 CONFIG_SCHEDSTATS=y 且 kernel.sched_schedstats=1):
nr_wakeups:                                5678   # 总唤醒次数
nr_wakeups_migrate:                         123   # 唤醒时发生 CPU 迁移的次数
nr_wakeups_remote:                           45   # 跨域唤醒次数

13.2 /sys/kernel/debug/sched/ 接口

调度器 debugfs 节点(由 kernel/sched/debug.c:596-630 创建):

ls /sys/kernel/debug/sched/
# debug           - 全系统调度状态(类似 /proc/sched_debug)
# verbose         - 调度器详细日志开关
# features        - 调度器特性开关(SCHED_FEAT_*)
# domains/        - 调度域层次结构
# numa_balancing/ - NUMA 均衡参数(同 sysctl)

/sys/kernel/debug/sched/domains/cpuN/domainM/ 下的文件提供了每个调度域的详细参数:

cat /sys/kernel/debug/sched/domains/cpu4/domain0/flags
# 显示该调度域的 SD_* 标志位(十六进制),可用于确认 SD_ASYM_CPUCAPACITY 是否设置
# SD_ASYM_CPUCAPACITY = 0x40 (位 6)
# SD_NUMA             = 0x400 (位 10)

13.3 能量模型 debugfs 接口

/sys/kernel/debug/energy_model/kernel/power/energy_model.cem_debug_create_pd() 函数创建:

# 结构:每个性能域一个目录,以其 CPU 掩码命名
ls /sys/kernel/debug/energy_model/
# cpu0/   # 小核集群(CPU 0-3)
# cpu4/   # 大核集群(CPU 4-7)

# 每个域下的 OPP 目录:
ls /sys/kernel/debug/energy_model/cpu0/
# ps:500000/   ps:800000/   ps:1200000/
# (ps:<频率kHz>)

# 每个 OPP 的详细信息:
cat /sys/kernel/debug/energy_model/cpu0/ps:500000/power       # 功耗(uW 或抽象单位)
cat /sys/kernel/debug/energy_model/cpu0/ps:500000/cost        # cost 系数
cat /sys/kernel/debug/energy_model/cpu0/ps:500000/performance # 性能容量(0-1024)
cat /sys/kernel/debug/energy_model/cpu0/ps:500000/flags       # 标志(含 INEFFICIENT)

# 域级别信息:
cat /sys/kernel/debug/energy_model/cpu0/cpus      # CPU 掩码(如 0-3)
cat /sys/kernel/debug/energy_model/cpu0/flags     # 域标志(MICROWATTS/SKIP_INEFF)

13.4 tracepoint 完整列表与使用

EAS 相关 tracepoint(在 include/trace/events/sched.h 中定义):

# 启用全部 EAS 相关 tracepoint:
for tp in sched_compute_energy_tp sched_overutilized_tp sched_find_energy_efficient_cpu; do
    echo 1 > /sys/kernel/tracing/events/sched/${tp}/enable 2>/dev/null
done

# sched_compute_energy_tp 字段:
# task=<comm>/<pid>  dst_cpu=<N>  energy=<value>  max_util=<N>  busy_time=<N>
# 其中 energy 是相对值(不是物理单位),用于跨 CPU 比较

# sched_overutilized_tp 字段:
# rd=<addr>  overutilized=<0|1>
# overutilized=1 表示 EAS 已退出,系统进入负载均衡模式

AutoNUMA 相关 tracepoint(在 include/trace/events/sched.hinclude/trace/events/migrate.h):

# 启用 AutoNUMA 全部相关 tracepoint:
for tp in sched_skip_vma_numa sched_stick_numa sched_skip_cpuset_numa; do
    echo 1 > /sys/kernel/tracing/events/sched/${tp}/enable
done
echo 1 > /sys/kernel/tracing/events/migrate/mm_migrate_pages/enable

# sched_skip_vma_numa 字段:
# pid=<N>  tgid=<N>  vma_start=<addr>  vma_end=<addr>
# type=<NUMAB_SKIP_UNSUITABLE|NUMAB_SKIP_SHARED_RO|...>
# (type 值见 fair.c 中的 enum numab_skip_reason)

# mm_migrate_pages 字段(内存迁移结果):
# nr_succeeded=<N>  nr_failed=<N>  nr_thp_succeeded=<N>
# reason=<MR_NUMA_MISPLACED|MR_MEMORY_TIERING|...>

IPA 热管理 tracepoint:

echo 1 > /sys/kernel/tracing/events/thermal/thermal_power_allocator/enable
echo 1 > /sys/kernel/tracing/events/thermal/thermal_power_allocator_pid/enable

# thermal_power_allocator_pid 字段(PID 控制器状态):
# thermal_zone=<name>  err=<mC>  err_integral=<N>  p=<mW>  i=<mW>  d=<mW>  power=<mW>
# 通过这些字段可以观察 PID 控制器的稳定性

# thermal_power_allocator 字段(功率分配结果):
# thermal_zone=<name>  req_power=<mW>  granted_power=<mW>
# total_req=<mW>  total_granted=<mW>  power_range=<mW>

13.5 schedstats 详解

schedstats 需要编译时启用 CONFIG_SCHEDSTATS=y,运行时通过 sysctl 开关:

echo 1 > /proc/sys/kernel/sched_schedstats

关键统计字段含义(通过 /proc/schedstat/proc/PID/sched):

字段 来源 含义
nr_wakeups schedstat_inc(p->stats.nr_wakeups) 总唤醒次数
nr_wakeups_migrate 唤醒时发生 CPU 迁移 迁移导致的唤醒
nr_wakeups_affine 亲和性唤醒成功 唤醒到亲和 CPU 上
nr_failed_migrations_affine 亲和性迁移失败 CPU 已被占用
nr_failed_migrations_running 目标 CPU 正在运行 等待时被占用
wait_sum schedstat_add(stats->wait_sum, delta) 累计等待时间(ns)
iowait_sum IO 等待累计时间 评估 IO 延迟

EAS 特定统计:当前版本没有专用的 EAS 命中/错过计数器,但可以通过 nr_wakeups_migrate / nr_wakeups 的比例间接评估 EAS 的迁移效果(EAS 减少不必要迁移,该比例应降低)。


14. 性能分析案例

14.1 案例一:移动设备轻负载场景的 EAS 验证

场景描述:Android 智能手机,ARM Cortex-A55×4 + A78×4,空闲状态浏览网页(~15% CPU 利用率)。

目标:验证 EAS 正确将轻负载任务打包到小核,大核进入 C2 深睡眠。

验证步骤

# 步骤1:确认 EAS 已启用
cat /proc/sys/kernel/sched_energy_aware  # 应为 1
cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor | sort -u  # 应为 schedutil

# 步骤2:获取 CPU 利用率分布
cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_cur_freq
# 若 EAS 有效:A55 运行在 600-800MHz,A78 idle(0 或最低频率)

# 步骤3:通过 tracepoint 观察唤醒路径
echo 1 > /sys/kernel/tracing/events/sched/sched_compute_energy_tp/enable
# 在浏览器滚动时触发一些 trace
cat /sys/kernel/tracing/trace | grep compute_energy | head -5
# 输出示例(dst_cpu 0-3 为小核):
# sched_compute_energy_tp: task=chrome/1234 dst_cpu=2 energy=1200 max_util=180 busy_time=320

# 步骤4:验证大核不被不必要唤醒
cat /sys/devices/system/cpu/cpu4/cpufreq/stats/time_in_state
# A78 在低频或 idle 的时间应占主体(>95%)

# 步骤5:功耗对比(需要 PMIC 功耗传感器或 PowerTOP)
echo 0 > /proc/sys/kernel/sched_energy_aware
# 等待 30 秒后记录功耗
echo 1 > /proc/sys/kernel/sched_energy_aware
# 再等待 30 秒记录功耗
# 期望:EAS 开启时整体功耗降低 10-30%(典型值,取决于负载)

常见问题分析

问题:A78 仍然被频繁唤醒(EAS 看起来无效)
可能原因:
1. rd->overutilized = 1(某个 CPU 超过 80% 容量)
   诊断:echo 1 > .../sched_overutilized_tp/enable
2. uclamp_min 设置了过高值(某些 Android 进程设置了 uclamp.min)
   诊断:cat /proc/$(pgrep chrome)/sched | grep uclamp
3. EM 数据不准确(标记了 EM_PERF_DOMAIN_ARTIFICIAL)
   诊断:cat /sys/kernel/debug/energy_model/cpu0/flags

14.2 案例二:双路 x86 服务器 AutoNUMA 调优

场景描述:两路 AMD EPYC 9654(共 192 核,两 NUMA 节点,各 192GB RAM),运行 PostgreSQL 数据库(100 个并发连接,混合读写)。

初始状态观察

# 查看 NUMA 远端访问率
numastat
# 输出(问题状态):
# NUMA hit:     45000000   NUMA miss: 8000000
# NUMA local:   45000000   NUMA other: 8000000
# Remote 比率: 8000000 / 53000000 ≈ 15%(过高,理想应 < 3%)

# 查看哪些进程在跨节点访问
numastat -p $(pgrep -d, postgres)
# 若 node1 分配的内存远多于 node1 上运行的进程,说明错位

# 查看 AutoNUMA 的迁移效果
grep numa /proc/vmstat
# numa_hint_faults:         250000   ← PROT_NONE 触发的缺页
# numa_hint_faults_local:   210000   ← 其中本地缺页(占 84%,表示大量内存已在本地)
# numa_pages_migrated:       15000   ← 已迁移的页数
# numa_migrate_fail:          8000   ← 迁移失败(占 53%,异常高)

问题诊断

# 迁移失败率高(53%)可能原因:
# 1. DRAM 水位不足,目标节点内存不够
free -m
# node0: 45GB free, node1: 2GB free(node1 内存严重不足)

# 2. 迁移的页太多是脏页(数据库写密集)
grep "^pgmigrate" /proc/vmstat

# 诊断:启用 VMA 扫描跳过 trace
echo 1 > /sys/kernel/tracing/events/sched/sched_skip_vma_numa/enable
cat /sys/kernel/tracing/trace | grep NUMAB_SKIP | sort | uniq -c | sort -rn | head -5
# 可能输出:
# 5000 NUMAB_SKIP_UNSUITABLE     ← 共享库(.so 文件)不迁移
#  800 NUMAB_SKIP_SCAN_DELAY     ← 扫描速率被限制
#  200 NUMAB_SKIP_PID_INACTIVE   ← 该 VMA 未被活跃访问

调优操作

# 操作1:增大扫描粒度(减少频率但覆盖更多)
echo 512 > /proc/sys/kernel/numa_balancing_scan_size_mb

# 操作2:给 node1 补充内存后,确认迁移能成功
# (在本例中需要增加 node1 内存,或通过 cgroup 限制 node1 用量)

# 操作3:对 PostgreSQL 启用大页(减少 PTE 数量,降低扫描开销)
echo "huge_pages = on" >> /etc/postgresql/*/main/postgresql.conf

# 操作4:若 PostgreSQL 已通过 numactl 绑定 node0,关闭 AutoNUMA 避免干扰
numactl --cpubind=0 --membind=0 postgres &
echo 0 > /proc/sys/kernel/numa_balancing  # 关闭全局 AutoNUMA

# 验证:关闭 AutoNUMA 后的 NUMA miss 率应接近 0(因为内存和 CPU 已手动绑定)

结果对比

            AutoNUMA 开启   AutoNUMA 关闭+numactl
NUMA miss率  15%            < 1%
QPS          42000          51000(+21%)
p99 延迟     8.2ms          5.1ms(-38%)

这个案例说明:对于已明确知道内存布局的应用,手动 NUMA 绑定比 AutoNUMA 自动调整效果更好,AutoNUMA 的扫描开销反而成了负担。

14.3 案例三:CXL 内存环境的 Memory Tiering 调优

场景描述:双路服务器,每路附加 512GB CXL 内存(node2/node3),DRAM(node0/node1)各 128GB,运行 JVM 应用(Kafka broker)。

目标:最大化 DRAM 中热数据的命中率,冷数据卸载到 CXL。

# 步骤1:确认 memory tiering 模式已启用
echo 2 > /proc/sys/kernel/numa_balancing

# 步骤2:观察初始状态(所有内存可能都在 DRAM,因为 JVM 启动时用 first-touch)
numastat -n | grep -E "node[0-3]"
# 若 DRAM 已满,kswapd 会将冷页 demote 到 CXL

# 步骤3:监控提升速率
watch -n 2 'grep -E "pgpromote|pgdemote|PGPROMOTE" /proc/vmstat'
# pgpromote_success: CXL -> DRAM 提升成功数
# pgdemote_kswapd: DRAM -> CXL 降级数(kswapd 发起)

# 步骤4:调整热页阈值(默认 1000ms)
# 若 Kafka 消息保留时间短(高吞吐),降低阈值使更多页被认为是热页
echo 300 > /proc/sys/kernel/numa_balancing_hot_threshold_ms

# 步骤5:控制提升速率避免冲击 DRAM 带宽
echo 4096 > /proc/sys/kernel/numa_balancing_promote_rate_limit_MBps

# 步骤6:监控效果
# 理想状态:pgpromote_success 持续发生,Kafka 消费延迟降低
# 若提升太激进:DRAM 带宽饱和(iowait 升高),需降低 rate_limit

调优结论

热页阈值调优效果(Kafka 8MB/s 吞吐场景):

  hot_threshold  提升速率(MB/s)  消费延迟p99  DRAM 带宽利用率
  2000ms(偏保守) 120             12ms         45%
  1000ms(默认)   380             8ms          68%
  300ms(激进)    1200            6ms          95%  ← 带宽趋于饱和
  100ms(过激)    2800            9ms          105% ← 带宽饱和,延迟反升

  最优点约在 300-500ms,具体取决于 DRAM 带宽上限。

15. EAS 与 AutoNUMA 的未来发展方向

15.1 NUMA-aware EAS(研究方向)

当前 EAS 的一个重要限制是它不感知 NUMA 拓扑。在多路异构 ARM 服务器上,EAS 选核时只考虑能耗,不考虑任务内存的 NUMA 亲和性,可能导致以下情况:

现状问题示例:

  Socket0: 小核(cap=400)  Socket1: 大核(cap=1024)
  NUMA node0                NUMA node1
  任务的内存: 80% 在 node1

  EAS 决策: 放到 socket0 小核(更节能)
  AutoNUMA 反应: 数据在 node1,任务跑在 node0 → NUMA miss 高
  AutoNUMA 迁移: 将任务移到 socket1 大核(NUMA 亲和)

  结果: EAS 和 AutoNUMA 相互拉锯,任务来回迁移

可能的改进方向(内核讨论中):

  • EAS 在 find_energy_efficient_cpu() 中加入 NUMA 亲和性权重
  • 为跨 NUMA 节点的迁移增加额外"NUMA 惩罚"项
  • energy_env 中添加 NUMA 距离因子

15.2 智能 Memory Tiering 与 ML 预测

当前 Memory Tiering 基于简单的时间窗口判断热度(numa_hint_fault_latency()),未来可能引入:

  • 访问频率预测(基于 PMU 硬件计数器)
  • 工作集分析(结合 kswapd 的 refault distance 机制)
  • 跨层次协同(DRAM/CXL/NVMe 三级分层)

15.3 EAS 对 SMT 的扩展支持

当前 EAS 明确不支持 SMT(sched_smt_active() 检查),主要原因是 SMT 的 CPU 共享 L1/L2 cache,两个超线程的利用率叠加时能量模型不准确。

未来可能通过引入 em_perf_domain 的 SMT 感知版本(考虑超线程间的功耗耦合),使 EAS 支持超线程开启的 x86 服务器,扩大 EAS 的适用范围。


16. 附录:关键 sysctl 与 debugfs 完整参考

16.1 EAS 相关参数

路径 类型 默认值 说明
/proc/sys/kernel/sched_energy_aware rw 1 EAS 总开关,0=禁用
/sys/kernel/debug/sched/features rw - SCHED_FEAT 位标志,如 ENERGY_AWARE
/sys/kernel/debug/energy_model/ ro - EM debugfs 树
/sys/devices/system/cpu/cpufreq/policyN/schedutil/rate_limit_us rw ~4000 schedutil 频率更新间隔(µs)

16.2 AutoNUMA 相关参数

路径 默认值 说明
/proc/sys/kernel/numa_balancing 1 0=关, 1=普通 NUMA, 2=+Memory Tiering
/proc/sys/kernel/numa_balancing_scan_period_min_ms 1000 最小扫描周期(ms)
/proc/sys/kernel/numa_balancing_scan_period_max_ms 60000 最大扫描周期(ms)
/proc/sys/kernel/numa_balancing_scan_size_mb 256 每轮扫描量(MB)
/proc/sys/kernel/numa_balancing_scan_delay_ms 1000 VMA 首次扫描延迟(ms)
/proc/sys/kernel/numa_balancing_hot_threshold_ms 1000 热页判定阈值(ms,Memory Tiering)
/proc/sys/kernel/numa_balancing_promote_rate_limit_MBps 65536 热页提升速率上限(MB/s)

16.3 关键 /proc/vmstat NUMA 计数器

计数器 说明
numa_hit 内存分配在请求节点(本地),理想值越高越好
numa_miss 内存分配到非预期节点(内存不足时 fallback)
numa_foreign 期望本地分配但跑到远端
numa_interleave 通过 MPOL_INTERLEAVE 策略分配的页数
numa_local 访问本地节点内存的次数
numa_other 访问远端节点内存的次数
numa_pte_updates AutoNUMA 标记为 PROT_NONE 的 PTE 数
numa_hint_faults 触发 NUMA hint 缺页(PROT_NONE 被访问)的次数
numa_hint_faults_local 其中的本地缺页数(内存与 CPU 已在同一节点)
numa_pages_migrated AutoNUMA 迁移成功的页数
numa_migrate_fail AutoNUMA 迁移失败的次数
pgpromote_success Memory Tiering: 慢层->DRAM 提升成功
pgpromote_candidate Memory Tiering: 被考虑提升的候选页数
pgdemote_kswapd Memory Tiering: DRAM->慢层 降级(kswapd)
pgdemote_direct Memory Tiering: DRAM->慢层 降级(直接回收)

16.4 热管理相关参数

路径 说明
/sys/class/thermal/thermal_zoneN/trip_point_N_temp 触发温度阈值(毫摄氏度)
/sys/class/thermal/thermal_zoneN/sustainable_power IPA 可持续功率(mW)
/sys/class/thermal/thermal_zoneN/k_po IPA PID 超调比例系数
/sys/class/thermal/thermal_zoneN/k_pu IPA PID 欠调比例系数
/sys/class/thermal/thermal_zoneN/k_i IPA PID 积分系数
/sys/class/thermal/thermal_zoneN/k_d IPA PID 微分系数
/sys/class/thermal/cooling_deviceN/cur_state 冷却设备当前状态(0=最高性能)
/sys/class/thermal/cooling_deviceN/max_state 最大冷却状态(对应最低频率)

17. 附录:扩展函数速查表

函数名 文件:行号 功能简述
cpufreq_update_pressure() drivers/cpufreq/cpufreq.c:2586 热限频时更新 per-CPU 容量压力值
cpufreq_get_pressure() include/linux/cpufreq.h:258 读取 CPU 热压力(调度器使用)
sched_is_eas_possible() kernel/sched/topology.c:214 检查是否满足 EAS 启用条件
build_perf_domains() kernel/sched/topology.c:409 构建性能域链表并链接到 root_domain
sched_energy_set() kernel/sched/topology.c:388 根据 has_eas 更新 sched_energy_present 静态 key
pid_controller() drivers/thermal/gov_power_allocator.c:239 IPA PID 控制器,输出功率预算
allocate_power() drivers/thermal/gov_power_allocator.c:406 IPA 主入口,收集功率请求并分配
divvy_up_power() drivers/thermal/gov_power_allocator.c:350 按比例将功率预算分配给各 cooling actor
cpufreq_get_requested_power() drivers/thermal/cpufreq_cooling.c:230 读取 CPU 当前功率(CPU 负载 × 频率)
cpufreq_state2power() drivers/thermal/cpufreq_cooling.c:274 冷却 state → 对应功率(查 EM 表)
cpufreq_power2state() drivers/thermal/cpufreq_cooling.c:314 功率约束 → 冷却 state(限频目标)
sugov_effective_cpu_perf() kernel/sched/cpufreq_schedutil.c:209 含余量的 CPU 性能需求(EAS/schedutil 共用)
numa_classify() kernel/sched/fair.c:2142 将 NUMA 节点分类为 spare/busy/overloaded
task_numa_find_cpu() kernel/sched/fair.c:2506 在目标节点搜索最优迁移目标 CPU
preferred_group_nid() kernel/sched/fair.c:2868 NUMA_BACKPLANE 拓扑下的层次搜索偏好节点
numa_hint_fault_latency() kernel/sched/fair.c:1919 计算 hint 缺页延迟(Memory Tiering 热度判断)
pgdat_free_space_enough() kernel/sched/fair.c:1886 检查 DRAM 节点是否有足够空间激进提升
numa_promotion_rate_limit() kernel/sched/fair.c:1934 限制热页提升速率,防止冲击 DRAM 带宽
change_prot_numa() mm/mempolicy.c:884 将 VMA 范围内的 PTE 标记为 PROT_NONE
print_numa_stats() kernel/sched/debug.c:1226 输出 /proc/PID/sched 中的 NUMA 统计
sched_show_numa() kernel/sched/debug.c:1236 聚合 NUMA 调试信息输出
em_dev_update_chip_binning() include/linux/energy_model.h:186 运行时基于实测功耗更新 EM(chip binning)
em_update_performance_limits() include/linux/energy_model.h:187 热限制时收窄 EAS 可用 OPP 范围
estimate_sustainable_power() drivers/thermal/gov_power_allocator.c:116 估算热区可持续散热功率
estimate_pid_constants() drivers/thermal/gov_power_allocator.c:149 自动估算 IPA PID 参数

由 Claude Code 分析生成