Linux에서는 CPU, Memory, IO 등 자원을 사용하기 위한 경쟁 및 자원의 고갈로 인한 성능 영향도를 측정할 수 있는 인터페이스를 제공한다.
즉, 자원을 얻기위해 프로세스가 동작하지 못하고 얼마나 대기(stall) 하였는지 측정이 가능하다.
이 기능은 Pressure Stall Information(PSI) 이름으로 Kernel에 추가되었고, 아래에서는 그 의미와 사용 방법을 간단하게 작성하였다.
※ PSI 인터페이스 ※
아래처럼 커맨드를 입력하여 PSI 값을 읽을 수 있다.
$ cat /proc/pressure/cpu
some avg10=2.98 avg60=2.81 avg300=1.41 total=268109926
$ cat /proc/pressure/memory
some avg10=0.30 avg60=0.12 avg300=0.02 total=4170757
full avg10=0.12 avg60=0.05 avg300=0.01 total=1856503
$ cat /proc/pressure/io
some avg10=0.08 avg60=0.03 avg300=0.00 total=702350375
full avg10=0.00 avg60=0.00 avg300=0.00 total=539254260
의미하는 바는 아래와 같다.

full은 모든 프로세스가 더 이상 진행이 되지 않음을 의미하므로 시스템 성능에 매우 큰 영향을 미친다.
※ PSI 예시 ※
some / full 의미를 이해하기 위한 좋은 예시를 아래에 소개한다.

최근 60초 동안, IO를 위해서 TaskB가 30초 동안 대기(stall)하였다. 그렇다면, 어떤 임의의 process가 stall 상태이므로 some 구간이라고 하며, 그 수치는 "30/60 = 0.5 = 50%"로 계산된다.
다른 예시를 보면,

최근 60초 동안, IO를 위해서 TaskB가 stall 상태이다. 여기서 TaskA 또한 10초동안 stall 되었다.
10초 동안은 모든 프로세스가 IO를 하기 위해서 stall 되었으므로 10초는 full 구간이다.
그러므로 "full = 10/60 = 0.1666 = 16.66%"이다.
하나 더 예시를 들면,

같은 방법으로, 적어도 한 개의 프로세스가 stall 상태일 때 some 구간, 그리고 모든 프로세스가 해당 자원을 얻기 위하여 stall 되었으면 full 구간으로 계산한다.
※ PSI 동작 개념 ※

PSI 모듈은 User에게 두 가지 인터페이스를 제공한다.
전체적인 System의 pressure를 확인할 수 있는 인터페이스와, cgroup별로 pressure 정보를 확인 할 수 있는 인터페이스를 통해 user는 PSI 정보를 얻을 수 있다.
PSI는 2개의 커널 모듈(sched + mm)과 함께 동작하여 정보를 생성한다.
각 프로세스마다 memory, IO, CPU에 접근하면서 발생하는 signal과 task 상태변화를 모니터링하고 집계한다.
즉, task가 iowait 상태로 유지되는 시간을 측정하거나, 메모리 할당을 위해서 slow path로 소요되는 시간을 측정한다.
(일반적으로 PSI 값이 갑자기 변화하여 노이즈가 섞이는 걸 막기 위해 값을 이동 평균하여 사용한다.)
※ 초기화 과정 ※
static int __init psi_proc_init(void)
{
if (psi_enable) {
proc_mkdir("pressure", NULL);
proc_create("pressure/io", 0666, NULL, &psi_io_proc_ops);
proc_create("pressure/memory", 0666, NULL, &psi_memory_proc_ops);
proc_create("pressure/cpu", 0666, NULL, &psi_cpu_proc_ops);
}
return 0;
}
module_init(psi_proc_init);
proc/pressure 디렉터리를 만들고, 그 아래에 io, memory, cpu 파일을 만든다.
void __init psi_init(void)
{
// ...
psi_period = jiffies_to_nsecs(PSI_FREQ);
group_init(&psi_system);
}
각각의 cgroup에 대하여 PSI값을 계산하기 위하여 초기화를 진행한다.
만약 cgroup config가 disable 상태라면, 전체 시스템에 대한 PSI 계산(system_group_percpu)만을 지원한다.
/* System-level pressure and stall tracking */
static DEFINE_PER_CPU(struct psi_group_cpu, system_group_pcpu);
struct psi_group psi_system = {
.pcpu = &system_group_pcpu,
};
group 기능을 활용하기 위해서는 mount cgroup2 필요하다.)
/*
* 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,
PSI_CPU_FULL,
/* Only per-CPU, to weigh the CPU in the global average: */
PSI_NONIDLE,
NR_PSI_STATES = 7,
};
PSI 상태는 총 7개가 있다.
| PSI_IO_SOME | 해당 CPU에서 적어도 하나의 task가 iowait 상태 |
| PSI_IO_FULL | 해당 CPU에서 적어도 하나의 task가 iowait 상태이면서, 실행 가능한 task가 해당 CPU에는 없는 상태 |
| PSI_MEM_SOME | 해당 CPU에서 적어도 하나의 task가 memstall 상태 |
| PSI_MEM_FULL | 해당 CPU에서 적어도 하나의 task가 memstall 상태이면서, 실행 가능한 task가 해당 CPU에는 없는 상태 |
| PSI_CPU_SOME | 해당 CPU에서 runqueue에 적어도 하나의 task가 scheduling waiting 상태 |
| PSI_CPU_FULL | 해당 CPU에서 runqueue에 적어도 하나의 task가 scheduling waiting 상태이면서, 실행 가능한 task가 없는 상태 |
| PSI_NONIDLE | 해당 CPU가 동작중인 상태 |
여기서 CPU idle이라는 것은 아래의 3개 조건을 만족해야 한다.
#1. 해당 CPU에서 IO waiting 상태 task가 없음
#2. 해당 CPU에서 memstall 상태 task가 없음
#3. 해당 CPU의 runqueue에서 scheduling waiting 상태의 task가 없으며, 실행 중인 task도 없음
※ 동작 방식 ※
결국 PSI는 task의 상태변화를 모니터링하면서 동작한다.
각 task의 상태를 저장하기 위해서 task_struct의 멤버 변수와 flag 값을 활용한다.
struct task_struct {
// ...
#ifdef CONFIG_PSI
/* Pressure stall state */
unsigned int psi_flags;
#endif
// ...
};
/* 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)
#define TSK_MEMSTALL_RUNNING (1 << NR_MEMSTALL_RUNNING)
상태 변화를 기록하는 함수는 주로 psi_task_change() 활용하는데, schedule runqueue에 삽입되거나 제거될 때 해당 함수가 호출되게 된다.

(psi_memstall_tick()은 task 상태변화는 없으며, scheduling tick 발생했을 때 시간을 누적하기 위함.)
PSI는 주기적으로 데이터를 취합하고 계산하기 위해서 worker thread를 이용한다.
매 PSI_FREQ마다 psi_avg_work()를 호출하여 값을 계산한다.
static void psi_avgs_work(struct work_struct *work)
{
// ...
collect_percpu_times(group, PSI_AVGS, &changed_states);
// ...
if (now >= group->avg_next_update)
group->avg_next_update = update_averages(group, now);
}
collect_percpu_times() 함수는 CPU 별로 각 상태의 누적 시간을 갱신하며, update_averages() 함수에서는 10초, 60초, 300초에 대한 pressure 값을 계산한다.
즉, 서로 다른 상태에 있는 task의 개수는 상태변화가 일어날 때마다 집계되며, 그 시간의 경과는 매 PSI_FREQ 시간마다 누적되어 계산한다.
※ PSI 계산 방식 ※
task 개수와 상태, 그리고 상태가 지속된 시간을 모두 얻었으면 PSI를 계산한다.
만약 한 개의 CPU만 존재한다면, 간단하게 아래와 같이 계산하면 된다.
만약 한 개의 CPU만 존재한다면, 간단하게 아래와 같이 계산하면 된다.
%SOME = time(SOME) / period
%FULL = time(FULL) / period
하지만, CPU가 여러 개라면,
tSOME[cpu] = time(nr_delayed_tasks[cpu] != 0)
tFULL[cpu] = time(nr_delayed_tasks[cpu] && !nr_productive_tasks[cpu])
tNONIDLE[cpu] = time(nr_nonidle_tasks[cpu] != 0)
tNONIDLE = sum(tNONIDLE[i])
tSOME = sum(tSOME[i] * tNONIDLE[i]) / tNONIDLE
tFULL = sum(tFULL[i] * tNONIDLE[i]) / tNONIDLE
%SOME = tSOME / period
%FULL = tFULL / period
tNONIDLE[i], tSOME[cpu] 및 tFULL[cpu] 값은 주기적으로 per-CPU 통계 값을 확인하여 얻어진다.
이때, 주기 사이에(Default 2초) 평균값을 얻는 방법은 load average와 같은 exponential 계산 방법을 사용한다.
newload = load * exp + active * (FIXED_1 - exp)
여기서 active는 이전에 계산했던 load average 값을 의미한다.
exp 값은 아래와 같이 정의된다.
/* Running averages - we need to be higher-res than loadavg */
#define PSI_FREQ (2*HZ+1) /* 2 sec intervals */
#define EXP_10s 1677 /* 1/exp(2s/10s) as fixed-point */
#define EXP_60s 1981 /* 1/exp(2s/60s) */
#define EXP_300s 2034 /* 1/exp(2s/300s) */
※ 사용 예: User ※
아래부터는 사용 예시에 대해서 살펴보자.
만약 user level에서 PSI 값의 threshold를 정하고 이를 모니터링하고자 한다면,
아래의 예시 코드와 같이 poll() API를 사용한다.
(아래의 예시는 "1s 동안, memory 할당에서 some stall 150ms 발생"하는 경우를 모니터링하는 코드이다.)
// Documentation/accounting/psi.txt
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <poll.h>
#include <string.h>
#include <unistd.h>
/*
* Monitor memory partial stall with 1s tracking window size
* and 150ms threshold.
*/
int main() {
const char trig[] = "some 150000 1000000";
struct pollfd fds;
int n;
fds.fd = open("/proc/pressure/memory", O_RDWR | O_NONBLOCK);
if (fds.fd < 0) {
printf("/proc/pressure/memory open error: %s\n",
strerror(errno));
return 1;
}
fds.events = POLLPRI;
if (write(fds.fd, trig, strlen(trig) + 1) < 0) {
printf("/proc/pressure/memory write error: %s\n",
strerror(errno));
return 1;
}
printf("waiting for events...\n");
while (1) {
n = poll(&fds, 1, -1);
if (n < 0) {
printf("poll error: %s\n", strerror(errno));
return 1;
}
if (fds.revents & POLLERR) {
printf("got POLLERR, event source is gone\n");
return 0;
}
if (fds.revents & POLLPRI) {
printf("event triggered!\n");
} else {
printf("unknown event received: 0x%x\n", fds.revents);
return 1;
}
}
return 0;
}
※ 사용 예: Android ※
실제 Android Platform에서는 lmkd(Low Memory Killer Daemon)가 PSI 수치를 활용해서 메모리를 회수한다.
초기화 과정은 init_psi_monitor() 함수에서 이루어진다.
main() // system/core/lmkd/lmkd.c
init() // system/core/lmkd/lmkd.c
init_psi_monitors() // system/core/lmkd/lmkd.c
init_mp_psi() // system/core/lmkd/lmkd.c
init_psi_monitor() // system/core/lmkd/libpsi/psi.c
최종적으로는 3종류의 Threshold를 가지고 동작한다.
/* memory pressure levels */
enum vmpressure_level {
VMPRESS_LEVEL_LOW = 0,
VMPRESS_LEVEL_MEDIUM,
VMPRESS_LEVEL_CRITICAL,
VMPRESS_LEVEL_COUNT
};
static struct psi_threshold psi_thresholds[VMPRESS_LEVEL_COUNT] = {
{ PSI_SOME, 70 }, /* 70ms out of 1sec for partial stall */
{ PSI_SOME, 100 }, /* 100ms out of 1sec for partial stall */
{ PSI_FULL, 70 }, /* 70ms out of 1sec for complete stall */
};
간략하게만 설명하면,
PSI Threshold 조건의 만족 여부를 판단하여 kill 대상의 oom_adj_score를 조정하여 동작한다.
/* OOM score values used by both kernel and framework */
#define OOM_SCORE_ADJ_MIN (-1000)
#define OOM_SCORE_ADJ_MAX 1000
static bool update_props() {
// ...
/* By default disable low level vmpressure events */
level_oomadj[VMPRESS_LEVEL_LOW] =
GET_LMK_PROPERTY(int32, "low", OOM_SCORE_ADJ_MAX + 1);
level_oomadj[VMPRESS_LEVEL_MEDIUM] =
GET_LMK_PROPERTY(int32, "medium", 800);
level_oomadj[VMPRESS_LEVEL_CRITICAL] =
GET_LMK_PROPERTY(int32, "critical", 0);
// ...
}
※ 참고 및 출처 ※
https://facebookmicrosites.github.io/psi/docs/overview
Getting Started with PSI · PSI
This page describes Pressure Stall Information (PSI), how to use it, tools and components that work with PSI, and some case studies showing how PSI is used in production today for resource control in large data centers.
facebookmicrosites.github.io
How to Monitor Server via PSI (Pressure Stall Information) and cgroupv2?
We’ve been using the load average to see the health of the servers.
adil.medium.com
https://docs.kernel.org/accounting/psi.html
PSI - Pressure Stall Information — The Linux Kernel documentation
PSI - Pressure Stall Information Date April, 2018 Author Johannes Weiner When CPU, memory or IO devices are contended, workloads experience latency spikes, throughput losses, and run the risk of OOM kills. Without an accurate measure of such contention, us
docs.kernel.org
https://www.likecs.com/show-204104268.html#sc=1600
纯干货,PSI原理解析与应用-爱码网
一、什么是 PSI Pressure Stall Information 提供了一种评估系统资源压力的方法。系统有三个基础资源:CPU、Memory 和 IO,无论这些资源配置如何增加,似乎永远无法满足软件的需求。一旦产生资源竞争
www.likecs.com
https://zhuanlan.zhihu.com/p/523716850
Linux 的资源控制监测 - PSI [上]
在 Linux 系统中,多个进程是共享 CPU、I/O、物理内存等硬件资源的,每个进程都希望能多分得一些资源,那怎么评估一个进程到底需要多少资源呢? 空闲页面追踪以内存资源为例,一般会看 进程
zhuanlan.zhihu.com
https://blog.csdn.net/qq_23542165/article/details/127298727
PSI와 비슷한 기능 #1 : Load Average
- load = "process의 running 시간" + "process의 uninerruptible 시간"
- 확인 방법 : uptime 명령어
$ uptime
22:15:05 up 2 days, 1:43, 1 user, load average: 0.08, 0.02, 0.01
- 각각 1분/5분/15분 동안의 load average를 나타냄
- 한 개의 CPU를 최대로 사용하면 값은 1 (ex. 4 Core CPU의 경우, load average의 최댓값 = 4)
- 계산 원리 : per_cpu 변수를 집계하여 exp 평균 계산
for_each_possible_cpu(cpu)
nr_active += cpu_of(cpu)->nr_running + cpu_of(cpu)->nr_uninterruptible;
avenrun[n] = avenrun[0] * exp_n + nr_active * (1 - exp_n)
PSI와 비슷한 기능 #2 : vmpressure
- do_try_to_free_pages() 함수가 수행할 때 집계됨
- 메모리 회수 실패하는 비율을 나타냄
- 계산 원리
vmpressure = (1 - reclaimed/scanned)*100
- 3개의 Threshold 값
- Low : 정상 회수
- Medium : 65% 회수 실패 → Page Write Back 수행
- Critical : 90% 회수 실패 → OOM 발생
'Kernel > 理解' 카테고리의 다른 글
| ZRAM 분석 (0) | 2024.02.02 |
|---|---|
| ELF 실행 & execve (0) | 2024.02.02 |
| Bootloader 개요 (0) | 2024.02.02 |
| [mm][fs] File/Anon + VMA/Page (0) | 2024.02.02 |
| ARM64 (ARMv8) Assembly 참고 자료 (0) | 2024.02.02 |