VMPressure

<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초 뒤인 틱 값을 설정한다.

 

다음 그림은 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일 때 이벤트를 수신할 수 있게 한다.

# 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에서 소개되었다.

 

참고

 

댓글 남기기