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);

这些接口由 schedulermemory 子系统 调用,最终会统计当前处于各种状态的 task 数量。


任务状态

  • TSK_IOWAIT
    • 任务因 IO 等待而挂起(一般是 D 状态,等待磁盘/网络响应)。
  • 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

tasks的计数方式举例

  1. 内存回收开始
    • 调用 psi_memstall_enter
    • tasks[NR_MEMSTALL] 计数 +1
  2. 内存回收结束
    • 调用 psi_memstall_leave
    • tasks[NR_MEMSTALL] 计数 -1
  3. 任务等待 IO
    • io_schedule_prepare()__schedule()psi_task_change()
    • tasks[NR_IOWAIT] 计数 +1
  4. IO 等待结束
    • io_schedule_finish()__schedule()psi_task_change()
    • tasks[NR_IOWAIT] 计数 -1
  5. 新任务进入运行队列
    • 调用 psi_task_change()
    • tasks[NR_RUNNING] 计数 +1
  6. 任务切换 (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
  1. 计算样本值(本周期新增的 stall 时间):
    sample = group->total[PSI_AVGS][s] - group->avg_total[s];
    
  2. 限制样本值(避免超过 100%):
    if (sample > period)
        sample = period;
    
  3. 累计总量更新:
    group->avg_total[s] += sample;
    
  4. 更新指数平滑平均值:
    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 触发器相关变量详解
  1. 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
  2. polling_next_update

    group->polling_next_update = now + group->poll_min_period;
    

    意义:下一次检查触发器条件的时间点

    作用

    • 控制触发器检查的频率
    • 确保在监控期间定期调用 update_triggers 函数
    • 更新值为当前时间加上 poll_min_period

    工作机制

    • psi_poll_work 中检查是否到达更新时间
    • 如果到达,则调用 update_triggers 检查所有触发器条件
    • 每次更新后重新设置下一个更新时间点
  3. last_event_time

    t->last_event_time = now;  // 在触发事件时更新
    

    意义:记录触发器上一次触发事件的时间

    作用

    • 防止在同一个窗口期内重复触发同一事件
    • 实现事件的去重机制
    • 确保每个窗口周期内每个触发器最多只触发一次事件

    防重机制

    if (now < t->last_event_time + t->win.size)
        continue;  // 如果还在上次事件的窗口期内,则跳过
    
  4. 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秒)
  5. threshold

    t->threshold = threshold_us * NSEC_PER_USEC;
    

    意义:触发器的阈值(以纳秒为单位),threshold_us为用户态输入的阈值

    作用

    • 当窗口期内的压力增长量超过此值时触发事件
    • 是触发事件的判定条件

    约束条件

    • 必须大于0
    • 必须小于等于窗口大小(win.size
  6. 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);
      

整体工作流程

  1. 初始化:当检测到资源状态变化时,设置 polling_untilpolling_next_update
  2. 周期性检查:每隔 poll_min_period 时间检查一次所有触发器
  3. 增长量计算:对每个触发器计算其窗口期内的 growth
  4. 事件触发:当 growth >= threshold 且不在防重期内时触发事件
  5. 持续监控:直到达到 polling_until 时间点才停止监控

这套机制确保了 PSI 能够准确、及时地检测资源压力事件,同时避免重复触发和资源浪费。

图例

在这里插入图片描述
PSI 事件触发机制示意

  1. 横坐标 表示时间单位,每个单位是一次 poll_min_period(即 PSI 机制检测的最小周期)。

  2. 纵坐标 表示该周期内的 stall 时间

  3. 假设监控窗口 win->size 恰好包含 10 个 poll_min_period

  4. 在第 1 个 poll_min_period,stall 值超过了设定的 threshold,因此立即触发事件,退出用户态 poll 的阻塞。

  5. 尽管在 [2,10] 区间 内 stall 值始终高于 threshold,但由于仍处于同一个窗口期,没有新的事件产生。

  6. 到了第 11 个 poll_min_period,窗口期累积满了 win->size,因此允许再次上报事件,从而退出用户态的 poll 阻塞。

  7. 假设在第 18 个 poll_min_period,stall 值下降到了 0,那么在 10 个 poll_min_period 周期之后,polling_until 会到期,从而暂停 psi_poll_worker

  8. psi_avgs_work 的检测周期中,如果发现 PSI 的stall值再次大于 0,则会重启并移动 psi_poll_worker,继续进行检测与事件上报


总结

PSI 的四个部分形成了一个闭环:

  1. 调度点采集任务状态(精准到 percpu)。

  2. 结合时间计算 stall 占比(通过公式与滑动窗口)。

  3. 支持阀值触发告警(用户可配置,epoll 高效通知)。

  4. 提供 proc/cgroup 接口(全局 & 容器可观测性)。

通过 PSI,Linux 内核第一次提供了一个 统一的资源压力量化框架,相比传统负载指标,它能:

  • 更准确反映 性能退化点

  • 支持 前瞻性预警

  • 容器化和云场景 下的自动化调度尤为关键。

肝了一周的晚上,终于把这篇搞完了,这里感谢chatgpt哥,如果有错误和疑问,欢迎评论,如果觉得文章不错,帮忙点个赞关注下,您的鼓励是我的最大动力

PSI已完结
上一篇:LINUX 性能指标工具 - PSI(Pressure Stall Information)原理分析-初探

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐