一、简介

在现代操作系统中,CPU调度器不仅需要公平地分配计算资源,更需要精准地感知系统负载状况,以便做出智能化的调度决策。Linux内核的完全公平调度器(Completely Fair Scheduler, CFS)作为默认的进程调度器,其核心创新之一便是引入了Per-Entity Load Tracking(PELT)机制,而util_avg正是这一机制中最关键的指标之一。

util_avg(Utilization Average)用于量化任务或CPU运行队列对处理器的实际占用比例,与load_avg(反映任务权重和可运行时间)不同,util_avg直接度量了任务在物理CPU上实际执行的时间占比。这一指标在现代Linux内核中承担着多重关键职责:

  1. CPU频率调节(DVFS)schedutil Governor直接基于util_avg计算目标频率,实现能效优化

  2. 负载均衡决策:在异构多核(big.LITTLE)架构中,调度器依据util_avg判断任务计算需求,实现任务与CPU能力的匹配

  3. 任务放置策略:Energy Aware Scheduling(EAS)利用util_avg预测任务能耗,选择最优运行CPU

  4. 容量感知调度:确保不同性能等级的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_avgload_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架构(推荐支持schedutil Governor的现代处理器)

  • 内存:≥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的阶段,频率应降至最低(受suspendidle状态影响)

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或异常值?

可能原因

  1. 内核配置问题CONFIG_SCHED_DEBUG未启用,导致/proc/sched_debug不显示详细数据

  2. 权限问题:读取/proc/sched_debug需要root权限

  3. 架构差异:某些架构(如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_avgtop命令显示的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 性能优化建议

  1. 避免不必要的任务迁移:任务迁移会触发util_avg的重新计算和传播,增加调度开销。在NUMA系统中,尽量保持任务在本地CPU运行。

  2. 合理设置schedutilrate_limit_us

    # 对于延迟敏感型应用,可适当降低限制(默认可能为2000us)
    echo 500 | sudo tee /sys/devices/system/cpu/cpufreq/schedutil/rate_limit_us
  3. 利用util_est优化唤醒延迟:确保内核启用UTIL_EST特性,这对间歇性工作负载(如Web服务器)尤为重要。

  4. 监控cfs_rqutil_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内核进行动态频率调节负载均衡容量感知调度的关键输入。

核心要点回顾

  1. 计算机制:基于1024us时间段的指数衰减几何级数,util_sum累加实际运行时间,util_avg = util_sum / divider

  2. load_avg区别util_avg仅度量实际运行时间,与优先级无关;load_avg包含可运行时间和权重

  3. 初始化策略:新任务通过post_init_entity_util_avg进行外推初始化,避免冷启动问题

  4. 频率调节schedutil Governor使用公式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行为,从而在实际项目中做出更明智的调度策略决策。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐