util
的基本定义在 Linux 调度器中,util
通常指的是 调度实体的"运行需求"或"计算需求",它不是简单的 CPU 使用率百分比,而是一个经过加权和归一化的值,反映任务对 CPU 的"压力"。
util
类型:util_avg
: util_avg = 1024
表示一个任务持续占用一个 CPU 核心。util_est
: cpu_util
: util_avg
总和。util
的用途util
来估算任务对 CPU 的需求,并结合 CPU 的性能能力(capacity),选择既能满足性能需求又能节省能耗的 CPU。util
任务放到高性能大核,低 util
任务放到节能小核。util_avg
被用作 schedutil
频率调节器的输入,动态调整 CPU 频率。util_avg
高,就提升频率;如果低,就降低频率以省电。cpu_util
,决定是否迁移任务以平衡负载。SCHED_IDLE
或 SCHED_DEADLINE
中,util
可用于判断系统是否还有足够容量运行新任务。util
的计算方式(简化)Linux 使用 per-entity load tracking (PELT) 机制来计算 util_avg
。
se.avg.util_avg
。公式大致如下(概念性):
util_avg = util_avg * decay_factor + (running_time * 1024 / sampling_period)
假设一个任务在一个 CPU 上持续运行,其 util_avg
会趋近于 1024(表示占满一个 CPU)。
如果一个 CPU 上有两个任务,每个 util_avg = 600
,那么该 CPU 的总 cpu_util = 1200
。如果该 CPU 的 capacity 是 1024,则表示 过载(over-utilized),可能需要迁移到其他 CPU。
在内核代码中,你可以在以下位置找到 util
相关实现:
kernel/sched/pelt.c
:PELT 核心逻辑kernel/sched/fair.c
:CFS 调度器使用 util_avg
/proc/sched_debug
:可查看每个任务和 CPU 的 util_avg
例如:
cat /proc/sched_debug | grep util
输出可能包含:
cpu: 0, load: 512, util: 720, capacity: 1024
表示 CPU 0 的利用率是 720/1024 ≈ 70%。
概念 | 含义 |
---|---|
util_avg | 任务或 CPU 的加权平均利用率,用于调度决策 |
util_est | 任务的利用率预测值,用于唤醒优化 |
cpu_util | CPU 上所有任务的 util_avg 之和 |
单位 | 通常以 1024 为满量程(一个 CPU 核心) |
用途 | EAS、CPUFreq、负载均衡、调度决策 |
Documentation/scheduler/sched-energy.txt
kernel/sched/
+------------------+
| Task Execution |
+--------+---------+
|
v
+--------+---------+
| CFS + PELT Engine| ← 生产者
| Produces util_avg |
+--------+---------+
|
v
+------------+------------+
| util_avg signal |
+------------+------------+
|
+-----------------+------------------+
| | |
v v v
+--------+--------+ +-----+------+ +----------+----------+
| schedutil | | Load Balance| | EAS / CPU Selection |
| (CPU Frequency) | | (Migration) | | (Big.LITTLE Scheduling) |
+-----------------+ +-------------+ +-----------------------+
| | |
v v v
Adjust Frequency Balance Load Save Energy & Boost Perf
非常好的问题!你问到了 Linux 调度器中 capacity
(调度容量) 的根本来源:它是怎么定出来的?是不是以最强的 CPU 核定为 1024?每个系统 + 硬件平台是怎么确定各个 CPU 的 capacity
值的?
我们来一步步深入解析。
是的,Linux 调度器通常将系统中最强 CPU 的最大性能定为
1024
,其他 CPU 的capacity
按相对性能比例缩放。
这个值不是随意定的,而是通过 硬件性能基准 + 设备树(Device Tree)或 ACPI + 内核启动时的 CPU 拓扑探测 来确定的。
capacity
?再回顾在 Linux 调度器(特别是 EAS 和负载均衡)中:
capacity
是一个 归一化的整数,表示某个 CPU 的相对计算能力。capacity = 1024
capacity = 896
capacity = 512
📌 注意:这是"逻辑容量",不是物理频率或功耗。
capacity
是怎么确定的?流程图解[硬件设计]
↓
[芯片厂商提供性能数据] → Dhrystone/MIPS/SpecInt 等基准
↓
[设备树 (DT) 或 ACPI 表] → 描述 CPU 拓扑和性能等级
↓
[Linux 内核启动] → 解析 DT/ACPI,构建 CPU 性能域 (performance domains)
↓
[计算相对性能] → 找出最大 capacity,设为 1024
↓
[设置每个 CPU 的 capacity] → 按比例缩放
↓
[scheduler 使用] → 用于任务分配、EAS、负载均衡
capacity
的计算方法在 ARM64 平台上,常见如下设备树片段:
cpu0: cpu@0 {
device_type = "cpu";
compatible = "arm,cortex-a76";
capacity = <1024>;
cpu-idle-states = <&CPU_IDLE_STATES>;
};
cpu4: cpu@4 {
device_type = "cpu";
compatible = "arm,cortex-a55";
capacity = <512>;
};
👉 这是最直接的方式:厂商或 BSP 开发者手动设定。
如果设备树中没有 capacity
字段,内核会尝试自动计算。
📌 公式:
capacity = (max_freq_of_this_cpu / global_max_freq) * 1024
CPU 类型 | 最大频率 | 相对 capacity 计算 |
---|---|---|
Cortex-A78 | 3.0 GHz | (3.0 / 3.0) × 1024 = 1024 |
Cortex-A76 | 2.4 GHz | (2.4 / 3.0) × 1024 ≈ 819 |
Cortex-A55 | 1.8 GHz | (1.8 / 3.0) × 1024 ≈ 614 |
⚠️ 注意:这假设 性能与频率线性相关,实际中不完全准确(还受架构、IPC 影响),但 Linux 调度器采用此简化模型。
有些平台使用更精确的指标,比如:
然后:
capacity = (DMIPS_of_cpu / DMIPS_of_max_cpu) * 1024
例如:
CPU | DMIPS/MHz | 最大频率 | 总 DMIPS | capacity |
---|---|---|---|---|
A78 | 6.0 | 3.0 GHz | 18000 | 1024 |
A55 | 2.1 | 1.8 GHz | 3780 | (3780/18000)*1024 ≈ 215 |
这比纯频率更准确,但需要厂商提供数据。
capacity
?/proc/cpuinfo
(间接)cat /proc/cpuinfo | grep -E "processor|model name"
看 CPU 类型,结合知识推断。
cat /proc/sched_debug | grep "cpu_capacity"
输出示例:
cpu 0: capacity 1024
cpu 1: capacity 1024
cpu 2: capacity 512
cpu 3: capacity 512
dmesg | grep -i capacity
可能看到:
Detected capacity of cpu0: 1024
Scaling driver specifies max freq, using it for capacity
capacity
的存储位置(内核代码层面)在内核中,capacity
存储在:
struct sched_domain {
...
};
struct sched_group {
unsigned int cpu_capacity; // 每个 CPU 组的 capacity
};
// 获取某个 CPU 的 capacity
capacity_of(cpu) → cpu_rq(cpu)->cpu_capacity
初始化发生在 setup_cpu_capacity()
函数中,路径:
start_kernel()
→ smp_init()
→ cpu_up()
→ set_cpu_topology()
→ update_cpu_capacity() // 根据 DT 或频率表设置
问题 | 回答 |
---|---|
capacity 是怎么定的? | 由设备树、ACPI 或内核自动根据频率/性能推导。 |
是不是最强 CPU 定为 1024? | ✅ 是的,1024 是归一化最大值。 |
其他 CPU 怎么算? | 按相对性能(频率或 DMIPS)比例缩放。 |
频率变了 capacity 会变吗? | ❌ 不会,capacity 是静态的。 |
不同系统会不同吗? | ✅ 是的,每个 SoC 平台独立定义。 |
CPU 核心 | 类型 | 最大频率 | capacity(典型) |
---|---|---|---|
1x | Cortex-X1 | 2.84 GHz | 1024 |
3x | Cortex-A78 | 2.42 GHz | ~870 |
4x | Cortex-A55 | 1.80 GHz | ~512 |
这些值在设备树中定义,或由内核根据频率自动计算。
kernel/sched/topology.c
→ update_cpu_capacity()
drivers/base/arch_topology.c
→ arch_scale_cpu_capacity()
Documentation/admin-guide/cgroup-v2.rst
Documentation/scheduler/sched-capacity.txt
太好了!下面是一个 Python 模拟 PELT(Per-Entity Load Tracking)算法 的脚本,它演示了:
runtime
(运行时间)出发util_avg
(利用率),并与 capacity = 1024
对齐pelt_simulator.py
import matplotlib.pyplot as plt
# ========================
# 参数配置
# ========================
WINDOW = 32.0 # PELT 时间窗口(单位:ms)
TOTAL_CAPACITY = 1024 # CPU 最大容量(归一化基准)
DECAY_FACTOR = 0.5 # 每个窗口衰减 50%(简化模型,真实为连续衰减)
# 模拟数据:每个时间窗口内的任务运行时间(ms)
# 示例:任务在某些周期运行,某些周期睡眠
runtime_history = [
32, 32, 0, 0, # 满负载运行 2 个周期,然后空闲 2 个
16, 16, 16, # 50% 负载运行 3 个周期
32, 32, 32, 0 # 再次满负载后空闲
]
# 初始化状态
util_sum = 0.0 # 累积的利用率"和"(带衰减)
util_avg_list = [] # 记录每个周期后的 util_avg
time_steps = list(range(len(runtime_history)))
# ========================
# 模拟 PELT 更新过程
# ========================
for i, runtime in enumerate(runtime_history):
# 1. 归一化当前 runtime 为 [0, 1024] 范围内的贡献值
normalized_util = (runtime / WINDOW) * TOTAL_CAPACITY # 例如:16ms → 512
# 2. 应用指数衰减:新值 = 旧值 * y + 新贡献 * (1 - y)
util_sum = util_sum * DECAY_FACTOR + normalized_util * (1 - DECAY_FACTOR)
# 3. util_avg 可以近似为 util_sum 的"稳定值"
# (真实内核中还有更复杂的截断和整数处理)
util_avg = min(util_sum, TOTAL_CAPACITY) # 不超过 1024
util_avg_list.append(util_avg)
print(f"周期 {i+1:2d}: runtime={runtime:2d}ms → normalized={normalized_util:4.0f}, "
f"util_sum={util_sum:6.1f}, util_avg={util_avg:6.1f}")
# ========================
# 可视化结果
# ========================
plt.figure(figsize=(10, 6))
# 绘制 runtime 对应的"瞬时利用率"(未平滑)
instant_util = [(rt / WINDOW) * TOTAL_CAPACITY for rt in runtime_history]
plt.plot(time_steps, instant_util, label='瞬时利用率 (runtime 直接归一化)',
color='red', linestyle='--', alpha=0.7)
# 绘制 PELT 平滑后的 util_avg
plt.plot(time_steps, util_avg_list, label='PELT 平滑 util_avg',
color='blue', linewidth=2)
# 绘制 capacity 线(1024)
plt.axhline(y=TOTAL_CAPACITY, color='green', linestyle='-', label='CPU capacity (1024)', linewidth=2)
# 图表设置
plt.title('PELT 算法模拟:从 runtime 计算 util_avg\n(归一化到 1024 以匹配 capacity)')
plt.xlabel('时间周期(每周期 32ms)')
plt.ylabel('利用率 (0~1024)')
plt.xticks(time_steps)
plt.ylim(0, 1200)
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
# 显示图表
plt.show()
周期 1: runtime=32ms → normalized=1024, util_sum= 512.0, util_avg= 512.0
周期 2: runtime=32ms → normalized=1024, util_sum= 768.0, util_avg= 768.0
周期 3: runtime= 0ms → normalized= 0, util_sum= 384.0, util_avg= 384.0
周期 4: runtime= 0ms → normalized= 0, util_sum= 192.0, util_avg= 192.0
周期 5: runtime=16ms → normalized= 512, util_sum= 288.0, util_avg= 288.0
...
周期 10: runtime=32ms → normalized=1024, util_sum= 896.0, util_avg= 896.0
👉 可以看到:
util_avg
不会立刻归零(记忆效应)util_avg
趋近于实际负载水平[0, 1024]
范围内,便于与 capacity
比较你会看到一张图,包含三条线:
曲线 | 含义 |
---|---|
红色虚线 | 直接用 runtime / window 计算的瞬时利用率(毛刺大) |
蓝色实线 | PELT 平滑后的 util_avg (稳定、适合调度决策) |
绿色横线 | CPU 的 capacity = 1024 ,作为比较基准 |
✅ 当蓝色线低于绿色线时,表示 CPU "能容纳" 该任务。
你可以修改 runtime_history
来测试不同场景:
# 高负载
runtime_history = [32] * 10
# 间歇性爆发
runtime_history = [32, 0, 32, 0, 16, 16]
# 低负载
runtime_history = [8] * 10
观察 util_avg
如何响应。
runtime
→ util_avg
的转化过程capacity
在同一尺度比较如果你希望,我还可以:
需要的话告诉我即可 😊
🎯 你现在对 util
和 capacity
的关系,是不是更直观了?
这是一个非常核心、也非常经典的问题!
load
和util
的区别是什么?
在 Linux 调度器(尤其是 CFS 和 PELT 系统)中,load
和 util
都是用来衡量任务"重量"或"压力"的指标,但它们的物理意义、计算方式和用途完全不同。
概念 | 含义 |
---|---|
util (Utilization) | 衡量任务 实际占用了多少 CPU 时间(CPU 时间资源的消耗) |
load (Load) | 衡量任务的 调度权重(weight),反映其 优先级 + 运行需求(调度重要性) |
👉 更直观地说:
util
→ "这个任务跑得多快?用了多少 CPU 时间?"load
→ "这个任务有多重要?调度器有多想让它运行?"维度 | util_avg (Utilization) | load_avg (Load) |
---|---|---|
本质 | CPU 时间使用率(0~1024) | 调度权重的加权平均 |
单位 | 归一化到 1024(一个 CPU 满载) | 无量纲,但可放大(如 nice 影响) |
是否受 nice 影响 | ❌ 不受影响 | ✅ 受影响(nice 越低,load 越高) |
用途 | EAS、CPUFreq、capacity 匹配 | 负载均衡、vruntime 计算、权重分配 |
最大值 | 最大 ≈ 1024(一个 CPU) | 可远大于 1024(高权重任务) |
归一化基准 | 与 capacity 对齐(1024 = 一个 CPU) | 与 WEIGHT_IDLE 等调度权重表对齐 |
假设有两个任务:
任务 | nice 值 | 实际运行时间 | util_avg | load_avg |
---|---|---|---|---|
A(普通任务) | 0 | 占用 50% CPU | 512 | 1024(基准权重) |
B(高优任务) | -10 | 占用 50% CPU | 512 | 3072(更高权重) |
👉 关键点:
util_avg
相同 → 消耗的 CPU 时间一样多load_avg
不同 → 任务 B 更"重要",调度器更倾向于让它运行util_avg
的计算(简化)基于 实际运行时间,归一化到 1024:
util_contrib = (delta_runtime / window) * 1024
然后通过 PELT 指数衰减平均得到 util_avg
。
📌 与
nice
无关,只关心"是否在跑"。
load_avg
的计算(简化)基于任务的 调度权重(weight),再乘以运行时间比例:
load_contrib = weight * (delta_runtime / window)
weight
来自 nice
值查表(sched_prio_to_weight[]
)nice=0
→ weight=1024
nice=-10
→ weight=3072
然后也通过 PELT 衰减平均得到 load_avg
。
📌 所以即使两个任务运行时间相同,
nice
不同 →load_avg
不同。
util_avg
的消费者:task_util(p) <= capacity_of(cpu)
?cpu_util
调整频率关注:CPU 时间资源是否够用
load_avg
的消费者:load_avg
,决定是否迁移vruntime += runtime / weight
关注:调度优先级和公平性
把 CPU 想象成一条高速公路:
概念 | 高速公路类比 |
---|---|
util_avg | 车辆实际占用道路的时间比例(比如 50% 时间有车在跑) |
load_avg | 车辆的"优先级权重"(比如 VIP 车道使用权重高) |
util_avg
相同(都占了 1 小时道路)load_avg
不同(VIP 车"调度优先级"更高)在 kernel/sched/pelt.c
中:
/*
* Update both load and utilization averages
*/
__update_load_avg_contrib(struct sched_entity *se)
{
se->avg.load_avg = ... // 基于 weight 和运行时间
se->avg.util_avg = ... // 基于运行时间,归一化到 1024
}
你可以通过 ftrace
查看:
trace-cmd record -e sched:sched_pelt_cfs
trace-cmd show | grep util_avg
trace-cmd show | grep load_avg
误区 | 正确认识 |
---|---|
load 就是 CPU 使用率? | ❌ 不是!load 是调度权重,util 才是使用率 |
util_avg 受 nice 影响? | ❌ 不影响,util 只看运行时间 |
load_avg 能用来调频? | ❌ 不适合,schedutil 用 util_avg |
load_avg 越高,CPU 越忙? | ⚠️ 不一定,可能是高权重但运行少 |
util
= "我用了多少 CPU 时间" → 资源消耗load
= "我有多重要" → 调度优先级
场景 | 用 util 还是 load ? |
---|---|
选 CPU(大核 or 小核)? | ✅ util vs capacity |
调整 CPU 频率? | ✅ util |
负载均衡(迁移任务)? | ✅ load (但也结合 util ) |
计算 vruntime? | ✅ load (权重) |
如果你还想看一个 Python 脚本同时模拟 util_avg
和 load_avg
的变化,我可以写一个对比演示。需要吗? 😊
这是一个非常好的问题,但答案是:
❌
load
并不直接用来决定时间片(time slice)大小。
在 Linux 的 CFS(Completely Fair Scheduler) 调度器中,没有固定的时间片概念,而是使用 vruntime
(虚拟运行时间) 来实现"公平调度"。
我们来一步步澄清这个常见的误解。
nice
值(即权重)👉 取而代之的是:最小 vruntime 优先调度。
load
的真正作用:影响 vruntime
的增长速度load
(更准确说是 调度权重 weight
)的作用是:控制
vruntime
的增长速度,从而间接影响任务能运行多久。
vruntime += delta_runtime * (NICE_0_LOAD / weight)
其中:
delta_runtime
:任务实际运行的时间weight
:由 nice
值决定的调度权重(load
的基础)NICE_0_LOAD = 1024
(基准权重)任务 | nice | weight | vruntime 增长速度 | 实际运行时间(效果上) |
---|---|---|---|---|
A(nice=0) | 0 | 1024 | 正常速度 | 中等时长 |
B(nice=-5) | -5 | 2048 | 更慢(增长慢) | 更长(更"优先") |
C(nice=5) | 5 | 512 | 更快(增长快) | 更短 |
👉 所以:
nice
低)→ weight
高 → vruntime
增长慢 → 更容易保持"最小 vruntime" → 被优先调度,看起来像"时间片更长"load
到底是什么?load
(如 load_avg
)是任务或调度实体的 加权负载平均值,它:
nice
决定)vruntime
但它 不直接决定"你这次能运行多久"。
在 CFS 中,任务的"运行时长"是动态的,近似公式为:
target_latency = sched_latency_ns // 默认 6ms
min_granularity = sched_min_granularity_ns // 默认 0.75ms
ideal_time_per_task = target_latency / nr_running
actual_slice = max(min_granularity, ideal_time_per_task)
nr_running
:当前就绪队列中可运行的任务数👉 所以:运行时长主要由系统负载(任务数)决定,而不是 load
load
的间接影响虽然 load
不直接决定时间片,但它有间接影响:
途径 | 说明 |
---|---|
1. 影响 vruntime | 高 weight (低 nice )→ vruntime 增长慢 → 更容易被调度 → 累计运行时间更长 |
2. 影响调度顺序 | vruntime 小的任务优先运行 → 高优先级任务"感觉上"有更大时间片 |
3. 在组调度中 | 高 load 的组会分到更多 CPU 时间 |
load
与"时间片"的关系问题 | 回答 |
---|---|
load 直接决定时间片大小吗? | ❌ 不是,CFS 没有固定时间片 |
load 影响任务能运行多久吗? | ✅ 间接影响,通过 vruntime 机制 |
什么决定任务一次运行多久? | ✅ nr_running 、target_latency 、min_granularity |
load 的主要用途是什么? | ✅ 计算 vruntime 、负载均衡、组调度权重分配 |
把 CFS 想象成一个"公平分蛋糕"的系统:
nr_running
)weight
,来自 nice
)👉 load
就是"优先级"的体现,但它不直接说"你这次吃几口"。
Documentation/scheduler/sched-design-CFS.rst
kernel/sched/fair.c
→ __update_vruntime()
, pick_next_entity()
kernel/sched/sched.h
→ NICE_0_LOAD
, weight
表如果你想看一个 Python 模拟 CFS 调度 + vruntime + load 权重影响 的脚本,我可以写一个帮助你直观理解。需要吗? 😊
代表任务过去运行了多少时间。过去运行的越少,越应该被调度。
这是一个非常精准、深入的问题!
vruntime
是"上次运行的时间"还是"下次要运行的时间"?
vruntime
既不是"上次运行时间",也不是"下次要运行时间",
它是 任务到目前为止累计消耗的"公平 CPU 时间" —— 可以理解为 "已经跑了多远的虚拟里程"。
想象多个任务在一条"公平赛道"上赛跑:
vruntime
vruntime
最小,说明他跑得最少 → 该他上了vruntime
就往前"跑一段"👉 所以:
vruntime
不是"上次运行了多久"(那是 delta_exec
)vruntime
的本质:累计值概念 | 说明 |
---|---|
vruntime | 累计虚拟运行时间,从任务创建开始不断累加 |
单位 | 纳秒(ns),但经过权重归一化 |
更新时机 | 每次任务被调度出 CPU 时更新 |
用途 | 用于比较哪个任务"最该被调度"(最小 vruntime 优先) |
[调度器决定下一个任务]
↓
遍历红黑树,找 vruntime 最小的任务
↓
选中任务 A:vruntime = 100,000,000 ns
↓
任务 A 开始运行(真实时间开始流逝)
↓
运行了 5ms(5,000,000 ns)
↓
任务被切换出去(时间片到 or 被抢占)
↓
更新 vruntime:
vruntime += 5ms * (1024 / weight)
→ 新 vruntime = 100,000,000 + 调整后的值
↓
放回红黑树,等待下次调度
👉 所以:
vruntime
是 历史累计值(已经用了多少)delta_exec
算)假设两个任务:
任务 | nice | weight | 当前 vruntime | 运行 10ms 后 vruntime |
---|---|---|---|---|
A(普通) | 0 | 1024 | 100,000,000 ns | 110,000,000 ns |
B(高优) | -5 | 2048 | 100,000,000 ns | 105,000,000 ns |
vruntime
增得慢(因为权重高)vruntime
> B,B 会优先被选中误解 | 正确认识 |
---|---|
vruntime 是上次运行时间? | ❌ 不是,delta_exec 才是 |
vruntime 是下次运行时间? | ❌ CFS 不预分配时间,由 vruntime 差决定 |
vruntime 小 = 该运行了? | ✅ 正确!最小 vruntime 优先调度 |
vruntime 大 = 运行得久? | ⚠️ 不一定,可能是低优先级或长时间未调度 |
vruntime
和调度决策的关系CFS 的核心逻辑:
task = pick_min_vruntime_task(rq->cfs.rb_root);
vruntime
最小,谁就最"饿"(最该被调度)vruntime
?使用 ftrace
:
trace-cmd record -e sched:sched_switch
# 运行一些程序
trace-cmd report
输出示例:
<...>-1234 [001] ...1 1234.567890: sched_switch: prev_comm=sh prev_pid=1234 prev_vruntime=123456789 ...
next_comm=myscript next_pid=5678 next_vruntime=98765432 ...
👉 这里的 prev_vruntime
和 next_vruntime
都是 累计值。
问题 | 回答 |
---|---|
vruntime 是上次运行时间? | ❌ 不是,那是 delta_exec |
vruntime 是下次要运行时间? | ❌ CFS 不预分配时间片 |
vruntime 到底是什么? | ✅ 累计的"公平 CPU 时间",用于调度决策 |
调度器怎么选任务? | ✅ 选 vruntime 最小的 |
🎯 记住一句话:
vruntime
就像一个"公平里程表"——谁跑得最少,谁就该上场。
如果你想看一个 Python 动画式模拟 CFS 调度 + vruntime 累计过程,我可以写一个脚本,展示多个任务的 vruntime
如何变化。需要吗? 😊