# vmpressure 📊
관련 소스:mm/vmpressure.c,include/linux/vmpressure.h,mm/vmscan.c,mm/memcontrol.c
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
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; /* 지연된 이벤트 알림 워크큐 작업 */
};
사용자가 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 연결 노드 */
};
압력 수준은 스캔/회수 비율에 따라 세 단계로 분류됩니다.
// mm/vmpressure.c:85-90
enum vmpressure_levels {
VMPRESSURE_LOW = 0, /* 회수 효율 양호 (< 60%) */
VMPRESSURE_MEDIUM, /* 주의 수준 (≥ 60%) */
VMPRESSURE_CRITICAL, /* 위험 수준 (≥ 95%) */
VMPRESSURE_NUM_LEVELS,
};
알림 모드는 이벤트의 전파 범위를 결정합니다.
// 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 */
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: 즉시 소켓 알림
리클레이머의 스캔 깊이가 기준 이하로 내려가면 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%를 스캔한 셈입니다.
압력 퍼센트를 계산하고 수준을 판정합니다.
// 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 임계값으로 판정 */
}
워크큐에서 실행되며, 윈도우 도달 후 상위 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)));
}
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);
}
[사용자空间] 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=true | tree=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.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 | cgroup v2 |
|---|---|---|
| tree=true 호출 | 허용 | 허용 |
| tree=false 호출 | 무시 (`!cgroup_subsys_on_dfl()` 체크) | 허용 |
| 이벤트 등록 | `memory.pressure_level` (별도) | `memory.pressure_level` (기본) |
| in-kernel 사용자 | 없음 | 소켓 할당기 등 |
| memcg 루트 | `mem_cgroup_is_root()` → non-tree 무시 | 동일 |
Linux 4.20에서 도입된 PSI는 시간 기반의 메모리 압력 측정 반면, vmpressure는 스캔/회수 비율 기반입니다. 두 시스템은 서로 보완적으로 동작합니다:
memcg v2에서는 PSI와 vmpressure가 모두 활성화되며, memory.pressure_level (vmpressure)과 /proc/pressure/memory (PSI)가 각각 다른 관점에서 메모리 압력을 제공합니다.
| 항목 | vmpressure | PSI |
|---|---|---|
| 측정 단위 | 회수 실패 비율 (%) | 대기 시간 (μs) |
| 트리거 | 리클레이머 스캔/회수 | 프로세스 대기 |
| 알림 방식 | eventfd | 파일 읽기 (`/proc/pressure/memory`) |
| 도입 커널 | 3.11 | 4.20 |