简介

在多核 Linux 系统中,CPU 负载均衡、动态调频调压(DVFS)是保障系统吞吐、响应延迟与功耗平衡的两大核心能力。传统topmpstat等工具读取/proc/stat计算的 CPU 使用率,仅做宏观时间片统计,存在滞后性、粒度粗的问题,无法满足内核调度决策的精细化需求。为此,Linux 内核引入cpu_util(核心底层为util_avg)指标,依托 PELT(Per-Entity Load Tracking,实体级负载追踪)算法,以滑动加权方式实时量化 CPU 及调度实体的真实资源占用比例。

cpu_util 贯穿内核两大核心模块:一是多核负载均衡,调度器依靠该指标判断各 CPU 核心的繁忙程度,决策任务迁移,避免部分核心过载、部分核心空闲的负载倾斜问题;二是CPU 频率调节schedutil调频策略直接读取 cpu_util 数值,动态拉升或降低主频,兼顾性能与功耗。无论是服务器集群、嵌入式终端、工控设备还是移动异构多核平台,cpu_util 都是底层调度与能效管理的核心数据源。

对于 Linux 内核研发、嵌入式开发、性能调优工程师而言,吃透 cpu_util 的计算逻辑、更新时机、数据流转与应用规则,是理解多核调度、定位负载不均、优化调度延迟、调优功耗策略的必备技能。本文从基础概念、环境搭建、源码解析、实操验证、问题排查到工程实践全维度展开,配套可直接运行的代码、调试命令,内容可支撑技术报告、毕业论文撰写以及线上线下故障排查工作。

一、核心概念与术语解析

1.1 区分三类负载指标

很多开发者会混淆load_avgutil_avg(cpu_util)与系统负载,这里做明确界定,也是理解后续内容的基础:

  1. load_avg:调度负载值,和任务优先级、权重强相关,统计任务处于可运行态的时间占比,主要用于 CFS 调度器的公平调度与传统负载均衡,偏向 “任务竞争激烈程度”。
  2. util_avg / cpu_util:CPU 利用率指标,与任务优先级无关,仅统计调度实体在 CPU 上实际运行的时间占比,纯粹表征硬件资源消耗,是本文研究核心。内核中对外暴露的cpu_util本质就是运行队列聚合后的util_avg
  3. 系统 Load/proc/loadavg展示的 1/5/15 分钟平均可运行任务数,是系统全局负载宏观体现,不用于内核实时调度决策。

1.2 PELT 实体级负载追踪算法

cpu_util 的底层计算依赖 PELT 算法,该算法采用指数加权滑动平均模型,特点如下:

  • 时间窗口:默认采用固定时间窗口,对历史数据做指数衰减,越久远的运行记录权重越低,保证指标实时性;
  • 统计粒度:以调度实体(sched_entity) 为最小统计单元,再逐级聚合到运行队列cfs_rq、CPU 运行队列rq
  • 核心优势:支持周期性时钟(Tick)与无时钟(NO_HZ)两种内核模式,在休眠、低功耗场景下依然能精准更新利用率。

1.3 核心结构体与关键字段

1.3.1 调度实体 sched_entity

每个 CFS 任务对应一个sched_entity,内置负载统计结构体:

// kernel/sched/sched.h
struct sched_entity {
    /* CFS调度核心字段:虚拟运行时间 */
    u64                     vruntime;
    /* PELT负载统计结构体 */
    struct sched_avg        avg;
    // 其他CFS相关字段省略
};
1.3.2 负载统计结构体 sched_avg

承载单个任务的负载与利用率数据,是 cpu_util 的数据源:

// kernel/sched/pelt.h
struct sched_avg {
    /* 负载累加值,用于计算load_avg */
    u64                     load_sum;
    /* 利用率累加值,用于计算util_avg */
    u64                     util_sum;
    /* 可运行状态时间累加值 */
    u64                     runnable_sum;
    /* 最终输出:平均负载、平均利用率 */
    u32                     load_avg;
    u32                     util_avg;
    u32                     runnable_avg;
    /* 上一次更新时间戳 */
    u64                     last_update_time;
};

关键字段说明util_avg即为单个任务的 CPU 利用率,多个任务聚合后得到cfs_rq->avg.util_avg,也就是内核接口中常说的cpu_util

1.3.3 CFS 运行队列 cfs_rq

每个 CPU 核心维护独立的cfs_rq,聚合当前核心所有 CFS 任务的负载数据:

// kernel/sched/fair.h
struct cfs_rq {
    struct rb_root          tasks_timeline;
    /* 整个运行队列的聚合负载与利用率 */
    struct sched_avg        avg;
    // 队列任务计数、节流控制等字段省略
};

1.4 调度域与负载均衡基础

多核系统中,内核通过sched_domain调度域划分 CPU 拓扑(NUMA 节点、CPU 簇、物理核心),负载均衡分为三类触发场景:

  • 周期性均衡:内核定时器定时扫描调度域内所有 CPU,对比 cpu_util,迁移过载核心的任务;
  • 空闲均衡:CPU 进入 idle 空闲态时,主动拉取其他繁忙核心的任务;
  • 唤醒均衡:新任务创建、休眠任务唤醒时,依据 cpu_util 选择最优 CPU 绑定。

1.5 调度调频 schedutil

Linux 主流 CPU 调频策略schedutil不再依赖传统的硬件采样,直接读取cfs_rq->avg.util_avg,根据 CPU 利用率阶梯式调整主频:利用率高则升频保障性能,利用率低则降频降低功耗,cpu_util 是调频决策的唯一依据。

二、环境准备

2.1 软硬件环境清单

本文基于主流长期支持内核开发调试,兼容物理机与虚拟机,具体配置如下:

环境分类 版本 / 配置要求 补充说明
操作系统 Ubuntu 20.04 / 22.04 64 位 推荐原生物理机,虚拟机需开启多核
Linux 内核 5.15 LTS、6.1 LTS、6.6 LTS 三个版本 PELT、cpu_util 逻辑完全一致
硬件架构 x86_64 多核 CPU(≥4 核) 单核无法验证负载均衡效果
编译工具 gcc 9.4+、make、binutils 用于编译内核、编写测试程序
调试工具 ftrace、perf、gdb、trace-cmd 跟踪 cpu_util 更新函数、采样利用率
依赖库 libncurses-dev、bison、flex、libelf-dev 内核编译必备依赖

2.2 环境部署步骤

2.2.1 安装基础编译与调试工具

执行以下命令批量安装依赖,可直接复制运行:

# 更新软件源
sudo apt update && sudo apt upgrade -y

# 安装编译、调试全套工具
sudo apt install build-essential libncurses-dev bison flex \
libssl-dev libelf-dev gdb trace-cmd perf -y
2.2.2 下载并编译 Linux 6.1 LTS 内核

该版本为工业、服务器场景主流版本,源码逻辑稳定:

# 下载内核源码
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.1.tar.xz
# 解压源码包
tar -xf linux-6.1.tar.xz
cd linux-6.1
2.2.3 内核配置(开启调度调试与 PELT)
# 继承当前系统内核配置
cp /boot/config-$(uname -r) .config

# 图形化配置界面
make menuconfig

必须开启的核心配置项(保证 cpu_util 可追踪、调试):

CONFIG_PELT=y                  # 启用PELT负载追踪(cpu_util依赖)
CONFIG_SCHED_DEBUG=y           # 调度器调试开关
CONFIG_FTRACE=y                # 函数跟踪,观测util更新流程
CONFIG_SCHEDUTIL=y             # 启用schedutil调频策略
CONFIG_NO_HZ_COMMON=y          # 支持无时钟模式,验证极端场景
CONFIG_DEBUG_KERNEL=y          # 内核调试

配置完成后保存退出。

2.2.4 编译、安装内核并重启
# 多线程编译,nproc为CPU核心数,加速编译
make -j$(nproc)

# 安装内核模块
sudo make modules_install

# 安装新内核镜像
sudo make install

# 更新系统引导项
sudo update-grub

执行完成后重启服务器,在 GRUB 引导界面选择新编译的Linux 6.1内核进入。

2.2.5 源码路径定位

后续解析、调试均基于以下路径,提前熟记:

  1. PELT 算法、util_avg 计算核心:kernel/sched/pelt.c
  2. CFS 运行队列聚合逻辑:kernel/sched/fair.c
  3. 负载均衡决策逻辑:kernel/sched/sched.hkernel/sched/balancing.c
  4. schedutil 调频逻辑:kernel/sched/cpufreq_schedutil.c

三、应用场景

cpu_util 作为内核统一的 CPU 利用率指标,在工业生产、服务器运维、嵌入式设备三大领域落地广泛。在云服务器集群中,调度器依托各核心 cpu_util 数值做精细化负载均衡,避免单机 CPU 核心负载分化,保障虚拟机、容器服务的响应时延稳定,同时配合 schedutil 调频降低整机功耗。在工业嵌入式异构多核平台(Big.LITTLE 架构)中,系统根据任务计算强度对应的 cpu_util,将高负载任务调度至大核、轻负载任务调度至小核,兼顾实时性与续航。在高性能计算集群中,运维人员通过抓取内核 cpu_util 原始数据,替代传统工具做细粒度性能分析,定位 CPU 密集型任务、线程漂移、负载不均等问题。此外,移动端、车载操作系统的功耗优化模块,也完全基于 cpu_util 动态调整 CPU 主频与核心上下线,是软硬件协同优化的核心桥梁。

四、实际案例与源码深度剖析

本章节从底层计算源码数据聚合逻辑内核接口调用用户态测试ftrace 动态跟踪五个维度展开,所有代码均可直接编译、运行。

4.1 PELT 算法:util_avg 核心计算源码

___update_load_sum 是更新util_sumutil_avg的底层函数,位于kernel/sched/pelt.c,也是 cpu_util 的数据源头:

/**
 * ___update_load_sum - 更新调度实体的利用率累加值
 * @delta: 距离上一次更新的时间差(纳秒)
 * @curr: 当前调度实体
 * @running: 标记任务是否处于CPU运行态
 */
static void ___update_load_sum(u64 delta, struct sched_avg *sa, int running)
{
    u64 decay;
    u32 scale;

    /* 1. 计算历史数据衰减系数:指数加权滑动平均核心 */
    decay = pelt_decay_sample(delta);
    scale = pelt_scale_from_decay(decay);

    /* 2. 对历史util_sum做衰减,弱化久远数据的影响 */
    sa->util_sum = (sa->util_sum * scale) >> PELT_SHIFT;

    /* 3. 仅当任务在CPU上运行时,才累加运行时间到util_sum */
    if (running)
        sa->util_sum += delta << PELT_SHIFT;
}

代码说明

  1. delta:两次统计之间的时间间隔,单位纳秒,由内核时钟或任务状态切换触发计算;
  2. decay/scale:指数衰减系数,时间越久,历史利用率数据权重越低,保证指标实时性;
  3. running 是关键判断:任务仅在运行态才会累加 util_sum,休眠、就绪态不会计入,这也是 cpu_util 只统计真实 CPU 占用的原因。

4.2 计算最终 util_avg(单任务利用率)

pelt.c中,___update_load_avg 将累加值换算为最终的平均利用率util_avg

/**
 * ___update_load_avg - 将sum累加值换算为平均利用率util_avg
 * @sa: 调度实体的sched_avg结构体
 */
static void ___update_load_avg(struct sched_avg *sa)
{
    /* 换算为0~1024区间的利用率值(SCHED_CAPACITY_SCALE = 1024) */
    sa->util_avg = (sa->util_sum * SCHED_CAPACITY_SCALE) >> PELT_AVG_SHIFT;
}

代码说明: 内核约定SCHED_CAPACITY_SCALE = 1024,即util_avg取值范围 0 ~ 1024

  • 0:CPU 完全空闲,无任务运行;
  • 1024:CPU 满载,资源被完全占用; 该统一标尺让负载均衡、调频模块可以无差别读取和对比利用率。

4.3 队列聚合:单 CPU 核心 cpu_util 生成逻辑

单个任务的util_avg需要逐级聚合到cfs_rq,最终形成整个 CPU 核心的 cpu_util,代码位于kernel/sched/fair.c

/**
 * __update_load_avg_cfs_rq - 聚合队列内所有任务的util,生成CPU级利用率
 * @rq: 当前CPU的CFS运行队列
 * @delta: 时间差值
 */
void __update_load_avg_cfs_rq(struct cfs_rq *cfs_rq, u64 delta)
{
    struct sched_avg *sa = &cfs_rq->avg;
    /* 1. 更新队列整体的util_sum,逻辑同单任务 */
    ___update_load_sum(delta, sa, cfs_rq->nr_running > 0);
    /* 2. 计算队列最终util_avg,即该CPU核心的cpu_util */
    ___update_load_avg(sa);
}

核心逻辑:当队列中有运行任务(nr_running > 0),则标记为running状态,累加运行时间;最终cfs_rq->avg.util_avg就是调度器使用的CPU 核心利用率(cpu_util)

4.4 内核对外接口:cpu_util 读取函数

内核负载均衡、schedutil 调频模块通过专用接口获取指定 CPU 的 cpu_util,源码kernel/sched/cpufreq_schedutil.c

/**
 * cpu_util - 获取指定CPU的当前利用率
 * @cpu: CPU核心编号
 * 返回值:0 ~ 1024 的利用率数值
 */
unsigned long cpu_util(int cpu)
{
    struct rq *rq = cpu_rq(cpu);
    struct cfs_rq *cfs_rq = &rq->cfs;

    /* 原子读取,避免数据竞争 */
    return READ_ONCE(cfs_rq->avg.util_avg);
}

使用场景:该函数是全内核通用接口,负载均衡模块调用它对比多个 CPU 的繁忙程度,schedutil 调频模块调用它计算目标运行主频。

4.5 编写用户态测试程序:压测 CPU 并观测利用率变化

编写 CPU 密集型测试程序,绑定指定 CPU 核心,人为制造负载差异,用于验证 cpu_util 与负载均衡效果。代码cpu_stress.c

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>

// 线程数量,模拟多任务压测
#define THREAD_NUM 4

// 线程执行函数:死循环消耗CPU资源
void *cpu_load_func(void *arg)
{
    (void)arg;
    while(1)
    {
        // 空循环,纯CPU密集运算
        __asm__ __volatile__("nop");
    }
    return NULL;
}

int main(int argc, char *argv[])
{
    pthread_t tid[THREAD_NUM];
    cpu_set_t cpuset;
    int ret, i;
    int bind_cpu = 0;  // 绑定任务到CPU0,制造单核高负载

    printf("Start CPU stress test, bind all task to CPU%d\n", bind_cpu);

    // 初始化CPU掩码,绑定到指定核心
    CPU_ZERO(&cpuset);
    CPU_SET(bind_cpu, &cpuset);

    // 设置当前进程亲和性,固定运行在CPU0
    ret = sched_setaffinity(getpid(), sizeof(cpu_set_t), &cpuset);
    if(ret != 0)
    {
        perror("sched_setaffinity failed");
        return -1;
    }

    // 创建多个压力线程
    for(i = 0; i < THREAD_NUM; i++)
    {
        ret = pthread_create(&tid[i], NULL, cpu_load_func, NULL);
        if(ret != 0)
        {
            perror("pthread_create failed");
            return -1;
        }
    }

    // 等待线程(永不退出)
    for(i = 0; i < THREAD_NUM; i++)
    {
        pthread_join(tid[i], NULL);
    }

    return 0;
}
编译与运行命令
# 编译代码,链接线程库
gcc cpu_stress.c -o cpu_stress -lpthread

# 后台运行压力程序,绑定CPU0制造高负载
./cpu_stress &

实操说明:运行后 CPU0 会被占满,其余核心空闲,内核会根据 cpu_util 的差值触发负载均衡。

4.6 Ftrace 跟踪 cpu_util 更新流程

使用 ftrace 跟踪util_avg更新、cpu_util读取的内核函数,直观观测数据流转,命令可直接复制:

# 1. 挂载debugfs(内核调试文件系统)
sudo mount -t debugfs none /sys/kernel/debug

# 2. 清空历史跟踪日志
sudo echo > /sys/kernel/debug/tracing/trace

# 3. 设置需要跟踪的核心函数
sudo echo ___update_load_sum >> /sys/kernel/debug/tracing/set_ftrace_filter
sudo echo ___update_load_avg >> /sys/kernel/debug/tracing/set_ftrace_filter
sudo echo cpu_util >> /sys/kernel/debug/tracing/set_ftrace_filter

# 4. 开启函数跟踪模式
sudo echo function > /sys/kernel/debug/tracing/current_tracer

# 5. 启动跟踪
sudo echo 1 > /sys/kernel/debug/tracing/tracing_on

# 等待10秒,让数据持续采集
sleep 10

# 6. 停止跟踪
sudo echo 0 > /sys/kernel/debug/tracing/tracing_on

# 7. 查看完整跟踪日志
sudo cat /sys/kernel/debug/tracing/trace

日志解读:日志中可以看到___update_load_sum周期性被调用(时钟 Tick 触发),CPU0 对应的函数调用频率远高于其他核心,对应其高利用率;同时cpu_util会被负载均衡、调频模块频繁调用读取数值。

4.7 传统工具对比:cpu_util 与 /proc/stat 使用率

执行以下命令查看传统 CPU 使用率,对比内核 cpu_util:

# 实时查看CPU整体使用率(1秒刷新一次)
mpstat -P ALL 1

# 查看/proc/stat原始时间片数据
cat /proc/stat

差异总结mpstat基于历史时间片计算,存在数百毫秒延迟;而 cpu_util 由 PELT 实时更新,延迟在微秒级,更适合内核实时决策。

五、常见问题与解答

Q1:cpu_util 取值范围是 0~1024,为什么不是百分比 0~100?

解答:内核使用SCHED_CAPACITY_SCALE=1024作为基准值,1024 是 2 的整数次幂,移位运算替代浮点运算,大幅降低内核计算开销。换算百分比公式:(cpu_util * 100) / 1024。该设计是内核为兼顾性能与精度的经典取舍。

Q2:任务处于就绪态、阻塞态时,util_avg 会更新吗?

解答:不会。util_sum仅在任务实际运行在 CPU 上时才累加时间。就绪态(等待 CPU)、阻塞态(等待 IO / 信号)不会计入 util_avg,这也是 cpu_util 只表征 “真实 CPU 占用” 的核心特性,和统计可运行时间的load_avg形成区分。

Q3:多核场景下,绑定 CPU 亲和性后,cpu_util 不再均衡,正常吗?

解答:属于正常现象。sched_setaffinity手动绑定线程 / 进程到指定核心后,内核负载均衡会跳过该任务,对应 CPU 的 cpu_util 会持续偏高。若需要自动均衡,解除 CPU 亲和性即可。

Q4:NO_HZ 无时钟模式下,cpu_util 还能正常更新吗?

解答:可以。PELT 算法做了专门适配,无时钟模式下依靠任务状态切换(唤醒、阻塞、切换) 触发 util 数据更新,不会因为关闭周期性时钟导致指标失效,适配嵌入式低功耗场景。

Q5:ftrace 跟踪不到 cpu_util 函数调用,是什么原因?

解答:优先排查三点:1. 未开启CONFIG_FTRACECONFIG_SCHED_DEBUG内核配置;2. 没有挂载debugfs文件系统;3. 系统使用了其他调频策略(如 performance),未启用schedutil,导致cpu_util接口极少被调用。

Q6:为什么两个 CPU 核心负载(cpu_util)差距很大,内核却不迁移任务?

解答:负载均衡存在阈值保护,避免频繁任务迁移造成开销。只有当核心间 cpu_util 差值超过内核预设阈值,且满足调度域规则、任务迁移代价评估后,才会触发迁移。另外,实时任务、绑定亲和性的任务不会被迁移。

六、实践建议与最佳实践

6.1 调试与观测技巧

  1. 分层排查原则:排查负载不均问题时,先通过mpstat做宏观判断,再用ftrace跟踪cpu_util更新函数,最后读取cfs_rq->avg.util_avg内核原始值,由外到内定位问题。
  2. perf 采样分析:使用perf record -g sleep 10采样 CPU 热点,结合 cpu_util 数据,快速定位 CPU 密集型线程。
  3. 区分指标使用场景:排查调度公平性看load_avg,排查 CPU 资源占用、功耗、调频问题,优先使用cpu_util

6.2 应用层开发最佳实践

  1. 谨慎使用 CPU 亲和性:业务程序非必要不要绑定 CPU 核心,会破坏内核基于 cpu_util 的负载均衡策略,造成局部核心过载。若必须绑定,建议分散任务到多个核心。
  2. 控制 CPU 密集型任务数量:单核心 cpu_util 达到 1024(满载)后,新增 CPU 密集任务会加剧竞争,调度延迟上升,业务侧需要做任务限流。
  3. 异构多核平台适配:在 Big.LITTLE 架构下,不要强行将高负载任务调度至小核,内核依靠 cpu_util 做任务择优放置,人为干预会导致性能下降。

6.3 内核调优与性能优化

  1. 负载均衡阈值调优:高并发服务器场景,可适当调低负载均衡触发阈值,让 cpu_util 差值更小就触发任务迁移,提升整体均衡性;低延迟嵌入式场景,调高阈值,减少任务迁移带来的切换开销。
  2. PELT 参数调优:对实时性要求极高的场景,可微调 PELT 衰减系数,缩短历史数据的权重周期,让 cpu_util 响应更快。
  3. 调频策略搭配:业务为 CPU 密集型时,推荐使用schedutil调频,依托 cpu_util 动态调速;静态高性能场景可使用performance策略,固定主频。

6.4 故障规避建议

  1. 禁止在内核模块中直接修改util_avg字段,会破坏 PELT 统计逻辑,导致 cpu_util 失真,引发负载均衡、调频逻辑异常。
  2. 长时间运行 CPU 压测程序后,及时终止进程,避免单核长期满载导致 cpu_util 居高不下,系统调度延迟累积。
  3. 内核升级时,重点核对pelt.cfair.c的改动,新版本若修改 PELT 算法,cpu_util 的计算逻辑会同步变化,原有调优规则需要重新适配。

七、总结与应用延伸

本文完整拆解了 Linux 负载均衡体系中cpu_util的底层原理、PELT 计算算法、数据聚合流程、内核接口、实操验证与工程调优。核心要点回顾:cpu_util 由调度实体的util_avg逐级聚合而来,依托指数加权滑动平均的 PELT 算法实现微秒级实时统计,仅统计任务真实 CPU 运行时间,取值范围 0~1024;它是 Linux 多核负载均衡、schedutil 动态调频两大核心模块的统一数据来源,也是区分于传统/proc/stat使用率的内核原生精细化指标。

从技术价值来看,理解 cpu_util 是打通 “调度算法 - 负载均衡 - 功耗管理” 三大模块的关键节点。在工程落地层面,云服务器、容器集群依靠它实现算力均衡;嵌入式工控、车载、移动设备依靠它实现性能与功耗的平衡;内核性能调优、故障排查工作中,cpu_util 是定位负载倾斜、调度延迟、调频异常的核心依据。

建议读者基于本文提供的内核源码、CPU 压测程序、ftrace 跟踪命令,在测试环境中复现实验:手动制造单核高负载,观察 cpu_util 数值变化、负载均衡触发时机、CPU 主频调整行为。也可以尝试修改内核 PELT 衰减系数,观测指标实时性的变化,加深对算法的理解。

在实际项目中,无论是做 Linux 内核二次开发、嵌入式系统裁剪、服务器性能调优,还是撰写相关技术论文、报告,cpu_util 相关的知识都具备极高的实用价值。吃透这套机制,能帮助开发者从底层理解 Linux 多核调度的运行逻辑,解决一线工作中各类 CPU 负载与性能问题。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐