Ryotta's Linux 7.0 MM

메모리 관리 서브시스템 완전 분석

# vmpressure 📊

관련 소스: mm/vmpressure.c, include/linux/vmpressure.h, mm/vmscan.c, mm/memcontrol.c

개요 (Overview)

vmpressure는 Linux 커널의 메모리 압력 알림 프레임워크로, vmscan 리클레이머가 스캔/회수한 페이지 비율을 기반으로 사용자 공간에 메모리 압력 수준을 통보합니다. 리클레이머가 페이지를 많이 스캔했지만 적게 회수하면 메모리 압력이 높다는 뜻이며, 이 정보를 eventfd를 통해 사용자 프로세스에 전달합니다. cgroup v2에서는 모든 memcg에 대해 기본으로 활성화되며, PSI(Pressure Stall Information)와 함께 메모리 압력 모니터링의 이중 경로를 구성합니다.

일상 비유: vmpressure는 압력솥의 안전밸브와 같습니다. 밥솥 안의 압력(메모리 사용량)이 일정 수준 이상으로 높아지면 밸브가 작동하여 사용자에게 "압력이 높아지고 있다"고 알려줍니다. low는 증기가 살짝 나기 시작할 때, medium은 본격적으로 김이 날 때, critical은 터지기 직전의 위험 수준입니다.

vmpressure는 세 가지 압력 수준(low/medium/critical)과 세 가지 알림 모드(default/hierarchy/local)를 제공합니다. 스캔/회수 비율 계산은 SWAP_CLUSTER_MAX * 16 윈도우(기본 512페이지 ≈ 2MB)에서 수행되며, 리클레이머의 스캔 우선순위(prio) 변화를 감지하여 critical 레벨을 보조적으로 탐지합니다. 이 문서에서는 vmpressure의 핵심 자료구조, 알고리즘, 알림 메커니즘, memcg 통합을 분석합니다.

소스 파일 경로

mm/vmpressure.c                 ← 메인 구현 (481줄)
include/linux/vmpressure.h      ← struct vmpressure, API 선언 (52줄)
mm/vmscan.c                     ← vmpressure() 호출 지점 (4곳)
mm/memcontrol.c                 ← memcg_to_vmpressure(), 초기화/정리
mm/memcontrol-v1.c              ← cgroup v1 이벤트 등록

빠른 점검 명령

# 1. vmpressure 관련 커널 심볼 확인
cat /proc/kallsyms | grep -E "vmpressure|memcg_to_vmpressure"

# 2. memcg 이벤트 vmpressure 등록 여부 확인
ls /sys/fs/cgroup/memory/*.event 2>/dev/null; cat /sys/fs/cgroup/cgroup.events 2>/dev/null

# 3. vmpressure 임계값 확인 (커널 상수)
cat /proc/kallsyms | grep -E "vmpressure_level_med|vmpressure_level_critical"

# 4. PSI 통계와 vmpressure 비교
cat /proc/pressure/memory

# 5. vmscan에서 vmpressure 호출부 grep
grep -rn "vmpressure\b" mm/vmscan.c

# 6. memcg vmpressure 초기화 확인
grep -rn "vmpressure_init\|vmpressure_cleanup" mm/memcontrol.c

# 7. cgroup v1 vs v2 이벤트 메커니즘 비교
grep -rn "vmpressure_register_event\|vmpressure_unregister_event" mm/

# 8. vmpressure 윈도우 크기 확인 (SWAP_CLUSTER_MAX * 16)
cat /proc/kallsyms | grep -E "vmpressure_win"

# 9. cgroup v2 memory 이벤트 파일 확인
ls /sys/fs/cgroup/*.events 2>/dev/null | head -5

# 10. 스캔 우선순위 임계값 확인 (prio ≤ 3 = LRU 12.5% 스캔)
cat /proc/kallsyms | grep -E "vmpressure_level_critical_prio"

# 11. vmscan.c에서 proactive 플래그와 함께 vmpressure 호출 여부 확인
grep -n "proactive.*vmpressure\|vmpressure.*proactive" mm/vmscan.c

# 12. mem_cgroup_set_socket_pressure() 호출부 확인
grep -rn "socket_pressure" mm/memcontrol.c

핵심 자료구조

struct vmpressure

memcg당 하나의 vmpressure 구조체가 존재합니다. 스캔/회수 카운터와 이벤트 알림 리스트를 관리합니다.

// include/linux/vmpressure.h:13-28
struct vmpressure {
	/* 레거시 cgroup 모드 (tree=true)용 스캔/회수 카운터 */
	unsigned long scanned;          /* memcg 자체 스캔 페이지 수 */
	unsigned long reclaimed;        /* memcg 자체 회수 페이지 수 */

	/* 트리 전체 통합 모드 (tree=true)용 카운터 */
	unsigned long tree_scanned;     /* 서브트리 전체 스캔 페이지 수 */
	unsigned long tree_reclaimed;   /* 서브트리 전체 회수 페이지 수 */

	spinlock_t sr_lock;             /* scanned/reclaimed 동기화 락 */

	struct list_head events;        /* vmpressure_event 연결 리스트 */
	struct mutex events_lock;       /* 이벤트 리스트 보호 뮤텍스 */

	struct work_struct work;        /* 지연된 이벤트 알림 워크큐 작업 */
};

struct vmpressure_event

사용자가 eventfd를 통해 등록한 알림 구독입니다. memcg의 memory.pressure_level 인터페이스와 연동됩니다.

// mm/vmpressure.c:150-155
struct vmpressure_event {
	struct eventfd_ctx *efd;        /* 알림 대상 eventfd 컨텍스트 */
	enum vmpressure_levels level;  /* 트리거 최소 압력 수준 */
	enum vmpressure_modes mode;    /* 알림 전파 모드 */
	struct list_head node;         /* vmpressure.events 연결 노드 */
};

enum vmpressure_levels

압력 수준은 스캔/회수 비율에 따라 세 단계로 분류됩니다.

// mm/vmpressure.c:85-90
enum vmpressure_levels {
	VMPRESSURE_LOW = 0,      /* 회수 효율 양호 (< 60%) */
	VMPRESSURE_MEDIUM,       /* 주의 수준 (≥ 60%) */
	VMPRESSURE_CRITICAL,     /* 위험 수준 (≥ 95%) */
	VMPRESSURE_NUM_LEVELS,
};

enum vmpressure_modes

알림 모드는 이벤트의 전파 범위를 결정합니다.

// mm/vmpressure.c:92-97
enum vmpressure_modes {
	VMPRESSURE_NO_PASSTHROUGH = 0, /* 기본: 현재 memcg만 알림 */
	VMPRESSURE_HIERARCHY,          /* 상위 memcg로 알림 전파 */
	VMPRESSURE_LOCAL,              /* 로컬: 조상은 알림 무시 */
	VMPRESSURE_NUM_MODES,
};

압력 임계값 상수

// mm/vmpressure.c:38,46-47,68
static const unsigned long vmpressure_win = SWAP_CLUSTER_MAX * 16;  /* 윈도우: 512페이지 ≈ 2MB */
static const unsigned int vmpressure_level_med = 60;                /* medium 임계값 */
static const unsigned int vmpressure_level_critical = 95;           /* critical 임계값 */
static const unsigned int vmpressure_level_critical_prio = ilog2(100 / 10); /* prio ≤ 3 */

핵심 함수

vmpressure() — 스캔/회수 비율 기반 압력 계산 (메인 엔트리)

vmscan 리클레이머가 각 회수 사이클에서 호출합니다. 두 가지 경로로 동작합니다.

// mm/vmpressure.c:239-322
void vmpressure(gfp_t gfp, struct mem_cgroup *memcg, bool tree,
		unsigned long scanned, unsigned long reclaimed)
{
	/* memcg 비활성화 시 무시 */
	if (mem_cgroup_disabled())
		return;

	/* cgroup v1 legacy 모드에서 tree=false는 무시 (인_kernel 사용자 없음) */
	if (!cgroup_subsys_on_dfl(memory_cgrp_subsys) && !tree)
		return;

	vmpr = memcg_to_vmpressure(memcg);

	/* DMA 등 사용자 도움이 불가능한 영역의 압력은 무시 */
	if (!(gfp & (__GFP_HIGHMEM | __GFP_MOVABLE | __GFP_IO | __GFP_FS)))
		return;

	/* 스캔된 페이지가 0이면 아직 안정적 (prio 상승 시 재호출됨) */
	if (!scanned)
		return;

	if (tree) {
		/* 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 {
		/* non-tree 모드: 인_kernel 알림 (소켓 할당기 등) */
		if (!memcg || mem_cgroup_is_root(memcg))
			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)
			mem_cgroup_set_socket_pressure(memcg);
	}
}

분기 로직:

1. cgroup_subsys_on_dfl() → cgroup v2이면 모든 모드 허용, v1이면 tree=true만 허용

2. GFP 플래그 검사 → DMA 전용 페이지 회수 시 사용자 알림 불필요

3. tree 플래그 → tree 모드: 워크큐 지연 알림, non-tree: 즉시 소켓 알림

vmpressure_prio() — 스캔 우선순위 기반 critical 탐지

리클레이머의 스캔 깊이가 기준 이하로 내려가면 critical을 강제합니다.

// mm/vmpressure.c:335-352
void vmpressure_prio(gfp_t gfp, struct mem_cgroup *memcg, int prio)
{
	/* prio > 3이면 아직 충분히 깊이 스캔하지 않음 — 무시 */
	if (prio > vmpressure_level_critical_prio)
		return;

	/* prio ≤ 3이면 LRU의 약 12.5% 스캔 → critical 강제 알림 */
	vmpressure(gfp, memcg, true, vmpressure_win, 0);
}

참고: vmpressure_level_critical_prio = ilog2(10) = 3. 리클레이머는 lru_size >> prio 페이지를 스캔하므로, prio=3이면 LRU의 12.5%를 스캔한 셈입니다.

vmpressure_calc_level() — 스캔/회수 비율 계산

압력 퍼센트를 계산하고 수준을 판정합니다.

// mm/vmpressure.c:120-148
static enum vmpressure_levels vmpressure_calc_level(unsigned long scanned,
						    unsigned long reclaimed)
{
	unsigned long scale = scanned + reclaimed;
	unsigned long pressure = 0;

	/* slab 등에서 reclaimed > scanned 가능 — 이 경우 pressure = 0 */
	if (reclaimed >= scanned)
		goto out;

	/* 압력 = (스캔된 것 중 회수 실패한 비율) × 100 */
	pressure = scale - (reclaimed * scale / scanned);
	pressure = pressure * 100 / scale;

out:
	return vmpressure_level(pressure);  /* 60/95 임계값으로 판정 */
}

vmpressure_work_fn() — 지연된 이벤트 알림 처리

워크큐에서 실행되며, 윈도우 도달 후 상위 memcg까지 전파합니다.

// mm/vmpressure.c:180-216
static void vmpressure_work_fn(struct work_struct *work)
{
	struct vmpressure *vmpr = work_to_vmpressure(work);

	spin_lock(&vmpr->sr_lock);
	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);

	/* 현재 memcg → 부모 memcg 순서로 이벤트 전파 */
	do {
		if (vmpressure_event(vmpr, level, ancestor, signalled))
			signalled = true;
		ancestor = true;       /* 이후 반복부터는 ancestor=true */
	} while ((vmpr = vmpressure_parent(vmpr)));
}

vmpressure_register_event() — eventfd 알림 구독 등록

cgroup의 memory.pressure_level 인터페이스에서 호출됩니다.

// mm/vmpressure.c:374-422
int vmpressure_register_event(struct mem_cgroup *memcg,
			      struct eventfd_ctx *eventfd, const char *args)
{
	/* args 파싱: "critical,hierarchy" 같은 쉼표 구분 문자열 */
	token = strsep(&spec, ",");
	level = match_string(vmpressure_str_levels, VMPRESSURE_NUM_LEVELS, token);

	token = strsep(&spec, ",");
	if (token)
		mode = match_string(vmpressure_str_modes, VMPRESSURE_NUM_MODES, token);

	ev = kzalloc_obj(*ev);
	ev->efd = eventfd;
	ev->level = level;
	ev->mode = mode;

	mutex_lock(&vmpr->events_lock);
	list_add(&ev->node, &vmpr->events);
	mutex_unlock(&vmpr->events_lock);
}

vmpressure 호출 흐름
vmpressure 자료구조 관계도

호출 흐름

[사용자空间] epoll_wait() / eventfd_read()
    ↑ eventfd_signal()
[커널: 이벤트 알림]
    vmpressure_event()
        ← vmpressure_work_fn()     ← schedule_work() ← vmpressure(tree=true)
        ← vmpressure() non-tree    ← mem_cgroup_set_socket_pressure()

[커널: 리클레이 경로 — MGLRU]
    vmscan.c: shrink_one() (L4931)
        └─ vmpressure(gfp, memcg, false, scanned, reclaimed)   ← non-tree 알림

[커널: 리클레이 경로 — 기존 LRU]
    vmscan.c: shrink_node_memcgs() (L6027)
        └─ vmpressure(gfp, memcg, false, scanned, reclaimed)   ← non-tree 알림
    vmscan.c: shrink_node() (L6069)
        └─ vmpressure(gfp, memcg, true, scanned, reclaimed)    ← tree 알림

[커널: 우선순위 변화 감지]
    vmscan.c: do_try_to_free_pages() (L6359)
        └─ vmpressure_prio(gfp, memcg, prio)
            └─ vmpressure(gfp, memcg, true, vmpressure_win, 0)

[초기화/정리]
    memcontrol.c: mem_cgroup_css_alloc() (L3792)
        └─ vmpressure_init(&memcg->vmpressure)
            └─ INIT_WORK(&vmpr->work, vmpressure_work_fn)
    memcontrol.c: mem_cgroup_css_free() (L3967)
        └─ vmpressure_cleanup(&memcg->vmpressure)
            └─ flush_work(&vmpr->work)

조건별 비교

압력 수준 판정 기준

수준스캔/회수 비율의미사용자 알림
`LOW`< 60% 회수 실패리클레이머가 충분히 회수 중알림 없음
`MEDIUM`60%~95% 회수 실패메모리 확보 어려움 시작eventfd 신호
`CRITICAL`≥ 95% 회수 실패 또는 prio ≤ 3메모리 부족 임박eventfd 신호

알림 모드 비교

모드현재 memcg부모 memcg용도
`default` (NO_PASSTHROUGH)알림알림 (signalled=false일 때만)일반 사용자 앱
`hierarchy`알림알림 (조상도)계층적 모니터링
`local`알림무시특정 memcg만 추적

tree 모드 vs non-tree 모드

항목tree=truetree=false
대상cgroup subtree 전체단일 memcg
카운터`tree_scanned/reclaimed``scanned/reclaimed`
알림 방식워크큐 지연 (윈도우 도달 후)즉시 계산
사용자 알림eventfd 시그널`mem_cgroup_set_socket_pressure()`
cgroup 버전v1, v2 모두v2만
호출 지점`shrink_node()` (tree 경로)`shrink_one()`, `shrink_node_memcgs()` (비-트리 경로)

vmscan 호출 지점

파일:라인호출 함수모드호출 컨텍스트
`vmscan.c:4931``vmpressure()``false` (non-tree)`shrink_one()` — MGLRU 경로
`vmscan.c:6027``vmpressure()``false` (non-tree)`shrink_node_memcgs()` — 기존 LRU 경로
`vmscan.c:6069``vmpressure()``true` (tree)`shrink_node()` — 서브트리 전체
`vmscan.c:6359``vmpressure_prio()``true``do_try_to_free_pages()` — 우선순위 변화

cgroup v1 vs v2 동작 비교

항목cgroup v1cgroup v2
tree=true 호출허용허용
tree=false 호출무시 (`!cgroup_subsys_on_dfl()` 체크)허용
이벤트 등록`memory.pressure_level` (별도)`memory.pressure_level` (기본)
in-kernel 사용자없음소켓 할당기 등
memcg 루트`mem_cgroup_is_root()` → non-tree 무시동일

PSI(Pressure Stall Information)와의 관계

Linux 4.20에서 도입된 PSI는 시간 기반의 메모리 압력 측정 반면, vmpressure는 스캔/회수 비율 기반입니다. 두 시스템은 서로 보완적으로 동작합니다:

  • PSI: CPU/메모리/IO 자원에서 프로세스가 대기한 시간 비율을 측정 (μs 단위)
  • vmpressure: 리클레이머의 회수 효율성을 측정 (비율 단위)
  • memcg v2에서는 PSI와 vmpressure가 모두 활성화되며, memory.pressure_level (vmpressure)과 /proc/pressure/memory (PSI)가 각각 다른 관점에서 메모리 압력을 제공합니다.

    항목vmpressurePSI
    측정 단위회수 실패 비율 (%)대기 시간 (μs)
    트리거리클레이머 스캔/회수프로세스 대기
    알림 방식eventfd파일 읽기 (`/proc/pressure/memory`)
    도입 커널3.114.20

    관련 문서

  • 메모리 관리 개요
  • 페이지 회수 (vmscan)
  • Memory Cgroup
  • Swap / zswap
  • OOM Killer