Linux CFS 的 util_avg:CPU 利用率的精准估计
一、简介
在现代操作系统中,CPU调度器不仅需要公平地分配计算资源,更需要精准地感知系统负载状况,以便做出智能化的调度决策。Linux内核的完全公平调度器(Completely Fair Scheduler, CFS)作为默认的进程调度器,其核心创新之一便是引入了Per-Entity Load Tracking(PELT)机制,而util_avg正是这一机制中最关键的指标之一。
util_avg(Utilization Average)用于量化任务或CPU运行队列对处理器的实际占用比例,与load_avg(反映任务权重和可运行时间)不同,util_avg直接度量了任务在物理CPU上实际执行的时间占比。这一指标在现代Linux内核中承担着多重关键职责:
-
CPU频率调节(DVFS):
schedutilGovernor直接基于util_avg计算目标频率,实现能效优化 -
负载均衡决策:在异构多核(big.LITTLE)架构中,调度器依据
util_avg判断任务计算需求,实现任务与CPU能力的匹配 -
任务放置策略:Energy Aware Scheduling(EAS)利用
util_avg预测任务能耗,选择最优运行CPU -
容量感知调度:确保不同性能等级的CPU上任务获得一致的执行比例
掌握util_avg的计算原理与应用方法,对于从事内核开发、系统性能优化、实时系统设计的工程师而言,是理解现代Linux调度体系不可或缺的一环。本文将从源码层面深入剖析其实现机制,并提供可复现的实验方法与调试技巧。
二、核心概念
2.1 PELT(Per-Entity Load Tracking)基础
PELT是Linux内核中用于跟踪调度实体负载的核心机制,由内核开发者Paul Turner在3.8版本引入。其核心思想是将时间划分为约1ms(1024μs)的片段,通过指数衰减的方式计算历史负载的加权总和。
PELT为每个调度实体(struct sched_entity)和CFS运行队列(struct cfs_rq)维护以下关键指标:
| 指标 | 含义 | 计算公式 | 应用场景 |
|---|---|---|---|
load_avg |
加权负载平均值 | load_sum / divider |
反映任务权重与可运行时间,用于负载均衡 |
util_avg |
利用率平均值 | util_sum / divider |
反映实际CPU占用时间,用于频率调节 |
runnable_avg |
可运行平均值 | runnable_sum / divider |
反映任务处于可运行状态的时间 |
其中divider为PELT算法中的归一化因子(通常为LOAD_AVG_MAX,即47742),用于将累加和转换为归一化的平均值。
2.2 util_avg 与 load_avg 的本质区别
理解util_avg与load_avg的差异至关重要:
-
load_avg:考虑任务权重(nice值)和可运行时间(runnable),即使任务在等待调度(处于就绪队列但未运行),其load_avg仍会持续累积。这主要用于负载均衡,确保高优先级任务获得更多CPU份额。 -
util_avg:仅考虑任务实际运行时间(running),且与任务优先级无关,纯粹反映任务对物理CPU资源的实际消耗。这主要用于频率调节和容量规划,因为CPU频率只需要根据实际计算需求调整,与任务优先级无关。
2.3 指数衰减算法
PELT采用几何级数进行指数衰减,衰减系数y的选择使得32ms前的贡献权重减半(即y^32 = 0.5)。数学表达式为:
Load = u_0 + u_1*y + u_2*y^2 + u_3*y^3 + ...
其中u_n表示n个时间单位前的负载贡献(对于util_avg,任务运行时为SCHED_CAPACITY_SCALE,通常为1024;休眠时为0)。
这种设计使得util_avg既能快速响应负载变化(近期权重高),又能平滑短期波动(历史数据衰减贡献),非常适合用于指导硬件频率调节。
三、环境准备
3.1 硬件环境
-
处理器:x86_64或ARM64架构(推荐支持
schedutilGovernor的现代处理器) -
内存:≥4GB RAM
-
存储:≥20GB可用空间(用于内核编译)
3.2 软件环境
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| 操作系统 | Ubuntu 22.04 LTS / Fedora 39 | 稳定发行版,内核源码易获取 |
| 内核版本 | Linux 5.15+ | PELT算法已成熟,util_est功能完善 |
| 编译工具 | gcc 11+, make, bc | 内核编译依赖 |
| 调试工具 | bpftrace, perf, trace-cmd | 用于观测util_avg实时变化 |
| 分析工具 | Python 3.9+, matplotlib | 用于数据可视化 |
3.3 环境配置步骤
步骤1:获取内核源码
# 下载与当前运行版本匹配的内核源码
uname -r # 查看当前内核版本,例如 5.15.0-91-generic
# 方法1:从apt获取(Ubuntu)
apt source linux-image-$(uname -r)
# 方法2:从kernel.org获取主线版本
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.tar.xz
tar -xf linux-5.15.tar.xz
cd linux-5.15
步骤2:安装编译依赖
# Ubuntu/Debian
sudo apt update
sudo apt install build-essential libncurses-dev bison flex libssl-dev \
libelf-dev bc dwarves zstd
# Fedora/RHEL
sudo dnf install gcc make ncurses-devel bison flex openssl-devel \
elfutils-libelf-devel bc dwarves zstd
步骤3:配置内核编译选项
# 复制当前内核配置作为基础
cp /boot/config-$(uname -r) .config
# 确保以下配置项已启用(用于PELT和调度调试)
cat >> .config << EOF
CONFIG_SCHED_DEBUG=y
CONFIG_SCHEDSTATS=y
CONFIG_DEBUG_KERNEL=y
CONFIG_FTRACE=y
CONFIG_DYNAMIC_FTRACE=y
CONFIG_BPF=y
CONFIG_BPF_SYSCALL=y
CONFIG_BPF_EVENTS=y
EOF
# 图形化配置(可选)
make menuconfig
# 编译内核(根据CPU核心数调整-j参数)
make -j$(nproc)
# 安装模块
sudo make modules_install
# 安装内核
sudo make install
# 重启进入新内核
sudo reboot
步骤4:验证schedutil Governor可用性
# 检查当前CPU频率调节策略
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
# 如果返回不是schedutil,切换到schedutil
echo schedutil | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
# 确认切换成功
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor # 应输出:schedutil
四、应用场景
util_avg在现代计算系统中具有广泛而关键的应用。在移动设备与嵌入式系统中,Android和嵌入式Linux广泛采用schedutil Governor,该Governor直接使用CFS运行队列的util_avg作为输入信号,通过公式f = 1.25 * f_0 * util / max计算目标频率,实现毫秒级的频率响应,相比传统的ondemand Governor显著降低调度延迟与能耗。在数据中心与云计算场景中,Kubernetes等容器编排平台结合cgroups v2的CPU统计信息,利用util_avg评估容器实际计算需求,实现更精细的CPU配额管理与节点负载预测,避免资源超售导致的性能抖动。对于异构多核处理器(如ARM big.LITTLE),util_avg是容量感知调度的核心输入,调度器通过比较任务util_avg与CPU容量,将重计算任务放置在高性能核心(big core),轻量任务保留在能效核心(LITTLE core),在性能与功耗间取得平衡。在实时系统中,虽然util_avg主要用于CFS类任务,但结合UTIL_EST特性可预测任务唤醒后的计算需求,辅助SCHED_DEADLINE的带宽控制与准入测试,确保硬实时任务的确定性执行。
五、实际案例与步骤
5.1 案例1:观测任务级util_avg变化
本案例通过bpftrace工具实时追踪特定任务的util_avg变化,验证PELT算法的衰减特性。
步骤1:编写BPF追踪脚本
# 创建trace_util_avg.bt文件
cat > trace_util_avg.bt << 'EOF'
#!/usr/bin/env bpftrace
#include <linux/sched.h>
// 追踪目标进程的PID(通过参数传入)
BEGIN {
printf("Tracing util_avg for PID %d. Press Ctrl-C to stop.\n", $1);
printf("%-10s %-20s %-10s %-10s %-10s\n",
"TIME(ms)", "COMM", "PID", "UTIL_AVG", "LOAD_AVG");
}
// 在update_load_avg函数入口追踪(该函数定期更新PELT指标)
kprobe:update_load_avg {
$task = (struct task_struct *)arg1;
// 只追踪目标PID
if ($task->pid == $1) {
$se = &$task->se;
$util_avg = $se->avg.util_avg;
$load_avg = $se->avg.load_avg;
printf("%-10u %-20s %-10d %-10lu %-10lu\n",
nsecs / 1000000,
$task->comm,
$task->pid,
$util_avg,
$load_avg);
}
}
END {
printf("Tracing completed.\n");
}
EOF
chmod +x trace_util_avg.bt
步骤2:创建测试负载程序
// cpu_burner.c - 创建可控的CPU负载
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <signal.h>
#include <sched.h>
#include <string.h>
volatile int running = 1;
void signal_handler(int sig) {
running = 0;
}
// 获取纳秒级时间戳
unsigned long long get_ns() {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return ts.tv_sec * 1000000000ULL + ts.tv_nsec;
}
// 绑定到指定CPU
void bind_cpu(int cpu) {
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(cpu, &cpuset);
if (sched_setaffinity(0, sizeof(cpuset), &cpuset) < 0) {
perror("sched_setaffinity");
exit(1);
}
}
int main(int argc, char *argv[]) {
if (argc < 3) {
printf("Usage: %s <cpu_id> <duty_cycle_percent> [duration_sec]\n", argv[0]);
printf("Example: %s 0 50 10 (CPU0上50%%占空比运行10秒)\n", argv[0]);
return 1;
}
int target_cpu = atoi(argv[1]);
int duty_cycle = atoi(argv[2]); // 占空比:0-100
int duration = (argc > 3) ? atoi(argv[3]) : 30;
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
bind_cpu(target_cpu);
unsigned long long period_ns = 100000000ULL; // 100ms周期
unsigned long long run_ns = period_ns * duty_cycle / 100;
unsigned long long sleep_ns = period_ns - run_ns;
printf("PID: %d, CPU: %d, Duty Cycle: %d%%, Period: %llu ms\n",
getpid(), target_cpu, duty_cycle, period_ns / 1000000);
unsigned long long start_time = get_ns();
unsigned long long next_wake = start_time;
long long iterations = 0;
while (running && (get_ns() - start_time) < (unsigned long long)duration * 1000000000ULL) {
// 运行阶段:忙等待消耗CPU
unsigned long long run_start = get_ns();
while ((get_ns() - run_start) < run_ns) {
// 执行无意义计算,防止编译器优化
volatile double dummy = 0;
for (int i = 0; i < 1000; i++) {
dummy += i * 3.14159;
}
iterations++;
}
// 睡眠阶段:让出CPU
if (sleep_ns > 0) {
next_wake += period_ns;
struct timespec ts = {
.tv_sec = 0,
.tv_nsec = sleep_ns
};
nanosleep(&ts, NULL);
}
}
printf("Completed. Total iterations: %lld\n", iterations);
return 0;
}
步骤3:编译并执行测试
# 编译测试程序
gcc -O2 -o cpu_burner cpu_burner.c
# 终端1:启动BPF追踪(需要root权限)
sudo bpftrace trace_util_avg.bt $(pgrep -f cpu_burner || echo "等待进程启动...")
# 终端2:启动CPU负载程序(50%占空比,绑定CPU0,运行30秒)
./cpu_burner 0 50 30
# 观察输出示例:
# TIME(ms) COMM PID UTIL_AVG LOAD_AVG
# 100 cpu_burner 12345 512 1024
# 200 cpu_burner 12345 768 1024
# 300 cpu_burner 12345 896 1024
# ... ... ... ... ...
预期现象分析:
-
UTIL_AVG会从0开始逐渐上升,最终稳定在约SCHED_CAPACITY_SCALE * duty_cycle / 100(即512左右,因为占空比为50%) -
LOAD_AVG会迅速上升至1024(满权重),因为任务始终处于可运行状态(即使睡眠也是主动让出,仍被视为可运行)
5.2 案例2:验证schedutil Governor的频率调节
本案例展示util_avg如何直接影响CPU频率决策。
步骤1:创建频率监控脚本
cat > monitor_freq.sh << 'EOF'
#!/bin/bash
CPU=$1
INTERVAL=${2:-0.1} # 默认100ms采样间隔
DURATION=${3:-30} # 默认运行30秒
echo "Monitoring CPU $CPU frequency and util_avg for ${DURATION}s..."
echo "Timestamp,Frequency(kHz),util_avg,Target_Freq(kHz)"
end_time=$(($(date +%s) + DURATION))
while [ $(date +%s) -lt $end_time ]; do
# 读取当前频率
freq=$(cat /sys/devices/system/cpu/cpu${CPU}/cpufreq/scaling_cur_freq 2>/dev/null || echo "N/A")
# 从sched_debug读取util_avg(需要root)
util_avg=$(sudo cat /proc/sched_debug 2>/dev/null | \
grep -A 20 "cpu#${CPU}," | \
grep "avg.util_avg" | \
awk '{print $2}' || echo "N/A")
# 计算schedutil的理论目标频率(简化公式)
# f = 1.25 * f_max * util_avg / 1024
max_freq=$(cat /sys/devices/system/cpu/cpu${CPU}/cpufreq/cpuinfo_max_freq)
target_freq=$(echo "scale=0; 1.25 * $max_freq * $util_avg / 1024" | bc 2>/dev/null || echo "N/A")
echo "$(date '+%H:%M:%S.%N'),$freq,$util_avg,$target_freq"
sleep $INTERVAL
done
EOF
chmod +x monitor_freq.sh
步骤2:创建阶梯负载测试程序
// step_load.c - 产生阶梯式CPU负载
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <time.h>
volatile int running = 1;
void handler(int sig) { running = 0; }
int main() {
signal(SIGINT, handler);
signal(SIGTERM, handler);
// 阶段定义:每个阶段持续5秒,占空比递增
int phases[] = {0, 25, 50, 75, 100, 75, 50, 25, 0};
int num_phases = sizeof(phases) / sizeof(phases[0]);
int phase_duration = 5; // 秒
printf("PID: %d - Step load test starting\n", getpid());
for (int i = 0; i < num_phases && running; i++) {
int duty = phases[i];
printf("Phase %d/%d: Duty cycle = %d%% (duration: %ds)\n",
i+1, num_phases, duty, phase_duration);
time_t phase_start = time(NULL);
unsigned long long period = 100000000ULL; // 100ms
while (running && (time(NULL) - phase_start) < phase_duration) {
unsigned long long run_time = period * duty / 100;
unsigned long long sleep_time = period - run_time;
// 运行期
unsigned long long start = clock();
while ((clock() - start) < run_time * CLOCKS_PER_SEC / 1000000000ULL) {
volatile int x = 0;
for (int j = 0; j < 10000; j++) x += j;
}
// 睡眠期
if (sleep_time > 0) {
usleep(sleep_time / 1000);
}
}
}
printf("Test completed\n");
return 0;
}
步骤3:执行联合测试
# 编译阶梯负载程序
gcc -O2 -o step_load step_load.c
# 终端1:监控CPU0的频率和util_avg
sudo ./monitor_freq.sh 0 0.1 60 > freq_log.csv &
# 终端2:运行阶梯负载(绑定CPU0)
taskset -c 0 ./step_load
# 终端3:同时查看schedutil的决策日志(如内核启用相关tracepoint)
sudo trace-cmd start -e schedutil:schedutil_update
sudo trace-cmd show > schedutil_trace.txt
sudo trace-cmd stop
# 分析结果:绘制频率-负载关系图
python3 << 'PYEOF'
import pandas as pd
import matplotlib.pyplot as plt
df = pd.read_csv('freq_log.csv', skipinitialspace=True)
df['Timestamp'] = pd.to_datetime(df['Timestamp'])
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8), sharex=True)
ax1.plot(df['Timestamp'], df['Frequency(kHz)'], label='Actual Frequency', color='blue')
ax1.plot(df['Timestamp'], df['Target_Freq(kHz)'], label='Target Frequency',
linestyle='--', color='red', alpha=0.7)
ax1.set_ylabel('Frequency (kHz)')
ax1.legend()
ax1.set_title('CPU Frequency vs util_avg under schedutil Governor')
ax2.plot(df['Timestamp'], df['util_avg'], label='util_avg', color='green')
ax2.set_ylabel('util_avg')
ax2.set_xlabel('Time')
ax2.legend()
plt.tight_layout()
plt.savefig('freq_util_analysis.png', dpi=150)
print("Analysis chart saved to freq_util_analysis.png")
PYEOF
关键观察点:
-
当
util_avg上升时,实际频率应跟随目标频率上升,但受硬件调节延迟影响可能存在滞后 -
当
util_avg下降时,频率应随之降低,验证PELT的衰减特性 -
在
util_avg为0的阶段,频率应降至最低(受suspend或idle状态影响)
5.3 案例3:内核源码级分析util_avg计算
本案例通过阅读内核源码,理解util_avg的精确计算流程。
步骤1:定位关键源码文件
# 在源码目录中查找PELT相关实现
find /usr/src/linux-$(uname -r) -name "*.c" -o -name "*.h" | xargs grep -l "util_avg" 2>/dev/null
# 核心文件:
# kernel/sched/fair.c - CFS调度器主实现
# kernel/sched/pelt.c - PELT算法核心(高版本内核)
# kernel/sched/sched.h - 数据结构定义
步骤2:分析___update_load_sum函数(PELT核心)
// 文件:kernel/sched/fair.c 或 kernel/sched/pelt.c(内核版本差异)
// 函数:___update_load_sum - 计算指数衰减和
/*
* 核心算法说明:
* 1. 时间被划分为1024us的段(约1ms)
* 2. 使用几何级数进行指数衰减,衰减因子y满足 y^32 = 0.5
* 3. 对于util_sum,任务运行时每段贡献SCHED_CAPACITY_SCALE(1024),休眠时贡献0
*/
static __always_inline int
___update_load_sum(u64 now, int cpu, struct sched_avg *sa,
unsigned long load, unsigned long runnable, int running)
{
u64 delta;
// 计算距离上次更新的时间差
delta = now - sa->last_update_time;
delta >>= 10; // 转换为1024us为单位
if (!delta)
return 0; // 无需更新
// 衰减历史贡献(通过查表或计算实现)
// decay_load()实现了 y^n 的快速计算
sa->load_sum = decay_load(sa->load_sum, delta);
sa->util_sum = decay_load(sa->util_sum, delta);
sa->runnable_sum = decay_load(sa->runnable_sum, delta);
// 累加当前周期的贡献
sa->load_sum += load * SCHED_CAPACITY_SCALE;
if (running)
sa->util_sum += SCHED_CAPACITY_SCALE * SCHED_CAPACITY_SCALE;
sa->runnable_sum += runnable * SCHED_CAPACITY_SCALE;
sa->last_update_time = now;
return 1;
}
步骤3:分析___update_load_avg函数(平均值计算)
// 文件:kernel/sched/fair.c
// 函数:___update_load_avg - 将累加和转换为平均值
static __always_inline void
___update_load_avg(struct sched_avg *sa, unsigned long load)
{
u32 divider = get_pelt_divider(sa);
// divider = LOAD_AVG_MAX - 1024 + sa->period_contrib
// 其中LOAD_AVG_MAX = 47742(32个半衰期后的级数和)
sa->load_avg = div_u64(load * sa->load_sum, divider);
sa->runnable_avg = div_u64(sa->runnable_sum, divider);
// util_avg计算:直接除法,因为util_sum已经是权重形式
WRITE_ONCE(sa->util_avg, sa->util_sum / divider);
}
步骤4:分析任务唤醒时的util_avg初始化
// 文件:kernel/sched/fair.c
// 函数:post_init_entity_util_avg - 新任务util_avg初始化
/*
* 新创建任务的util_avg初始化策略:
* 为了避免新任务从0开始缓慢上升导致的频率调节滞后,
* 内核根据当前CFS队列的负载状况进行 extrapolation(外推)
*/
void post_init_entity_util_avg(struct sched_entity *se)
{
struct cfs_rq *cfs_rq = cfs_rq_of(se);
struct sched_avg *sa = &se->avg;
long cpu_scale = arch_scale_cpu_capacity(cpu_of(rq_of(cfs_rq)));
// 公式:util_avg = cfs_rq->util_avg / (cfs_rq->load_avg + 1) * se_weight(se)
// 这确保新任务的初始util_avg与队列当前负载成比例
if (cfs_rq->avg.util_avg) {
sa->util_avg = cfs_rq->avg.util_avg * se_weight(se);
sa->util_avg /= cfs_rq->avg.load_avg + 1;
} else {
// 如果队列为空,初始化为CPU容量的一半,避免冷启动问题
sa->util_avg = cpu_scale / 2;
}
// 应用上限:防止新任务序列发散(几何级数求和约束)
// util_avg_cap = (cpu_scale - cfs_rq->util_avg) / 2^n
// 这确保多个新任务不会导致util_avg总和超过CPU容量
if (sa->util_avg > sa->util_avg_cap)
sa->util_avg = sa->util_avg_cap;
}
步骤5:分析schedutil Governor的调用路径
// 文件:drivers/cpufreq/schedutil.c(或kernel/sched/cpufreq_schedutil.c)
// 函数:sugov_update_single 或 sugov_update_shared
/*
* schedutil Governor使用util_avg计算目标频率:
*
* f = 1.25 * f_0 * util / max
*
* 其中:
* - util = cfs_rq->avg.util_avg(CFS根控制组的PELT值)
* - max = SCHED_CAPACITY_SCALE(通常为1024)
* - f_0 = 最大频率(如果PELT是频率不变的)或当前频率
*
* 1.25系数提供25%的性能余量,确保响应性
*/
static void sugov_update_single(struct update_util_data *hook, u64 time,
unsigned int flags)
{
struct sugov_cpu *sg_cpu = container_of(hook, struct sugov_cpu, update_util);
struct sugov_policy *sg_policy = sg_cpu->sg_policy;
unsigned int cpu = sg_cpu->cpu;
// 获取CFS的util_avg(通过schedutil接口)
unsigned long util = cpu_util_cfs(cpu); // 内部读取cfs_rq->avg.util_avg
unsigned long max = arch_scale_cpu_capacity(cpu);
// 计算目标频率
unsigned long freq = sg_policy->policy->cpuinfo.max_freq;
freq = freq * util / max;
freq = freq * 125 / 100; // 1.25倍系数
// 应用频率限制
freq = clamp(freq, sg_policy->policy->cpuinfo.min_freq,
sg_policy->policy->cpuinfo.max_freq);
// 设置频率(异步或同步取决于驱动能力)
cpufreq_driver_target(sg_policy->policy, freq, CPUFREQ_RELATION_L);
}
六、常见问题与解答
Q1:为什么我的util_avg始终显示为0或异常值?
可能原因:
-
内核配置问题:
CONFIG_SCHED_DEBUG未启用,导致/proc/sched_debug不显示详细数据 -
权限问题:读取
/proc/sched_debug需要root权限 -
架构差异:某些架构(如RISC-V早期支持)可能未完全实现PELT
排查步骤:
# 检查内核配置
grep CONFIG_SCHED_DEBUG /boot/config-$(uname -r)
# 确认sched_debug输出
sudo cat /proc/sched_debug | grep -A 5 "cpu#0," | head -20
# 检查PELT是否启用(通过sched_features)
cat /sys/kernel/debug/sched/features | grep PELT
# 应包含:PELT_UTIL_AVG PELT_UTIL_EST
Q2:util_avg与top命令显示的CPU使用率有何区别?
关键差异:
| 特性 | util_avg(PELT) |
top命令 |
|---|---|---|
| 计算方式 | 指数衰减加权平均 | 瞬时采样(通常1秒间隔) |
| 时间粒度 | ~1ms(1024us) | 1秒或自定义 |
| 包含阻塞 | 包含历史衰减(任务休眠后仍贡献) | 仅统计采样周期内 |
| 用途 | 调度决策、频率调节 | 用户观察系统状态 |
| 延迟 | 毫秒级响应 | 秒级更新 |
top显示的是瞬时使用率,而util_avg是平滑的历史加权值,更适合预测未来负载。
Q3:如何清零或手动设置util_avg用于测试?
方法:通过sched_debug接口或编写内核模块(仅限调试环境):
# 临时方法:将任务迁移到空闲CPU再迁回,会触发重新计算
taskset -c 1 <pid> # 迁移到CPU1
taskset -c 0 <pid> # 迁回CPU0,触发post_init_entity_util_avg
# 或者使用cgroups重置(会创建新的sched_entity)
echo <pid> > /sys/fs/cgroup/cpuset/new_group/cgroup.procs
注意:生产环境不建议手动干预util_avg,会破坏调度器状态一致性。
Q4:UTIL_EST(Utilization Estimation)与util_avg的关系?
解答:UTIL_EST是内核4.17+引入的优化特性,用于解决任务唤醒时的冷启动问题。
-
util_avg:实时计算的PELT值,任务休眠时持续衰减 -
util_est:任务休眠前保存的util_avg值,作为唤醒时的初始估计
调度器取max(util_avg, util_est)作为最终利用率,确保唤醒后立即获得合理的频率和CPU选择决策。
启用方法:
# 检查是否启用
cat /sys/kernel/debug/sched/features | grep UTIL_EST
# 动态启用(如未启用)
echo UTIL_EST > /sys/kernel/debug/sched/features
Q5:为什么多线程程序的util_avg可能超过SCHED_CAPACITY_SCALE(1024)?
原因:util_avg在CFS运行队列(cfs_rq)级别是可累加的。如果CPU上有多个任务同时运行(超线程或快速切换),cfs_rq->avg.util_avg可能暂时超过单核容量(1024),反映的是总计算需求而非物理执行能力。
在schedutil中,会通过min(util, max_capacity)进行钳制,确保频率计算合理性。
七、实践建议与最佳实践
7.1 调试技巧
技巧1:使用trace-cmd捕获调度事件
# 捕获PELT更新事件(需内核启用相关tracepoint)
sudo trace-cmd start -e sched:sched_pelt_se -e sched:sched_pelt_cfs_rq
# 运行测试程序
./cpu_burner 0 75 10
# 停止并查看
sudo trace-cmd stop
sudo trace-cmd report | head -100
技巧2:通过proc文件系统监控
# 实时监控特定任务的调度统计
watch -n 0.1 'cat /proc/<pid>/sched | grep -E "se\.|avg\."'
# 输出示例:
# se.avg.load_avg : 1024
# se.avg.util_avg : 768
# se.avg.runnable_avg : 1024
7.2 性能优化建议
-
避免不必要的任务迁移:任务迁移会触发
util_avg的重新计算和传播,增加调度开销。在NUMA系统中,尽量保持任务在本地CPU运行。 -
合理设置
schedutil的rate_limit_us:# 对于延迟敏感型应用,可适当降低限制(默认可能为2000us) echo 500 | sudo tee /sys/devices/system/cpu/cpufreq/schedutil/rate_limit_us -
利用
util_est优化唤醒延迟:确保内核启用UTIL_EST特性,这对间歇性工作负载(如Web服务器)尤为重要。 -
监控
cfs_rq级util_avg而非单个任务:对于容量规划,应关注cpu#X/cfs_rq/util_avg,它反映了CPU的总计算需求。
7.3 常见错误解决方案
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
util_avg长时间不更新 |
内核未编译CONFIG_SCHED_DEBUG |
重新编译内核并启用该选项 |
schedutil频率不随负载变化 |
Governor未切换至schedutil | 检查并切换Governor |
任务util_avg始终为0 |
任务为IDLE优先级或从未运行 | 检查任务状态和优先级 |
多核util_avg总和异常 |
未考虑CPU容量缩放 | 使用arch_scale_cpu_capacity归一化 |
八、总结与应用场景
本文深入剖析了Linux CFS调度器中util_avg的计算原理、内核实现及实际应用。util_avg作为PELT机制的核心指标,通过指数衰减算法实现了对CPU利用率的精准、平滑估计,其毫秒级的响应特性使其成为现代Linux内核进行动态频率调节、负载均衡和容量感知调度的关键输入。
核心要点回顾:
-
计算机制:基于1024us时间段的指数衰减几何级数,
util_sum累加实际运行时间,util_avg = util_sum / divider -
与
load_avg区别:util_avg仅度量实际运行时间,与优先级无关;load_avg包含可运行时间和权重 -
初始化策略:新任务通过
post_init_entity_util_avg进行外推初始化,避免冷启动问题 -
频率调节:
schedutilGovernor使用公式f = 1.25 * f_0 * util / max实现能效优化
未来应用场景:
-
AI推理优化:在边缘AI设备中,利用
util_avg预测推理任务的计算需求,动态调整NPU/CPU频率 -
云原生调度:将
util_avg暴露给Kubernetes调度器,实现基于实际负载的Pod放置决策 -
实时系统扩展:将PELT机制扩展至
SCHED_DEADLINE类任务,实现更精确的带宽控制
掌握util_avg不仅是理解Linux调度器的钥匙,更是进行系统级性能优化的基础。建议读者结合实际硬件平台,通过本文提供的BPF追踪和频率监控方法,深入观察工作负载下的util_avg行为,从而在实际项目中做出更明智的调度策略决策。
更多推荐



所有评论(0)