LINUX 性能指标工具 - PSI(Pressure Stall Information)原理分析-框架和关键源码分析
PSI 核心功能与数据结构概述 PSI(Pressure Stall Information)通过关键数据结构和调度钩子监控系统资源压力。主要包含: 资源状态统计: 定义6种压力状态(PSI_IO_SOME/FULL, PSI_MEM_SOME/FULL, PSI_CPU_SOME) 通过任务状态标志位(TSK_IOWAIT、TSK_MEMSTALL等)跟踪资源阻塞情况 核心数据结构: psi_g
文章目录
PSI 统计时间的关键结构体
支持的统计资源类型
/*
* Pressure states for each resource:
*
* SOME: Stalled tasks & working tasks
* FULL: Stalled tasks & no working tasks
*/
enum psi_states {
PSI_IO_SOME,
PSI_IO_FULL,
PSI_MEM_SOME,
PSI_MEM_FULL,
PSI_CPU_SOME,
/* Only per-CPU, to weigh the CPU in the global average: */
PSI_NONIDLE, /* 👉 不会输出到 /proc/pressure/ 下,只用于计算全局平均值 */
NR_PSI_STATES = 6,
};
task 状态标志位
/* Task state bitmasks */
#define TSK_IOWAIT (1 << NR_IOWAIT)
#define TSK_MEMSTALL (1 << NR_MEMSTALL)
#define TSK_RUNNING (1 << NR_RUNNING)
#define TSK_ONCPU (1 << NR_ONCPU)
数据结构
每个 CPU 有一个任务状态和时间跟踪数据 struct psi_group_cpu
:
struct psi_group_cpu {
/* 防止多任务 同时更新,并做了cache 优化 */
seqcount_t seq ____cacheline_aligned_in_smp;
/* States of the tasks belonging to this group */
unsigned int tasks[NR_PSI_TASK_COUNTS];
/* 根据各种任务的状态的task计数,用test_state()函数来判断当前PSI的状态 */
u32 state_mask;
/* 各种PSI状态持续的累计时间 ns */
u32 times[NR_PSI_STATES];
/* 上次task状态变化的时间(从rq_clock获取时间) */
u64 state_start;
/* 2nd cacheline updated by the aggregator */
/* 上次PSI状态变化的时间 ns */
u32 times_prev[NR_PSI_AGGREGATORS][NR_PSI_STATES]
____cacheline_aligned_in_smp;
};
struct psi_group {
/* Protects data used by the aggregator */
struct mutex avgs_lock; // 保护聚合器使用的数据的互斥锁
/* Per-cpu task state & time tracking */
struct psi_group_cpu __percpu *pcpu; // 每个CPU的任务状态和时间跟踪数据
/* Running pressure averages */
u64 avg_total[NR_PSI_STATES - 1]; // 运行压力平均值的累计总和
u64 avg_last_update; // 上次更新平均值的时间
u64 avg_next_update; // 下次更新平均值的时间
/* Aggregator work control */
struct delayed_work avgs_work; // 用于计算平均值的延迟工作队列
/* Total stall times and sampled pressure averages */
u64 total[NR_PSI_AGGREGATORS][NR_PSI_STATES - 1]; // 各种聚合器的总停滞时间
unsigned long avg[NR_PSI_STATES - 1][3]; // 三个时间窗口(10s, 60s, 300s)的采样压力平均值
/* Monitor work control */
struct task_struct __rcu *poll_task; // RCU保护的轮询监控任务
struct timer_list poll_timer; // 轮询定时器
wait_queue_head_t poll_wait; // 轮询等待队列
atomic_t poll_wakeup; // 轮询唤醒原子变量
/* Protects data used by the monitor */
struct mutex trigger_lock; // 保护监控器使用的数据的互斥锁
/* Configured polling triggers */
struct list_head triggers; // 配置的轮询触发器链表
u32 nr_triggers[NR_PSI_STATES - 1]; // 每种状态的触发器数量
u32 poll_states; // 需要轮询监控的状态位图
u64 poll_min_period; // 最小轮询周期
/* Total stall times at the start of monitor activation */
u64 polling_total[NR_PSI_STATES - 1]; // 监控激活时的总停滞时间(用于比较)
u64 polling_next_update; // 下次轮询更新时间
u64 polling_until; // 轮询监控持续到的时间点
};
struct psi_trigger {
/* PSI state being monitored by the trigger */
enum psi_states state; // 触发器监控的PSI状态(如PSI_IO_SOME, PSI_MEM_FULL等)
/* User-spacified threshold in ns */
u64 threshold; // 用户指定的阈值(纳秒),超过该值将触发事件
/* List node inside triggers list */
struct list_head node; // 链表节点,用于将触发器链接到psi_group的触发器列表中
/* Backpointer needed during trigger destruction */
struct psi_group *group; // 反向指针,指向所属的psi_group,在销毁触发器时使用
/* Wait queue for polling */
wait_queue_head_t event_wait; // 等待队列头,用于poll/select系统调用等待事件
/* Pending event flag */
int event; // 待处理事件标志,1表示有事件待处理,0表示无事件
/* Tracking window */
struct psi_window win; // 跟踪窗口,用于计算在指定时间窗口内的压力增长
/*
* Time last event was generated. Used for rate-limiting
* events to one per window
*/
u64 last_event_time; // 上次生成事件的时间,用于限制每个窗口只生成一个事件
};
PSI 的四个核心功能
一. 统计各种 stall 的总耗时
核心思路
PSI 需要准确知道哪些任务在等待、哪些任务在运行。
因此在 调度点 (schedule) 和阻塞点 (block) 上添加钩子,记录任务状态并更新持续时间。
PSI 对sheduler和memory组系统提供的统计接口
void psi_task_change(struct task_struct *task, int clear, int set);
void psi_task_switch(struct task_struct *prev, struct task_struct *next, bool sleep);
void psi_memstall_tick(struct task_struct *task, int cpu);
void psi_memstall_enter(unsigned long *flags);
void psi_memstall_leave(unsigned long *flags);
这些接口由 scheduler 和 memory 子系统 调用,最终会统计当前处于各种状态的 task 数量。
任务状态
- TSK_IOWAIT
- 任务因 IO 等待而挂起(一般是
D
状态,等待磁盘/网络响应)。
- 任务因 IO 等待而挂起(一般是
- TSK_MEMSTALL
- 内存压力导致的 stall,例如缺页异常、等待 direct reclaim、compaction。
- TSK_RUNNING
- 任务在运行队列里,可以随时调度。统计值
tasks[NR_RUNNING]
一般 ≥tasks[NR_ONCPU]
。
- 任务在运行队列里,可以随时调度。统计值
- TSK_ONCPU
- 任务已被调度器选中,正在某个 CPU 上运行。
注意:这是 per-CPU 计数,因此tasks[NR_ONCPU]
在单 CPU 上只能是 0 或 1
- 任务已被调度器选中,正在某个 CPU 上运行。
tasks的计数方式举例
- 内存回收开始
- 调用
psi_memstall_enter
tasks[NR_MEMSTALL]
计数 +1
- 调用
- 内存回收结束
- 调用
psi_memstall_leave
tasks[NR_MEMSTALL]
计数 -1
- 调用
- 任务等待 IO
io_schedule_prepare()
→__schedule()
→psi_task_change()
tasks[NR_IOWAIT]
计数 +1
- IO 等待结束
io_schedule_finish()
→__schedule()
→psi_task_change()
tasks[NR_IOWAIT]
计数 -1
- 新任务进入运行队列
- 调用
psi_task_change()
tasks[NR_RUNNING]
计数 +1
- 调用
- 任务切换 (schedule)
- 调用
psi_task_switch()
- 上一个任务:
tasks[NR_ONCPU]
-1 - 新的任务:
tasks[NR_ONCPU]
+1
- 调用
根据各种状态的tasks的计数判定 SOME 与 FULL
static bool test_state(unsigned int *tasks, enum psi_states state)
{
switch (state) {
case PSI_IO_SOME:
return tasks[NR_IOWAIT];
/* 有 IO_WAIT 任务时,统计 IO_SOME */
case PSI_IO_FULL:
return tasks[NR_IOWAIT] && !tasks[NR_RUNNING];
/* 有 IO_WAIT 且无 RUNNING → 所有任务都因 IO 阻塞,FULL */
case PSI_MEM_SOME:
return tasks[NR_MEMSTALL];
case PSI_MEM_FULL:
return tasks[NR_MEMSTALL] && !tasks[NR_RUNNING];
/* 和 IO_FULL 类似。但内存 FULL 还有额外机制:如果有正在运行的任务,且当前的task的flag标志包含in_memstall,
则说明CPU正在做内存规整或者回收动作,正在消耗此cpu,会能统计为 FULL */
case PSI_CPU_SOME:
return tasks[NR_RUNNING] > tasks[NR_ONCPU];
/* RUNNING > ONCPU → 就绪队列有人排队,CPU 繁忙 */
/* 注意:如果系统只有一个任务,并且它独占 CPU,
即使 CPU 利用率 100%,也不会算作 CPU stall */
case PSI_NONIDLE:
return tasks[NR_IOWAIT] || tasks[NR_MEMSTALL] || tasks[NR_RUNNING];
/* 任意任务非空闲时,记为 NONIDLE,用于加权计算全局平均 */
default:
return false;
}
}
统计单个CPU的stall耗时
static void record_times(struct psi_group_cpu *groupc, int cpu,
bool memstall_tick)
{
u32 delta;
u64 now;
now = cpu_clock(cpu); /* 获取当前绝对时间 */
delta = now - groupc->state_start; /* 和上次进入这个函数的时间差 */
groupc->state_start = now;
/* 统计IO STALL的总时间 */
if (groupc->state_mask & (1 << PSI_IO_SOME)) {
groupc->times[PSI_IO_SOME] += delta;
if (groupc->state_mask & (1 << PSI_IO_FULL))
groupc->times[PSI_IO_FULL] += delta;
}
/* 统计MEMORY STALL的总时间 */
if (groupc->state_mask & (1 << PSI_MEM_SOME)) {
groupc->times[PSI_MEM_SOME] += delta;
if (groupc->state_mask & (1 << PSI_MEM_FULL))
groupc->times[PSI_MEM_FULL] += delta;
else if (memstall_tick) {
u32 sample;
/* CPU 周期被回收占满,即使有可运行任务,想跑也跑不起来或跑不出有效进展(比如分配不到页、频繁卡在分配慢路径)
从 “lost potential” 角度看:这些 CPU 周期本来可以用于“有产出”的任务执行,但被迫用来回收内存,因此视为 FULL*/
sample = min(delta, (u32)jiffies_to_nsecs(1));
groupc->times[PSI_MEM_FULL] += sample;
}
}
/* 统计 PSI_CPU_SOME */
if (groupc->state_mask & (1 << PSI_CPU_SOME))
groupc->times[PSI_CPU_SOME] += delta;
/* 统计 PSI_NONIDLE, 只要有任务在运行或者有任务发生stall,就统计PSI_NONIDLE,用于计算stall的百分比 */
if (groupc->state_mask & (1 << PSI_NONIDLE))
groupc->times[PSI_NONIDLE] += delta;
}
stall 触发函数汇总
二、更新 PSI 的平均值
时间与状态的结合
PSI 并不只关心 任务数量,而是关心 时间占比。
- 每个 CPU 会通过 周期性 tick(基于 jiffies) 采样当前的 stall 状态,并计算持续时间。
- 内核通过
sched_clock()
(纳秒级时间戳)结合 per-CPU 状态 来累计 stall 时间。
时间窗口与加权平均
函数:psi_avgs_work
static void psi_avgs_work(struct work_struct *work)
{
struct delayed_work *dwork;
struct psi_group *group;
u32 changed_states;
bool nonidle;
u64 now;
dwork = to_delayed_work(work);
group = container_of(dwork, struct psi_group, avgs_work);
mutex_lock(&group->avgs_lock);
now = sched_clock();
/* 多 CPU 的加权合并公式:
tNONIDLE = sum(tNONIDLE[i])
tSOME = sum(tSOME[i] * tNONIDLE[i]) / tNONIDLE
tFULL = sum(tFULL[i] * tNONIDLE[i]) / tNONIDLE
*/
collect_percpu_times(group, PSI_AVGS, &changed_states);
nonidle = changed_states & (1 << PSI_NONIDLE);
/* 转换为占比:
%SOME = tSOME / period
%FULL = tFULL / period
*/
if (now >= group->avg_next_update)
group->avg_next_update = update_averages(group, now);
/* 系统非 idle 时才继续调度下一次计算 */
if (nonidle) {
schedule_delayed_work(dwork,
nsecs_to_jiffies(group->avg_next_update - now) + 1);
}
mutex_unlock(&group->avgs_lock);
}
多 CPU 下的加权平均
tNONIDLE = sum(tNONIDLE[i])
tSOME = sum(tSOME[i] * tNONIDLE[i]) / tNONIDLE
tFULL = sum(tFULL[i] * tNONIDLE[i]) / tNONIDLE
- 每个 CPU 按照它的 非空闲时间(tNONIDLE[i]) 占比来加权。
- 保证负载轻的 CPU 不会过度影响全局平均值。
函数:update_averages
- 计算样本值(本周期新增的 stall 时间):
sample = group->total[PSI_AVGS][s] - group->avg_total[s];
- 限制样本值(避免超过 100%):
if (sample > period) sample = period;
- 累计总量更新:
group->avg_total[s] += sample;
- 更新指数平滑平均值:
pct = (sample * 100) / period; avg[0] = calc_load(avg[0], EXP_10s, pct); avg[1] = calc_load(avg[1], EXP_60s, pct); avg[2] = calc_load(avg[2], EXP_300s, pct);
指数平滑公式
new_avg = old_avg * exp_coeff + pct * (1 - exp_coeff)
pct
=(sample * 100) / period
→ 本周期压力百分比exp_coeff
→ 预计算的衰减系数(固定点表示)
预计算的衰减系数
#define EXP_10s 1677 /* 10秒窗口的近似系数 */
#define EXP_60s 1981 /* 60秒窗口 */
#define EXP_300s 2034 /* 300秒窗口 */
- 大致对应:
- 10s → ~0.8187
- 60s → ~0.9672
- 300s → ~0.9934
指数平滑的特点
- 近期数据权重大 → 对变化敏感
- 历史数据逐渐衰减 → 避免过时数据影响
- 多窗口并行计算:
- 10秒平均值:快速反应
- 60秒平均值:平衡稳定性
- 300秒平均值:长期趋势
示例
采样周期 = 2 秒,stall 时间 = 0.5 秒:
pct = (0.5 / 2) * 100 = 25%
10 秒窗口(exp_coeff ≈ 0.8187):
new_avg = old_avg * 0.8187 + 25 * (1 - 0.8187)
= old_avg * 0.8187 + 25 * 0.1813
= old_avg * 0.8187 + 4.53
这意味着新平均值由 81.87% 的旧值和 18.13% 的当前值组成。
三、 stall的观察接口
没啥东西,挺简单的,输出psi_avgs_work的计算结果avg到用户态,下面展示基本的调用关系
四、PSI TRIGGER
关键术语定义
基于代码分析,以下是对这些变量的详细解释:
PSI 触发器相关变量详解
-
polling_until
group->polling_until = now + group->poll_min_period * UPDATES_PER_WINDOW;
意义:表示 PSI 轮询监控需要持续到的时间点
作用:
- 当检测到受监控的资源状态发生变化时,系统会启动轮询监控
polling_until
确定了这次监控会持续多长时间- 保证监控至少持续一个最小窗口周期的时间(
min_window_size
) - 在此期间,即使没有活动也会继续轮询,确保不遗漏事件
计算方式:
- 持续时间 =
poll_min_period
×UPDATES_PER_WINDOW
- 其中
UPDATES_PER_WINDOW = 10
(固定常量) poll_min_period
= 最小窗口大小 / 10
-
polling_next_update
group->polling_next_update = now + group->poll_min_period;
意义:下一次检查触发器条件的时间点
作用:
- 控制触发器检查的频率
- 确保在监控期间定期调用
update_triggers
函数 - 更新值为当前时间加上
poll_min_period
工作机制:
- 在
psi_poll_work
中检查是否到达更新时间 - 如果到达,则调用
update_triggers
检查所有触发器条件 - 每次更新后重新设置下一个更新时间点
-
last_event_time
t->last_event_time = now; // 在触发事件时更新
意义:记录触发器上一次触发事件的时间
作用:
- 防止在同一个窗口期内重复触发同一事件
- 实现事件的去重机制
- 确保每个窗口周期内每个触发器最多只触发一次事件
防重机制:
if (now < t->last_event_time + t->win.size) continue; // 如果还在上次事件的窗口期内,则跳过
-
win.size
t->win.size = window_us * NSEC_PER_USEC;
意义:触发器的时间窗口大小(以纳秒为单位)
作用:
- 定义了计算资源压力增长量的时间范围
- 用户通过接口设置的窗口大小参数
- 决定事件触发的评估周期
约束条件:
- 最小值:
WINDOW_MIN_US
(500,000μs = 0.5秒) - 最大值:
WINDOW_MAX_US
(10,000,000μs = 10秒)
-
threshold
t->threshold = threshold_us * NSEC_PER_USEC;
意义:触发器的阈值(以纳秒为单位),threshold_us为用户态输入的阈值
作用:
- 当窗口期内的压力增长量超过此值时触发事件
- 是触发事件的判定条件
约束条件:
- 必须大于0
- 必须小于等于窗口大小(
win.size
)
-
growth
growth = window_update(&t->win, now, total[t->state]);
意义:在当前窗口期内资源压力状态的累计增长量,人话是:窗口期内stall了多长时间
作用:
- 超过threshold后,触发事件,用户态退出阻塞的poll
- 通过窗口插值算法计算得出
计算方式:
- 如果窗口期已过:
growth = current_value - window_start_value
- 如果窗口期未过:还会加上基于历史数据的插值估算
// 插值计算 remaining = win->size - elapsed; growth += div64_u64(win->prev_growth * remaining, win->size);
整体工作流程
- 初始化:当检测到资源状态变化时,设置
polling_until
和polling_next_update
- 周期性检查:每隔
poll_min_period
时间检查一次所有触发器 - 增长量计算:对每个触发器计算其窗口期内的
growth
值 - 事件触发:当
growth >= threshold
且不在防重期内时触发事件 - 持续监控:直到达到
polling_until
时间点才停止监控
这套机制确保了 PSI 能够准确、及时地检测资源压力事件,同时避免重复触发和资源浪费。
图例
PSI 事件触发机制示意
-
横坐标 表示时间单位,每个单位是一次
poll_min_period
(即 PSI 机制检测的最小周期)。 -
纵坐标 表示该周期内的 stall 时间。
-
假设监控窗口
win->size
恰好包含 10 个 poll_min_period。 -
在第 1 个 poll_min_period,stall 值超过了设定的 threshold,因此立即触发事件,退出用户态
poll
的阻塞。 -
尽管在 [2,10] 区间 内 stall 值始终高于 threshold,但由于仍处于同一个窗口期,没有新的事件产生。
-
到了第 11 个 poll_min_period,窗口期累积满了
win->size
,因此允许再次上报事件,从而退出用户态的poll
阻塞。 -
假设在第 18 个 poll_min_period,stall 值下降到了 0,那么在 10 个 poll_min_period 周期之后,
polling_until
会到期,从而暂停psi_poll_worker
。 -
在
psi_avgs_work
的检测周期中,如果发现 PSI 的stall值再次大于 0,则会重启并移动psi_poll_worker
,继续进行检测与事件上报
总结
PSI 的四个部分形成了一个闭环:
-
调度点采集任务状态(精准到 percpu)。
-
结合时间计算 stall 占比(通过公式与滑动窗口)。
-
支持阀值触发告警(用户可配置,epoll 高效通知)。
-
提供 proc/cgroup 接口(全局 & 容器可观测性)。
通过 PSI,Linux 内核第一次提供了一个 统一的资源压力量化框架,相比传统负载指标,它能:
-
更准确反映 性能退化点;
-
支持 前瞻性预警;
-
对 容器化和云场景 下的自动化调度尤为关键。
肝了一周的晚上,终于把这篇搞完了,这里感谢chatgpt哥,如果有错误和疑问,欢迎评论,如果觉得文章不错,帮忙
点个赞关注下
,您的鼓励是我的最大动力
PSI已完结
上一篇:LINUX 性能指标工具 - PSI(Pressure Stall Information)原理分析-初探
更多推荐
所有评论(0)