基于 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(调度器内部数据结构)
- 概述与背景
- EAS:能效感知调度
- 2.1 能量模型框架(Energy Model)
- 2.2 OPP 表到能量模型的映射
- 2.3 em_dev_register_perf_domain 注册流程
- 2.4 CPU Capacity 概念与异构系统
- 2.5 arch_scale_cpu_capacity 与 arch_scale_freq_capacity
- 2.6 EAS 启用条件:sched_energy_enabled
- 2.7 schedutil governor 与 EAS 协作
- 2.8 energy_env 能耗估算环境
- 2.9 eenv_pd_busy_time 与 eenv_pd_max_util
- 2.10 compute_energy 能耗计算
- 2.11 find_energy_efficient_cpu 唤醒路径详解
- 2.12 overutilized 保护机制
- 2.13 uclamp 与 EAS 的交互
- AutoNUMA:自动 NUMA 平衡
- 3.1 问题背景:NUMA 远端内存访问
- 3.2 核心机制概览
- 3.3 NUMA 缺页统计数据结构
- 3.4 numa_faults 数组布局
- 3.5 NUMA 扫描:task_numa_work
- 3.6 VMA 过滤规则详解
- 3.7 change_prot_numa 与 PROT_NONE 标记
- 3.8 扫描速率动态调整
- 3.9 NUMA fault 处理路径:do_numa_page
- 3.10 task_numa_fault:缺页记录与统计
- 3.11 cpupid 字段:CPU/PID 编码机制
- 3.12 task_numa_placement:偏好节点计算
- 3.13 指数衰减与滑动窗口统计
- 3.14 task_weight 与 group_weight:亲和性评分
- 3.15 score_nearby_nodes:非直连拓扑处理
- 3.16 task_numa_migrate:任务迁移决策
- 3.17 numa_migrate_preferred:迁移入口
- 3.18 migrate_misplaced_folio:内存页迁移
- 3.19 numa_group:共享内存分组机制
- 3.20 task_numa_group:分组触发与合并
- 3.21 内存分层与 AutoNUMA 的交互
- 3.22 Memory Tiering 下的 cpupid 重用
- 3.23 sysctl 参数调优参考
- EAS 与 AutoNUMA 的协同分析
- 4.1 两者的边界与正交性
- 4.2 CPU 迁移路径的潜在干扰
- 4.3 update_scan_period:跨节点迁移后的扫描复位
- 4.4 ARM DSU 架构下的实践场景
- 4.5 异构 CPU + 异构内存的组合调度
- 调试与观测手段
- 性能影响与调优建议
- 附录:关键函数速查表
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 是 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 范围收窄
能量模型(EM)是 EAS 的数据基础,定义在 include/linux/energy_model.h。整个框架由三层数据结构构成。
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 计算时跳过。
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() 触发,不需要停机。
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 |
+------------------------------------------------------------------+
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):
max_util = min(max_util, allowed_cpu_cap)— 受热限制夹住,避免估算超出实际能力- 调用
em_pd_get_efficient_state()找到满足max_util需求的最低有效 OPP 索引 - 返回
ps->cost * sum_util— 整个性能域的能耗估算值
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 继续向上查找。
驱动程序通过 dev_pm_opp 框架提供 OPP 表,能量模型框架将其转换为内部的 em_perf_state 数组。关键路径在 kernel/power/energy_model.c 的 em_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 表提供的功耗数据有两种来源:
- 设备树
operating-points-v2节点的opp-microwatt属性(精确值) - 驱动程序通过
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 链表)
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 值(适合功耗曲线非线性的设备)
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
}
#endifARM 架构会覆盖此函数(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;
}扣除两种压力来源:
hw_load_avg():硬件层面报告的负载(如 ARM AMU 寄存器,反映 CPU stall 等开销)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% 余量预留了这个反应时间。
两者共同描述了 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
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.c 的 sched_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。
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 的能耗预测将失准。
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_env 在 find_energy_efficient_cpu() 的每次调用中栈上分配,包含了单次 EAS 决策所需的所有中间状态。
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 的频率。
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
能耗估算值(相对值,仅用于比较)
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 的三值语义:
-1:util_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 [保持原位]
关键设计决策:
- 每个 pd 只选一个候选 CPU(最大空闲容量者):避免 O(n^2) 比较,减少热路径开销
prev_cpu始终参与比较:体现调度器的"惰性"偏好,减少不必要迁移- 三值 fits 优先级:性能约束(uclamp_min)优先于能耗优化,保证 RT/延迟敏感任务得到满足
- 早退机制(
goto unlock):若检测到 CPU 利用率已变化(prev_delta < base_energy),说明数据不一致,直接退回 prev_cpu 而非做出错误决策
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->overutilized 由 check_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)
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; // 不满足容量约束,跳过该 CPUutil_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 会优先将其放到有足够容量的大核,而非小核。
在 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 的使命就是检测并修复这种错位。
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 │
│ │ │
│ └──────────────── 回到 ① │
└─────────────────────────────────────────────────────────────┘
kernel/sched/fair.c:1525-1544,struct 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_min~task_scan_max范围内动态调整numa_scan_seq:与mm->numa_scan_seq对比,判断是否需要重新 placement 评估numa_faults_locality[3]:[0]=远端缺页数, [1]=本地缺页数, [2]=迁移失败次数numa_pages_migrated:累计迁移成功的页数(用于统计)numa_migrate_retry:下次允许重试迁移的 jiffies 时间戳
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() 每个扫描轮次将缓冲区通过指数衰减合并到平滑统计区。
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);
}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 的开销。
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 实现
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 处调用):根据本次扫描周期内本地与远端缺页的比例调整下次周期:
- 本地缺页多(内存已在本地)→ 减小周期,减少不必要扫描(收敛后降频)
- 远端缺页多(仍有优化空间)→ 增大周期,让迁移有时间稳定
当用户态访问被标记为 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 为目标节点,失败时为原节点。这确保了统计的完整性。
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 路径)
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)。
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]);
}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 亲和性更可靠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 → 迁移对任务和组均不利 → 跳过
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]
节点按组聚集,组内低延迟,跨组高延迟
迁移时应考虑整个组的内存亲和性
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 负载不均的问题。
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 的设计:使迁移重试频率与扫描频率挂钩——扫描周期越短(访问模式变化越快),重试越频繁;扫描周期越长(已稳定),重试越少。
内存迁移通过 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)
struct numa_group(kernel/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)
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
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(取较大值),则激进地提升所有近期访问的慢层页,不依赖热页判定阈值。
在 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),避免频繁的冷热页抖动。
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)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层次(决定搜索范围)
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 的节能决策。
然而在大多数实际场景中,这种冲突不严重:
- big.LITTLE 系统通常是单 NUMA 节点(单芯片),AutoNUMA 不活跃
- 多路服务器通常是对称 CPU 容量(x86 或同类 ARM),EAS 不活跃
- 只有在多路异构 ARM 服务器(如 Ampere 的多 socket 配置)上两者才会同时活跃
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 的自适应响应。
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 特别受益于:
- 共享 L3 缓存:任务在小核/大核间迁移不会失去 L3 缓存(热数据依然可用),降低了迁移代价
- 精细的三层能量模型:A55/A78/A78-X 三个 PD 各有独立 EM,EAS 可以精细区分三档能效
EM_PERF_DOMAIN_SKIP_INEFFICIENCIES:跳过功耗曲线上的低效 OPP 点,进一步降低能耗
在未来的系统中(例如多 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。两者的协同优化仍是调度子系统的研究课题。
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进程级 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 等场景 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场景 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 测试
| 函数名 | 文件:行号 | 功能简述 |
|---|---|---|
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 分析生成
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 的一半)。
IPA 是 Linux 热管理框架中用于"功率感知调温"的核心 governor,定义在 drivers/thermal/gov_power_allocator.c。与简单的 step_wise governor(直接切 cooling state)不同,IPA 以功率为单位进行分配,能够在多个 cooling device 之间智能权衡。
struct power_allocator_params(gov_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)。
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,意味着升温响应比降温更激进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 的 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()
(功率 <-> 频率状态) (利用率 -> 能耗)
热管理通过两条路径影响 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 不会再考虑范围外的 OPPem_pd_get_efficient_state() 使用 min_perf_state 作为起始搜索点(energy_model.h:209-210),确保 EAS 在超温时不会"虚假地"估算使用高频 OPP 的能耗。
当系统深度过热时,热管理可能将所有 CPU 限到同一低频,此时:
- 所有 CPU 的
cpufreq_pressure均大幅增加 - 大核和小核的有效容量差距缩小
SD_ASYM_CPUCAPACITY标志在容量趋同后可能失去意义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()
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.c 中 em_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();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 的中频段尤为常见,是物理约束(如电源轨共享、电压域耦合)造成的功耗曲线非单调性。
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,使能耗估算的误差最小化。
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 密集任务的能耗估算更接近实际调频行为。
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 预测更准
- 功耗优先场景:保持默认或适当增大,减少频率切换损耗
在 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% 的负载不均,避免因轻微不平衡触发不必要的迁移。
struct numa_stats(fair.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字段记录) - 若非空闲:评估当前运行任务与目标任务的双向收益(任务交换逻辑)
SMALLIMP = 30(fair.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% 的迁移收益才值得迁移。这个阈值防止了因微小统计波动导致的任务频繁抖动。
当且仅当满足以下条件时才更新候选:
imp >= SMALLIMP(绝对收益足够大)imp > env->best_imp + SMALLIMP/2(比当前最优候选好至少 1.5%)
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 不适合交换,退回单向
}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
mm->numa_scan_seq 是进程地址空间级别的扫描序号,每完成一次完整扫描(遍历完整个 VMA 列表后绕回)递增一次。它的核心用途:
- 幂等化 task_numa_placement(
fair.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 去重),避免重复计算。
- 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。
- VMA 建立基线期豁免(
fair.c:1047-1049):
// 前两轮扫描无条件扫描(不检查 PID 活跃度)
if ((mm->numa_scan_seq - vma->numab_state->start_scan_seq) < 2)
return true;新建的 VMA 在前两轮内强制扫描,确保建立足够的统计基线。
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 影响其他线程。
struct vma_numab_state(include/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 链表查找,在进程数量多时性能优势显著。
透明大页(THP,2MB hugepage)对 AutoNUMA 有特殊处理:
- 跳过 hugetlb 页(
fair.c:1032):
if (is_vm_hugetlb_page(vma)) {
skip_reason = NUMAB_SKIP_UNSUITABLE;
continue;
}hugetlb(固定大页)因为 NUMA hint PTE 机制与普通页不同(ARM64 上 Block descriptor 格式差异),暂不支持 AutoNUMA。
- 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 机制)。
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 监控。
proc_sched_show_task() 通过 sched_show_numa() 函数(kernel/sched/debug.c:1236-1249)输出每个任务的 NUMA 统计:
cat /proc/$(pgrep myapp)/schedNUMA 相关输出字段(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 # 跨域唤醒次数
调度器 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)/sys/kernel/debug/energy_model/ 由 kernel/power/energy_model.c 中 em_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)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.h 和 include/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>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 减少不必要迁移,该比例应降低)。
场景描述: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
场景描述:两路 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 的扫描开销反而成了负担。
场景描述:双路服务器,每路附加 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 带宽上限。
当前 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 距离因子
当前 Memory Tiering 基于简单的时间窗口判断热度(numa_hint_fault_latency()),未来可能引入:
- 访问频率预测(基于 PMU 硬件计数器)
- 工作集分析(结合 kswapd 的 refault distance 机制)
- 跨层次协同(DRAM/CXL/NVMe 三级分层)
当前 EAS 明确不支持 SMT(sched_smt_active() 检查),主要原因是 SMT 的 CPU 共享 L1/L2 cache,两个超线程的利用率叠加时能量模型不准确。
未来可能通过引入 em_perf_domain 的 SMT 感知版本(考虑超线程间的功耗耦合),使 EAS 支持超线程开启的 x86 服务器,扩大 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) |
| 路径 | 默认值 | 说明 |
|---|---|---|
/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) |
| 计数器 | 说明 |
|---|---|
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->慢层 降级(直接回收) |
| 路径 | 说明 |
|---|---|
/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 |
最大冷却状态(对应最低频率) |
| 函数名 | 文件:行号 | 功能简述 |
|---|---|---|
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 分析生成