Linux 负载均衡的 cpu_util:CPU 利用率的精准评估
简介
在多核 Linux 系统中,CPU 负载均衡、动态调频调压(DVFS)是保障系统吞吐、响应延迟与功耗平衡的两大核心能力。传统top、mpstat等工具读取/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_avg、util_avg(cpu_util)与系统负载,这里做明确界定,也是理解后续内容的基础:
- load_avg:调度负载值,和任务优先级、权重强相关,统计任务处于可运行态的时间占比,主要用于 CFS 调度器的公平调度与传统负载均衡,偏向 “任务竞争激烈程度”。
- util_avg / cpu_util:CPU 利用率指标,与任务优先级无关,仅统计调度实体在 CPU 上实际运行的时间占比,纯粹表征硬件资源消耗,是本文研究核心。内核中对外暴露的
cpu_util本质就是运行队列聚合后的util_avg。 - 系统 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 源码路径定位
后续解析、调试均基于以下路径,提前熟记:
- PELT 算法、util_avg 计算核心:
kernel/sched/pelt.c - CFS 运行队列聚合逻辑:
kernel/sched/fair.c - 负载均衡决策逻辑:
kernel/sched/sched.h、kernel/sched/balancing.c - 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_sum与util_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;
}
代码说明:
delta:两次统计之间的时间间隔,单位纳秒,由内核时钟或任务状态切换触发计算;decay/scale:指数衰减系数,时间越久,历史利用率数据权重越低,保证指标实时性;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_FTRACE和CONFIG_SCHED_DEBUG内核配置;2. 没有挂载debugfs文件系统;3. 系统使用了其他调频策略(如 performance),未启用schedutil,导致cpu_util接口极少被调用。
Q6:为什么两个 CPU 核心负载(cpu_util)差距很大,内核却不迁移任务?
解答:负载均衡存在阈值保护,避免频繁任务迁移造成开销。只有当核心间 cpu_util 差值超过内核预设阈值,且满足调度域规则、任务迁移代价评估后,才会触发迁移。另外,实时任务、绑定亲和性的任务不会被迁移。
六、实践建议与最佳实践
6.1 调试与观测技巧
- 分层排查原则:排查负载不均问题时,先通过
mpstat做宏观判断,再用ftrace跟踪cpu_util更新函数,最后读取cfs_rq->avg.util_avg内核原始值,由外到内定位问题。 - perf 采样分析:使用
perf record -g sleep 10采样 CPU 热点,结合 cpu_util 数据,快速定位 CPU 密集型线程。 - 区分指标使用场景:排查调度公平性看
load_avg,排查 CPU 资源占用、功耗、调频问题,优先使用cpu_util。
6.2 应用层开发最佳实践
- 谨慎使用 CPU 亲和性:业务程序非必要不要绑定 CPU 核心,会破坏内核基于 cpu_util 的负载均衡策略,造成局部核心过载。若必须绑定,建议分散任务到多个核心。
- 控制 CPU 密集型任务数量:单核心 cpu_util 达到 1024(满载)后,新增 CPU 密集任务会加剧竞争,调度延迟上升,业务侧需要做任务限流。
- 异构多核平台适配:在 Big.LITTLE 架构下,不要强行将高负载任务调度至小核,内核依靠 cpu_util 做任务择优放置,人为干预会导致性能下降。
6.3 内核调优与性能优化
- 负载均衡阈值调优:高并发服务器场景,可适当调低负载均衡触发阈值,让 cpu_util 差值更小就触发任务迁移,提升整体均衡性;低延迟嵌入式场景,调高阈值,减少任务迁移带来的切换开销。
- PELT 参数调优:对实时性要求极高的场景,可微调 PELT 衰减系数,缩短历史数据的权重周期,让 cpu_util 响应更快。
- 调频策略搭配:业务为 CPU 密集型时,推荐使用
schedutil调频,依托 cpu_util 动态调速;静态高性能场景可使用performance策略,固定主频。
6.4 故障规避建议
- 禁止在内核模块中直接修改
util_avg字段,会破坏 PELT 统计逻辑,导致 cpu_util 失真,引发负载均衡、调频逻辑异常。 - 长时间运行 CPU 压测程序后,及时终止进程,避免单核长期满载导致 cpu_util 居高不下,系统调度延迟累积。
- 内核升级时,重点核对
pelt.c、fair.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 负载与性能问题。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)