<kernel v5.0>
VMPressure
Memory Control Ggroup을 통해 스캔한 페이지와 회수한 페이지 비율을 분석하여 메모리 압박률을 산출하고, 이에 대응하는 스레졸드별 3 가지 이벤트 레벨로 memcg에 등록한 vmpressure 리스너들에 통지할 수 있게 하였다. vmpressure 리스너들은 eventfd를 사용하여 이러한 이벤트를 수신할 수 있다.
이벤트 레벨
- low
- memcg로 지정한 메모리 압박이 적은 편이다.
- medium
- memcg로 지정한 메모리 압박이 많은 편이다.
- critical
- memcg로 지정한 메모리 압박이 심해 곧 OOM killer가 동작할 예정이다.
vmpressure_win
mm/vmpressure.c
/* * The window size (vmpressure_win) is the number of scanned pages before * we try to analyze scanned/reclaimed ratio. So the window is used as a * rate-limit tunable for the "low" level notification, and also for * averaging the ratio for medium/critical levels. Using small window * sizes can cause lot of false positives, but too big window size will * delay the notifications. * * As the vmscan reclaimer logic works with chunks which are multiple of * SWAP_CLUSTER_MAX, it makes sense to use it for the window size as well. * * TODO: Make the window size depend on machine size, as we do for vmstat * thresholds. Currently we set it to 512 pages (2MB for 4KB pages). */
static const unsigned long vmpressure_win = SWAP_CLUSTER_MAX * 16;
- SWAP_CLUSTER_MAX(32) * 16 = 512 페이지로 설정되어 있다.
- 이 윈도우 사이즈는 scanned/reclaim 비율을 분석을 시도하기 전에 사용하는 scanned 페이지 수이다.
- low 레벨 notification에 사용되고 medium/critical 레벨의 평균 비율을 위해서도 사용된다.
vmpressure_level_med & vmpressure_level_critical
mm/vmpressure.c
/* * These thresholds are used when we account memory pressure through * scanned/reclaimed ratio. The current values were chosen empirically. In * essence, they are percents: the higher the value, the more number * unsuccessful reclaims there were. */
static const unsigned int vmpressure_level_med = 60; static const unsigned int vmpressure_level_critical = 95;
- vmpressure_level_med
- scanned/reclaimed 비율로 메모리 pressure 계량시 사용되는 medium 레벨의 스레졸드 값
- vmpressure_level_critical
- scanned/reclaimed 비율로 메모리 pressure 계량시 사용되는 critical 레벨의 스레졸드 값
vmpressure_prio()
mm/vmpressure.c
/** * vmpressure_prio() - Account memory pressure through reclaimer priority level * @gfp: reclaimer's gfp mask * @memcg: cgroup memory controller handle * @prio: reclaimer's priority * * This function should be called from the reclaim path every time when * the vmscan's reclaiming priority (scanning depth) changes. * * This function does not return any value. */
void vmpressure_prio(gfp_t gfp, struct mem_cgroup *memcg, int prio)
{
/*
* We only use prio for accounting critical level. For more info
* see comment for vmpressure_level_critical_prio variable above.
*/
if (prio > vmpressure_level_critical_prio)
return;
/*
* OK, the prio is below the threshold, updating vmpressure
* information before shrinker dives into long shrinking of long
* range vmscan. Passing scanned = vmpressure_win, reclaimed = 0
* to the vmpressure() basically means that we signal 'critical'
* level.
*/
vmpressure(gfp, memcg, true, vmpressure_win, 0);
}
우선 순위가 높아져 스캔 depth가 깊어지는 경우 vmpressure 정보를 갱신한다.
- 코드 라인 7~8에서 요청 우선 순위가 vmpressure_level_critical_prio(3)보다 낮아 함수를 빠져나간다.
- prio는 낮을 수록 우선 순위가 높다.
- 코드 라인 17에서 스레졸드 이하로 prio가 떨어진 경우, 즉 우선 순위가 높아진 경우 shrinker가 오랫 동안 스캔하기 전에 vmpressure 정보를 업데이트한다.
vmpressure()
mm/vmpressure.c
/** * vmpressure() - Account memory pressure through scanned/reclaimed ratio * @gfp: reclaimer's gfp mask * @memcg: cgroup memory controller handle * @tree: legacy subtree mode * @scanned: number of pages scanned * @reclaimed: number of pages reclaimed * * This function should be called from the vmscan reclaim path to account * "instantaneous" memory pressure (scanned/reclaimed ratio). The raw * pressure index is then further refined and averaged over time. * * If @tree is set, vmpressure is in traditional userspace reporting * mode: @memcg is considered the pressure root and userspace is * notified of the entire subtree's reclaim efficiency. * * If @tree is not set, reclaim efficiency is recorded for @memcg, and * only in-kernel users are notified. * * This function does not return any value. */
void vmpressure(gfp_t gfp, struct mem_cgroup *memcg, bool tree,
unsigned long scanned, unsigned long reclaimed)
{
struct vmpressure *vmpr = memcg_to_vmpressure(memcg);
/*
* Here we only want to account pressure that userland is able to
* help us with. For example, suppose that DMA zone is under
* pressure; if we notify userland about that kind of pressure,
* then it will be mostly a waste as it will trigger unnecessary
* freeing of memory by userland (since userland is more likely to
* have HIGHMEM/MOVABLE pages instead of the DMA fallback). That
* is why we include only movable, highmem and FS/IO pages.
* Indirect reclaim (kswapd) sets sc->gfp_mask to GFP_KERNEL, so
* we account it too.
*/
if (!(gfp & (__GFP_HIGHMEM | __GFP_MOVABLE | __GFP_IO | __GFP_FS)))
return;
/*
* If we got here with no pages scanned, then that is an indicator
* that reclaimer was unable to find any shrinkable LRUs at the
* current scanning depth. But it does not mean that we should
* report the critical pressure, yet. If the scanning priority
* (scanning depth) goes too high (deep), we will be notified
* through vmpressure_prio(). But so far, keep calm.
*/
if (!scanned)
return;
if (tree) {
spin_lock(&vmpr->sr_lock);
scanned = vmpr->tree_scanned += scanned;
vmpr->tree_reclaimed += reclaimed;
spin_unlock(&vmpr->sr_lock);
if (scanned < vmpressure_win)
return;
schedule_work(&vmpr->work);
} else {
enum vmpressure_levels level;
/* For now, no users for root-level efficiency */
if (!memcg || memcg == root_mem_cgroup)
return;
spin_lock(&vmpr->sr_lock);
scanned = vmpr->scanned += scanned;
reclaimed = vmpr->reclaimed += reclaimed;
if (scanned < vmpressure_win) {
spin_unlock(&vmpr->sr_lock);
return;
}
vmpr->scanned = vmpr->reclaimed = 0;
spin_unlock(&vmpr->sr_lock);
level = vmpressure_calc_level(scanned, reclaimed);
if (level > VMPRESSURE_LOW) {
/*
* Let the socket buffer allocator know that
* we are having trouble reclaiming LRU pages.
*
* For hysteresis keep the pressure state
* asserted for a second in which subsequent
* pressure events can occur.
*/
memcg->socket_pressure = jiffies + HZ;
}
}
}
scaned 및 reclaimed 비율로 메모리 pressure를 계량한다.
- 코드 라인 4에서 요청한 memcg의 vmpressure 정보를 반환한다.
- 코드 라인 17~18에서 highmem, movable, FS, IO 플래그 요청이 하나도 없는 경우 pressure 계량을 하지 않는다.
- 코드 라인 28~29에서 인수 scanned가 0인 경우 함수를 중단한다.
- 코드 라인 31~39에서 기존 tree 방식의 presssure를 계량한다. tree_scanned와 tree_reclaimed 각각 그 만큼 증가시키고 vmpr->work에 등록한 작업을 실행시킨다. 만일 vmpr->scanned가 vmpressure_win 보다 작은 경우 함수를 중단한다.
- vmpressure_work_fn()
- 코드 라인 40~45에서 @tree가 0이면 커널 내부 사용자에게 통지하기 위해 @memcg를 위한 회수 효율성이 기록된다. memcg가 지정되지 않은 경우 함수를 중단한다.
- 코드 라인 47~55에서 scanned와 reclaimed 각각 그 만큼 증가시키고 만일 scanned가 vmpressure_win 보다 작은 경우 함수를 중단한다. 중단하지 않은 경우 vmpr의 scanned와 reclaimed는 0으로 리셋한다.
- 코드 라인 57~69에서 산출된 vmpressure 레벨이 VMPRESSURE_LOW를 초과하면 memcg의 socket_pressure를 현재 시각보다 1초 뒤인 틱 값을 설정한다.
- mem_cgroup_under_socket_pressure() 함수에서 이 값을 사용한다.
- 참고: mm: memcontrol: hook up vmpressure to socket pressure
다음 그림은 vmpressure() 함수가 처리되는 과정을 보여준다.
워크 큐에서 vmpressure에 따른 이벤트 통지
vmpressure_work_fn()
mm/vmpressure.c
static void vmpressure_work_fn(struct work_struct *work)
{
struct vmpressure *vmpr = work_to_vmpressure(work);
unsigned long scanned;
unsigned long reclaimed;
enum vmpressure_levels level;
bool ancestor = false;
bool signalled = false;
spin_lock(&vmpr->sr_lock);
/*
* Several contexts might be calling vmpressure(), so it is
* possible that the work was rescheduled again before the old
* work context cleared the counters. In that case we will run
* just after the old work returns, but then scanned might be zero
* here. No need for any locks here since we don't care if
* vmpr->reclaimed is in sync.
*/
scanned = vmpr->tree_scanned;
if (!scanned) {
spin_unlock(&vmpr->sr_lock);
return;
}
reclaimed = vmpr->tree_reclaimed;
vmpr->tree_scanned = 0;
vmpr->tree_reclaimed = 0;
spin_unlock(&vmpr->sr_lock);
level = vmpressure_calc_level(scanned, reclaimed);
do {
if (vmpressure_event(vmpr, level, ancestor, signalled))
signalled = true;
ancestor = true;
} while ((vmpr = vmpressure_parent(vmpr)));
}
메모리 압박 레벨을 산출하고 레벨 및 모드 조건을 만족시키는 vmpressure 리스너에 이벤트를 전송한다.
- 코드 라인 10~28에서 tree_scanned 값과 tree_reclaimed 값을 가져오고 리셋한다.
- 코드 라인 30에서 scanned 값과 reclaimed 값으로 레벨을 산출한다.
- 코드 라인 32~36에서 하이라키로 구성된 memcg의 vmpressure 값을 최상위 루트까지 순회하며 조건을 만족시키는 vmpressure 리스너에 이벤트를 통지한다.
memcg_to_vmpressure()
mm/memcontrol.c
/* Some nice accessors for the vmpressure. */
struct vmpressure *memcg_to_vmpressure(struct mem_cgroup *memcg)
{
if (!memcg)
memcg = root_mem_cgroup;
return &memcg->vmpressure;
}
요청한 memcg의 vmpressure 정보를 반환한다. memcg가 지정되지 않은 경우 root memcg의 vmpressure를 반환한다.
다음 그림은 등록된 vmpressure 리스너들 중 조건에 맞는 리스너들을 대상으로 이벤트를 보내는 과정을 보여준다.
vmpressure 이벤트 통지
vmpressure_event()
mm/vmpressure.c
static bool vmpressure_event(struct vmpressure *vmpr,
const enum vmpressure_levels level,
bool ancestor, bool signalled)
{
struct vmpressure_event *ev;
bool ret = false;
mutex_lock(&vmpr->events_lock);
list_for_each_entry(ev, &vmpr->events, node) {
if (ancestor && ev->mode == VMPRESSURE_LOCAL)
continue;
if (signalled && ev->mode == VMPRESSURE_NO_PASSTHROUGH)
continue;
if (level < ev->level)
continue;
eventfd_signal(ev->efd, 1);
ret = true;
}
mutex_unlock(&vmpr->events_lock);
return ret;
}
vmpressure에 등록된 이벤트들을 대상으로 요청 @level 이하로 등록한 vmpressure 리스터 application에 eventfd 시그널을 통지한다.
- 통지 대상이 아닌 경우는 다음과 같다.
- @ancestor=1일 때, local 모드는 제외한다.
- @signalled=1일 때, no_passthrough 모드는 제외한다.
- @level보다 큰 레벨로 등록한 경우는 제외한다.
다음 그림은 memcg에 등록한 vmpressure 리스너에 이벤트를 통지하는 조건들을 보여준다.
vmpressure 레벨 산출
vmpressure_calc_level()
mm/vmpressure.c
static enum vmpressure_levels vmpressure_calc_level(unsigned long scanned,
unsigned long reclaimed)
{
unsigned long scale = scanned + reclaimed;
unsigned long pressure = 0;
/*
* reclaimed can be greater than scanned for things such as reclaimed
* slab pages. shrink_node() just adds reclaimed pages without a
* related increment to scanned pages.
*/
if (reclaimed >= scanned)
goto out;
/*
* We calculate the ratio (in percents) of how many pages were
* scanned vs. reclaimed in a given time frame (window). Note that
* time is in VM reclaimer's "ticks", i.e. number of pages
* scanned. This makes it possible to set desired reaction time
* and serves as a ratelimit.
*/
pressure = scale - (reclaimed * scale / scanned);
pressure = pressure * 100 / scale;
out:
pr_debug("%s: %3lu (s: %lu r: %lu)\n", __func__, pressure,
scanned, reclaimed);
return vmpressure_level(pressure);
}
scanned, reclaimed 비율에 따라 pressure 값을 산출하고, 이에 따른 레벨을 반환한다.
다음과 예와 같이 scanned 페이지 수와 reclaimed 페이지 수에 대한 pressure 값과 레벨을 확인해보자.
- scanned=5, reclaimed=0
- pressure=100%, level=critical
- scanned=5, reclaimed=1
- pressure=66%, level=medium
- scanned=5, reclaimed=2
- pressure=57%, level=low
- scanned=5, reclaimed=3
- pressure=37%, level=low
- scanned=5, reclaimed=4
- pressure=11%, level=low
- scanned=5, reclaimed=5
- pressure=0%, level=low
다음 그림은 scanned, reclaimed 비율에 따른 pressure 값을 산출하고, 이에 따른 레벨을 결정하는 과정을 보여준다.
vmpressure_level()
mm/vmpressure.c
static enum vmpressure_levels vmpressure_level(unsigned long pressure)
{
if (pressure >= vmpressure_level_critical)
return VMPRESSURE_CRITICAL;
else if (pressure >= vmpressure_level_med)
return VMPRESSURE_MEDIUM;
return VMPRESSURE_LOW;
}
@pressure에 따른 레벨을 반환한다.
- critical
- 디폴트 값 95% 이상
- med
- 디폴트 값 60% 이상
- low
- 그 외
이벤트 수신 프로그램 데모
cgroup_event_listener
tools/cgroup 위치에서 make를 실행하면 다음 소스를 빌드하여 cgroup_event_listener 파일이 생성된다.
tools/cgroup/cgroup_event_listener.c
#include <assert.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <libgen.h>
#include <limits.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/eventfd.h>
#define USAGE_STR "Usage: cgroup_event_listener <path-to-control-file> <args>"
int main(int argc, char **argv)
{
int efd = -1;
int cfd = -1;
int event_control = -1;
char event_control_path[PATH_MAX];
char line[LINE_MAX];
int ret;
if (argc != 3)
errx(1, "%s", USAGE_STR);
cfd = open(argv[1], O_RDONLY);
if (cfd == -1)
err(1, "Cannot open %s", argv[1]);
ret = snprintf(event_control_path, PATH_MAX, "%s/cgroup.event_control",
dirname(argv[1]));
if (ret >= PATH_MAX)
errx(1, "Path to cgroup.event_control is too long");
event_control = open(event_control_path, O_WRONLY);
if (event_control == -1)
err(1, "Cannot open %s", event_control_path);
efd = eventfd(0, 0);
if (efd == -1)
err(1, "eventfd() failed");
ret = snprintf(line, LINE_MAX, "%d %d %s", efd, cfd, argv[2]);
if (ret >= LINE_MAX)
errx(1, "Arguments string is too long");
ret = write(event_control, line, strlen(line) + 1);
if (ret == -1)
err(1, "Cannot write to cgroup.event_control");
while (1) {
uint64_t result;
ret = read(efd, &result, sizeof(result));
if (ret == -1) {
if (errno == EINTR)
continue;
err(1, "Cannot read from eventfd");
}
assert(ret == sizeof(result));
ret = access(event_control_path, W_OK);
if ((ret == -1) && (errno == ENOENT)) {
puts("The cgroup seems to have removed.");
break;
}
if (ret == -1)
err(1, "cgroup.event_control is not accessible any more");
printf("%s %s: crossed\n", argv[1], argv[2]);
}
return 0;
}
사용 방법
다음과 같이 pressure 레벨이 medium일 때 이벤트를 수신할 수 있게 한다.
- 참고: memcg: Add memory.pressure_level events | LWN.net
# cd /sys/fs/cgroup/memory/ $ mkdir foo $ cd foo $ cgroup_event_listener memory.pressure_level medium & $ echo 8000000 > memory.limit_in_bytes $ echo 8000000 > memory.memsw.limit_in_bytes $ echo $$ > tasks $ dd if=/dev/zero | read x
PSI(Process Stall Information)
메모리 압박을 감시하고자 하는 유저 application(안드로이드의 lmkd 등)이 메모리 회수 동작에서 받는 압박 레벨을 catch 하고자 2018년 커널 v4.20-rc1에서 소개되었다.
- 참고
참고
- Memory Resource Controller (Documentation/cgroups/memory.txt) | LWN.net
- 로우 메모리 킬러 데몬 | Android



