Linux内存管理(十九):Linux PSI 详解
admin
2024-01-16 19:43:01

源码基于:Linux 5.4

0. 前言

之前在《Linux PSI 指标》一文中简单的描述了 PSI 指标的意义,以及PSI 出现的历史过程。

在PSI 之前,Linux 也有一些资源压力的评估方法,最具代表性的是 load average 和 vmpressure,而 load average 和vmpressure 都有各自的缺陷。详细可以查看《Linux PSI 指标》一文。

PSI,Pressure Stall Information 简称,是识别并量化 CPU、IO、memory 等资源紧张造成的中断,及其它对复杂工作负载甚至整个系统的时间影响。目录/proc/pressure/ 下面有三个资源指标:cpu、io、memory,可以通过cat /proc/pressure/* 方式查看压力统计信息。

  • cpu 只有some 行,但自 5.13 开始会报告出来,并为了往后兼容 full 值被设为0;
  • io 和memroy 有some 行和full 行,分别显示最近10s、60s和300s 的运行平均百分比。
  • total 是总累计时间,以 微秒 为单位;
  • some 这一行,代表至少有一些任务在某个资源上阻塞的时间占比;
  • full 这一行,代表所有的非 idle 任务同时被阻塞的时间占比,这期间 cpu 被完全浪费,在这种状态下花费较长时间的工作负载被认为是抖动。这会对性能产生严重影响;

更多信息可以查看《Linux PSI 指标》一文。

本篇重点围绕 PSI 原理和 PSI 在memory 上的监听和应用。

1. PSI 节点初始化

kernel/sched/psi.cstatic int __init psi_proc_init(void)
{proc_mkdir("pressure", NULL);proc_create("pressure/io", 0, NULL, &psi_io_fops);proc_create("pressure/memory", 0, NULL, &psi_memory_fops);proc_create("pressure/cpu", 0, NULL, &psi_cpu_fops);return 0;
}
module_init(psi_proc_init);

在module init 时调用 psi_proc_init() 进行 PSI 的初始化,创建了proc 虚拟文件目录 pressure,并在其下面创建了三个子文件:pressure/io、pressure/memory、pressure/cpu

三个文件节点的 file operations 结构体定义如下:

kernel/sched/psi.cstatic const struct file_operations psi_io_fops = {.open           = psi_io_open,.read           = seq_read,.llseek         = seq_lseek,.write          = psi_io_write,.poll           = psi_fop_poll,.release        = psi_fop_release,
};static const struct file_operations psi_memory_fops = {.open           = psi_memory_open,.read           = seq_read,.llseek         = seq_lseek,.write          = psi_memory_write,.poll           = psi_fop_poll,.release        = psi_fop_release,
};static const struct file_operations psi_cpu_fops = {.open           = psi_cpu_open,.read           = seq_read,.llseek         = seq_lseek,.write          = psi_cpu_write,.poll           = psi_fop_poll,.release        = psi_fop_release,
};

除了提供正常的open、read、write、llseek,还提供了poll 功能,这也是PSI 文件与用户通信的关键函数。本篇博文在剖析 PSI 原理的前提下重点分析memory 相关的代码和逻辑。

2. PSI 初始化

PSI 初始化的入口是 psi_init(),调用流程大致如下:

start_kernel()---->sched_init()---->psi_init()
kernel/sched/psi.cvoid __init psi_init(void)
{if (!psi_enable) {static_branch_enable(&psi_disabled);return;}psi_period = jiffies_to_nsecs(PSI_FREQ);group_init(&psi_system);
}

2.1 psi_enable

全局变量 psi_enable 受限于 CONFIG_PSI_DEFAULT_DISABLED,当没有设置此CONFIG,则psi 使能,psi_enable 默认值则为 true。

kernel/sched/psi.c#ifdef CONFIG_PSI_DEFAULT_DISABLED
static bool psi_enable;
#else
static bool psi_enable = true;
#endif

2.2 psi_period

获取PSI 时间间隔,此处psi_period 初始化为 2s:

#define PSI_FREQ	(2*HZ+1)	/* 2 sec intervals */

2.3 psi_system

static DEFINE_PER_CPU(struct psi_group_cpu, system_group_pcpu);
struct psi_group psi_system = {.pcpu = &system_group_pcpu,
};

psi_group 是PSI 管理的重要数据结构,PSI 系统有全局变量 psi_system 控制整个 PSI 各个资源的数据和状态。

2.4 group_init()

kernel/sched/psi.cstatic void group_init(struct psi_group *group)
{int cpu;for_each_possible_cpu(cpu)seqcount_init(&per_cpu_ptr(group->pcpu, cpu)->seq);group->avg_last_update = sched_clock();group->avg_next_update = group->avg_last_update + psi_period;INIT_DELAYED_WORK(&group->avgs_work, psi_avgs_work);mutex_init(&group->avgs_lock);/* Init trigger-related members */atomic_set(&group->poll_scheduled, 0);mutex_init(&group->trigger_lock);INIT_LIST_HEAD(&group->triggers);memset(group->nr_triggers, 0, sizeof(group->nr_triggers));group->poll_states = 0;group->poll_min_period = U32_MAX;memset(group->polling_total, 0, sizeof(group->polling_total));group->polling_next_update = ULLONG_MAX;group->polling_until = 0;rcu_assign_pointer(group->poll_kworker, NULL);
}

对 psi_system 变量进行初始化。

3. 变量 psi_memory_fops

3.1 psi_memory_open()

kernel/sched/psi.cstatic int psi_memory_open(struct inode *inode, struct file *file)
{return single_open(file, psi_memory_show, NULL);
}

single_open 会为文件节点创建一个 seq_operations,并通过 seq_open() 创建 seq_file,并设置 seq_operations 回调。single_open() 中指定了 seq_operations 的show 函数指针为 psi_memory_show()。

fs/seq_file.cint single_open(struct file *file, int (*show)(struct seq_file *, void *),void *data)
{struct seq_operations *op = kmalloc(sizeof(*op), GFP_KERNEL_ACCOUNT);int res = -ENOMEM;if (op) {op->start = single_start;op->next = single_next;op->stop = single_stop;op->show = show;res = seq_open(file, op);if (!res)((struct seq_file *)file->private_data)->private = data;elsekfree(op);}return res;
}
EXPORT_SYMBOL(single_open);

3.1.1 psi_memory_show()

kernel/sched/psi.cstatic int psi_memory_show(struct seq_file *m, void *v)
{return psi_show(m, &psi_system, PSI_MEM);
}

psi_show() 详细看下面第 6 节。

3.2 psi_memory_write()

kernel/sched/psi.cstatic ssize_t psi_memory_write(struct file *file, const char __user *user_buf,size_t nbytes, loff_t *ppos)
{return psi_write(file, user_buf, nbytes, PSI_MEM);
}

psi_write() 会对指定资源类型的文件节点写操作,通过 psi_trigger_create() 创建新的 psi_trigger,并将此新的 psi_trigger 添加到 psi_system 中的 triggers list 中。而且对于 psi_system 如果是第一次创建psi_trigger,会创建内核线程 psimon,详细看下面第 4 节。

3.3 psi_fop_poll()

kernel/sched/psi.c__poll_t psi_trigger_poll(void **trigger_ptr,struct file *file, poll_table *wait)
{__poll_t ret = DEFAULT_POLLMASK;struct psi_trigger *t;if (static_branch_likely(&psi_disabled))return DEFAULT_POLLMASK | EPOLLERR | EPOLLPRI;t = smp_load_acquire(trigger_ptr);if (!t)return DEFAULT_POLLMASK | EPOLLERR | EPOLLPRI;poll_wait(file, &t->event_wait, wait);if (cmpxchg(&t->event, 1, 0) == 1)ret |= EPOLLPRI;return ret;
}

PSI 文件节点的poll 机制发起者。

3.4 psi_fop_release()

kernel/sched/psi.cstatic int psi_fop_release(struct inode *inode, struct file *file)
{struct seq_file *seq = file->private_data;psi_trigger_destroy(seq->private);return single_release(inode, file);
}

当文件节点被close 之后,会触发file_operations 的release,会释放该资源中 psi_trigger,以及该资源整个过程中申请的内存空间。

4. psi_write()

kernel/sched/psi.cstatic ssize_t psi_write(struct file *file, const char __user *user_buf,size_t nbytes, enum psi_res res)
{char buf[32];size_t buf_size;struct seq_file *seq;struct psi_trigger *new;if (static_branch_likely(&psi_disabled)) //psi 没有enable,返回操作不支持错误return -EOPNOTSUPP;if (!nbytes)  //用户buf 为0,返回数据无效错误return -EINVAL;buf_size = min(nbytes, sizeof(buf)); //得出用户buf 大小if (copy_from_user(buf, user_buf, buf_size))  //copy 到本地bufreturn -EFAULT;buf[buf_size - 1] = '\0';seq = file->private_data; //在file_operations 中open 指定的函数指针中会创建seq_file,并赋值给file->private_data/* Take seq->lock to protect seq->private from concurrent writes */mutex_lock(&seq->lock);  //对seq_file 上锁,确认当前seq_file /* Allow only one trigger per file descriptor */if (seq->private) {mutex_unlock(&seq->lock);return -EBUSY;}//创建psi_trigger,并添加到psi_system的triggers列表中,第一次创建时会新建psimon 内核线程new = psi_trigger_create(&psi_system, buf, nbytes, res);if (IS_ERR(new)) {mutex_unlock(&seq->lock);return PTR_ERR(new);}smp_store_release(&seq->private, new);  //新建的psi_trigger 存入seq_file 的private 中mutex_unlock(&seq->lock);return nbytes;
}

psi_write() 函数主要工作:

  • 从用户层将数据存入本地;
  • 确认当前file 描述符是否已经新建过 psi_trigger,每个fp 只能创建一次psi_trigger;
  • 如果fp 是新open的还没有创建 psi_trigger,则调用 psi_trigger_create() 创建 psi_trigger,并将用户数据存入 psi_trigger中;
  • 将新建好的 psi_trigger 存入 seq_file->private 中,用以下次判断;

其中 psi_trigger_create() 对于 psi_system 是第一次创建 psi_trigger,则会同时创建一个内核线程 psimon,详细看下面一节。

5. psi_trigger_create()

kernel/sched/psi.cstruct psi_trigger *psi_trigger_create(struct psi_group *group,char *buf, size_t nbytes, enum psi_res res)
{struct psi_trigger *t;enum psi_states state;u32 threshold_us;u32 window_us;if (static_branch_likely(&psi_disabled))   //psi 是否enablereturn ERR_PTR(-EOPNOTSUPP);if (sscanf(buf, "some %u %u", &threshold_us, &window_us) == 2)  //解析是否为写 some 数据state = PSI_IO_SOME + res * 2;else if (sscanf(buf, "full %u %u", &threshold_us, &window_us) == 2) //解析是否为写 full 数据state = PSI_IO_FULL + res * 2;elsereturn ERR_PTR(-EINVAL);   //其他数据都认为无效if (state >= PSI_NONIDLE)   //pis_states 不能超过PSI_NONIDLEreturn ERR_PTR(-EINVAL);if (window_us < WINDOW_MIN_US ||  //窗口时长设置在500ms 至 10swindow_us > WINDOW_MAX_US)return ERR_PTR(-EINVAL);/* Check threshold */if (threshold_us == 0 || threshold_us > window_us)  //检测阈值不能为0,也不能大于窗口时长return ERR_PTR(-EINVAL);t = kmalloc(sizeof(*t), GFP_KERNEL);  //创建一个psi_triggerif (!t)return ERR_PTR(-ENOMEM);t->group = group;   //记录psi_trigger的 groupt->state = state;   //记录psi_trigger的 psi_statest->threshold = threshold_us * NSEC_PER_USEC;  //记录psi_trigger的检测时间,us 单位t->win.size = window_us * NSEC_PER_USEC;      //记录psi_trigger的窗口时长,us 单位t->event = 0;t->last_event_time = 0;init_waitqueue_head(&t->event_wait);mutex_lock(&group->trigger_lock);if (!rcu_access_pointer(group->poll_kworker)) { //psi 系统相当关键的地方,创建psimon 工作线程struct sched_param param = {.sched_priority = 1,};struct kthread_worker *kworker;kworker = kthread_create_worker(0, "psimon");if (IS_ERR(kworker)) {kfree(t);mutex_unlock(&group->trigger_lock);return ERR_CAST(kworker);}sched_setscheduler_nocheck(kworker->task, SCHED_FIFO, ¶m);kthread_init_delayed_work(&group->poll_work,psi_poll_work); //指定group work 的执行函数rcu_assign_pointer(group->poll_kworker, kworker);  //存放worker 到poll_kworkder}list_add(&t->node, &group->triggers);  //将新建的psi_trigger 存入group 中group->poll_min_period = min(group->poll_min_period,div_u64(t->win.size, UPDATES_PER_WINDOW)); //确认最新的poll 周期group->nr_triggers[t->state]++;    //计数当前psi 资源trigger数group->poll_states |= (1 << t->state);  //更新group 中资源状态mutex_unlock(&group->trigger_lock);return t;
}

5.1 创建 psi_trigger

psi_trigger_create() 函数最开始确认写入的数据是否正常,如果正常会创建一个 psi_trigger,并进行初始化;

该psi_trigger 记录了 threshold 和 window time,都是微妙为单位的数据,这两个数据都是针对某一个资源(io/memory/cpu)的 some 或 full 数据;

	t->group = group;t->state = state;t->threshold = threshold_us * NSEC_PER_USEC;t->win.size = window_us * NSEC_PER_USEC;window_reset(&t->win, 0, 0, 0);t->event = 0;t->last_event_time = 0;init_waitqueue_head(&t->event_wait);

5.2 kthread_create_worker()

新创建的 psi_trigger 会伴随着psi 系统的内核线程 psimon 创建,判断条件是 group->poll_kworker 是否已经赋值;

另外在线程创建成功后,会初始化 group->poll_work 并指定work 的执行函数为 psi_poll_work();

	if (!rcu_access_pointer(group->poll_kworker)) {struct sched_param param = {.sched_priority = 1,};struct kthread_worker *kworker;kworker = kthread_create_worker(0, "psimon");if (IS_ERR(kworker)) {kfree(t);mutex_unlock(&group->trigger_lock);return ERR_CAST(kworker);}sched_setscheduler_nocheck(kworker->task, SCHED_FIFO, ¶m);kthread_init_delayed_work(&group->poll_work,psi_poll_work);rcu_assign_pointer(group->poll_kworker, kworker);}

6. psi_show()

未完待续

上一篇:金元与粪肥

下一篇:发财

相关内容

热门资讯

Linux 7.0发布 Lin... 快科技2月25日消息,Linux内核7.0-rc1首个候选版本已正式发布,Linus Torvald...
人为何会暴饮暴食、发胖?这位教... 20年前,因为一个偶然的机会,上海交通大学特聘教授赵立平察觉到,肠道细菌与人体健康有关,从而开启了当...
福州“神兽们”报到,“超短学期... 2月25日福州的“神兽们”都背上书包重返校园报到啦!不少中小学生这几天有点焦虑——新学期马上开学了!...
美股开盘:美股三大指数集体高开 (来源:科创100ETF基金)  美股三大指数集体高开,标普500指数涨0.42%,道指涨0.35%...
美股异动 | 锂矿概念股走强 ... 周三,锂矿概念股走强,Sigma Lithium(SGML.US)大涨超32%,美国雅保(ALB.U...