Memory Cgroup는 cgroup v2의 메모리 컨트롤러로, 프로세스 그룹별 메모리 사용량을 계정(charge), 제한(limit), 회수(reclaim)하는 커널 서브시스템입니다. 컨테이너 환경(Docker, Kubernetes)에서 메모리 격리(Isolation)의 핵심 도구이며, memory.max(하드 제한), memory.high(소프트 제한/throttle), memory.min/memory.low(보호 수준)를 통해 유연한 메모리 관리를 제공합니다. 각 memcg는 독립적인 LRU 리스트, 회수 정책, OOM Killer 범위를 가지며, cgroup 계층 구조를 통해 부모-자식 간 자원 배분이 가능합니다.
커널 내부에서 memcg는 페이지 할당 시 page_counter를 통해 계정이 이루어지며, 한계 초과 시 직접 회수(direct reclaim) 또는 유저 모드 복귀 시 throttle(__mem_cgroup_handle_over_high)이 발생합니다. cgroup v1과 v2 모두 지원하며, v1에서는 memory.limit_in_bytes 등 레거시 인터페이스를, v2에서는 memory.max/memory.high/memory.low/memory.min을 사용합니다.
일상 비유로 보면 memcg는 대형 건물의 층별 전력 계량기와 차단기에 가깝습니다. 각 층은 자기 계량기(memory.current)로 사용량을 보고, 관리자는 절대 보장량(memory.min), 가능하면 지켜 줄 보호량(memory.low), 과열을 막는 경고선(memory.high), 넘으면 차단되는 최대치(memory.max)를 설정합니다. 컨테이너는 별도 물리 메모리맵을 갖는 것이 아니라 호스트 커널의 같은 메모리맵을 공유하면서 이 계량기와 차단기만 따로 적용받습니다.
// 소스 파일 경로
mm/memcontrol.c // 메모리 컨트롤러 핵심 구현 (5679줄)
mm/memcontrol-v1.c // cgroup v1 레거시 인터페이스 (2243줄)
include/linux/memcontrol.h // 핵심 구조체 정의 (1939줄)
include/linux/page_counter.h // 페이지 카운터 기반 계정
# 1. 메모리 cgroup 전체 현황
cat /proc/cgroups | grep memory
# 2. 현재 메모리 cgroup 계층 구조 (cgroup v2)
ls /sys/fs/cgroup/
cat /sys/fs/cgroup/memory.current # 현재 사용량 (바이트)
cat /sys/fs/cgroup/memory.max # 하드 제한
cat /sys/fs/cgroup/memory.high # 소프트 제한
cat /sys/fs/cgroup/memory.stat # 상세 통계
# 3. 프로세스의 memcg 소속 확인
cat /proc/$$/cgroup # 현재 셸의 cgroup 경로
cat /proc/$$/status | grep -i "voluntary_ctxt\|nonvoluntary_ctxt"
# 4. 메모리 이벤트 확인 (OOM, high 등)
cat /sys/fs/cgroup/memory.events
# oom: memcg OOM 발생 횟수
# oom_kill: OOM killer에 의해 종료된 프로세스 수
# high: memory.high 초과로 회수 시도된 횟수
# 5. memcg별 메모리 압력 확인 (PSI)
cat /proc/pressure/memory
cat /sys/fs/cgroup/memory.pressure # cgroup별 PSI (커널 6.1+)
# 6. 메모리 cgroup 통계 (v1 레거시)
cat /sys/fs/cgroup/memory/memory.stat 2>/dev/null | head -20
# 7. memcg OOM 그룹 킬 설정 확인
cat /sys/fs/cgroup/memory.oom.group # 0 또는 1
# 8. 스왑 사용량 확인
cat /sys/fs/cgroup/memory.swap.current # 현재 스왑 사용량
cat /sys/fs/cgroup/memory.swap.max # 스왑 하드 제한
# 9. cgroup v2 메모리 제한 테스트용 그룹 생성
sudo mkdir -p /sys/fs/cgroup/myapp
echo 512M | sudo tee /sys/fs/cgroup/myapp/memory.max
echo 400M | sudo tee /sys/fs/cgroup/myapp/memory.high
echo 256M | sudo tee /sys/fs/cgroup/myapp/memory.low
echo 128M | sudo tee /sys/fs/cgroup/myapp/memory.min
# 10. 프로세스를 테스트 그룹에 연결
# PID에는 관찰할 대상 프로세스 번호를 넣습니다.
PID=12345
echo $PID | sudo tee /sys/fs/cgroup/myapp/cgroup.procs
cat /sys/fs/cgroup/myapp/memory.current
cat /sys/fs/cgroup/myapp/memory.stat | head -30
cat /sys/fs/cgroup/myapp/memory.events
# 11. 계층 전체의 핵심 메모리 파일 훑기
grep -R . /sys/fs/cgroup/memory.{current,max,high,low,min,events,numa_stat,oom.group} 2>/dev/null
# 12. cgroup v2 컨트롤러 활성화 상태 확인
cat /sys/fs/cgroup/cgroup.controllers
cat /sys/fs/cgroup/cgroup.subtree_control
메모리 컨트롤러의 핵심 구조체. 각 cgroup에 하나씩 생성되며, 페이지 계정, 한계 설정, 통계, 회수 이터레이터 등을 포함합니다.
// include/linux/memcontrol.h:190-325
struct mem_cgroup {
struct cgroup_subsys_state css;
/* memcg 비공개 ID. cgroup보다 오래 사는 오브젝트 식별에 사용 */
struct mem_cgroup_private_id id;
/* 계정 대상 자원 */
struct page_counter memory; /* v1과 v2 모두 사용 */
union {
struct page_counter swap; /* v2 전용 */
struct page_counter memsw; /* v1 전용 */
};
/* 등록된 로컬 피크 감시자 */
struct list_head memory_peaks;
struct list_head swap_peaks;
spinlock_t peaks_lock;
/* 인터럽트 계정의 범위 강제 */
struct work_struct high_work;
#ifdef CONFIG_ZSWAP
unsigned long zswap_max;
/* 이 memcg의 페이지가 zswap에서 swap으로 되돌려 쓰이는 것을 제어 */
bool zswap_writeback;
#endif /* CONFIG_ZSWAP */
/* vmpressure 알림 */
struct vmpressure vmpressure;
/* OOM killer가 한 태스크를 죽일 때 소속 태스크 전체를 죽일지 여부 */
bool oom_group;
int swappiness;
/* memory.events와 memory.events.local */
struct cgroup_file events_file;
struct cgroup_file events_local_file;
/* memory.swap.events 핸들 */
struct cgroup_file swap_events_file;
/* memory.stat */
struct memcg_vmstats *vmstats;
/* memory.events */
atomic_long_t memory_events[MEMCG_NR_MEMORY_EVENTS];
atomic_long_t memory_events_local[MEMCG_NR_MEMORY_EVENTS];
#ifdef CONFIG_MEMCG_NMI_SAFETY_REQUIRES_ATOMIC
/* NMI 컨텍스트의 MEMCG_KMEM */
atomic_t kmem_stat;
#endif /* CONFIG_MEMCG_NMI_SAFETY_REQUIRES_ATOMIC */
/*
* 소켓 메모리 관리를 위한 회수 압력 힌트. 소켓 메모리를 별도
* 계정/충전하는 레거시 cgroup 모드에서는 사용하면 안 된다.
*/
u64 socket_pressure;
#if BITS_PER_LONG < 64
seqlock_t socket_pressure_seqlock;
#endif
int kmemcg_id;
/*
* objcg reparenting 과정에서 memcg->objcg는 지워진다.
* memcg->orig_objcg는 memcg 생애가 끝날 때까지 원래 objcg 포인터와
* 참조를 보존한다.
*/
struct obj_cgroup __rcu *objcg;
struct obj_cgroup *orig_objcg;
/* objcg_lock으로 보호되는 상속 objcg 목록 */
struct list_head objcg_list;
struct memcg_vmstats_percpu __percpu *vmstats_percpu;
#ifdef CONFIG_CGROUP_WRITEBACK
struct list_head cgwb_list;
struct wb_domain cgwb_domain;
struct memcg_cgwb_frn cgwb_frn[MEMCG_CGWB_FRN_CNT];
#endif /* CONFIG_CGROUP_WRITEBACK */
#ifdef CONFIG_TRANSPARENT_HUGEPAGE
struct deferred_split deferred_split_queue;
#endif /* CONFIG_TRANSPARENT_HUGEPAGE */
#ifdef CONFIG_LRU_GEN_WALKS_MMU
/* memcg별 mm_struct 목록 */
struct lru_gen_mm_list mm_list;
#endif /* CONFIG_LRU_GEN_WALKS_MMU */
#ifdef CONFIG_MEMCG_V1
/* 레거시 소비자 지향 카운터 */
struct page_counter kmem; /* v1 전용 */
struct page_counter tcpmem; /* v1 전용 */
struct memcg1_events_percpu __percpu *events_percpu;
unsigned long soft_limit;
/* memcg_oom_lock으로 보호 */
bool oom_lock;
int under_oom;
/* OOM-Killer 비활성화 */
int oom_kill_disable;
/* 임계값 배열 보호 */
struct mutex thresholds_lock;
/* 메모리 사용량 임계값. RCU로 보호 */
struct mem_cgroup_thresholds thresholds;
/* 메모리+스왑 사용량 임계값. RCU로 보호 */
struct mem_cgroup_thresholds memsw_thresholds;
/* oom notifier event fd용 */
struct list_head oom_notify;
/* 레거시 TCP 메모리 계정 */
bool tcpmem_active;
int tcpmem_pressure;
/* 사용자 공간이 수신하려는 이벤트 목록 */
struct list_head event_list;
spinlock_t event_list_lock;
#endif /* CONFIG_MEMCG_V1 */
struct mem_cgroup_per_node *nodeinfo[];
};
핵심 필드 설명:
| 필드 | 역할 |
|---|---|
| `css` | cgroup 서브시스템 상태 — 부모/자식 관계, 온라인 상태 관리 |
| `memory` | `page_counter` 기반 페이지 계정 — `memory.max`/`memory.high`의 내부 구현 |
| `swap` / `memsw` | 스왑 계정 — v2는 `swap` 독립, v1은 `memsw` 합산 |
| `vmstats` / `vmstats_percpu` | 메모리 통계 — `memory.stat` 출력의 원천 |
| `memory_events` | OOM, high 초과 등의 이벤트 카운터 — `memory.events` 출력 |
| `oom_group` | `memory.oom.group=1` 설정 시 memcg 내 모든 프로세스를 한 번에 OOM 킬 |
| `swappiness` | memcg별 스왑 경향 — 기본값 60, 0~200 범위 |
| `nodeinfo[]` | per-NUMA-node 정보 — LRU 벡터, 회수 이터레이터 포함 |
memcg의 memory, swap, v1 memsw는 모두 page_counter로 구현됩니다. usage는 현재 사용량, min/low/high/max는 cgroup v2 메모리 파일의 내부 값입니다.
// include/linux/page_counter.h:10-44
struct page_counter {
/*
* v2에서 'usage'가 다른 필드와 캐시라인을 공유하지 않게 한다.
* memcg->memory.usage는 struct mem_cgroup에서 자주 갱신되는 필드다.
*/
atomic_long_t usage;
unsigned long failcnt; /* v1 전용 필드 */
CACHELINE_PADDING(_pad1_);
/* 유효 memory.min과 memory.min 사용량 추적 */
unsigned long emin;
atomic_long_t min_usage;
atomic_long_t children_min_usage;
/* 유효 memory.low와 memory.low 사용량 추적 */
unsigned long elow;
atomic_long_t low_usage;
atomic_long_t children_low_usage;
unsigned long watermark;
/* 최근 cgroup v2 재설정 watermark */
unsigned long local_watermark;
/* 읽기가 많은 필드를 별도 캐시라인에 둔다. */
CACHELINE_PADDING(_pad2_);
bool protection_support;
bool track_failcnt;
unsigned long min;
unsigned long low;
unsigned long high;
unsigned long max;
struct page_counter *parent;
} ____cacheline_internodealigned_in_smp;
NUMA 노드별 memcg 정보를 담당합니다.
// include/linux/memcontrol.h:88-123
struct mem_cgroup_per_node {
/* 읽기 전용에 가까운 필드는 앞쪽에 둔다. */
struct mem_cgroup *memcg; /* 역참조, container_of를 쓸 수 없음 */
struct lruvec_stats_percpu __percpu *lruvec_stats_percpu;
struct lruvec_stats *lruvec_stats;
struct shrinker_info __rcu *shrinker_info;
#ifdef CONFIG_MEMCG_V1
/*
* v1 전용 필드를 중간 버퍼처럼 배치해 읽기 위주 필드와 자주 갱신되는
* 필드의 캐시라인 공유 경합을 줄인다.
*/
struct rb_node tree_node; /* RB tree 노드 */
unsigned long usage_in_excess;/* 소프트 한계 초과량 */
bool on_tree;
#else
CACHELINE_PADDING(_pad1_);
#endif
/* 자주 갱신되는 필드는 끝쪽에 둔다. */
struct lruvec lruvec;
CACHELINE_PADDING(_pad2_);
unsigned long lru_zone_size[MAX_NR_ZONES][NR_LRU_LISTS];
struct mem_cgroup_reclaim_iter iter;
#ifdef CONFIG_MEMCG_NMI_SAFETY_REQUIRES_ATOMIC
/* NMI 컨텍스트용 slab 통계 */
atomic_t slab_reclaimable;
atomic_t slab_unreclaimable;
#endif
};
커널 메모리(slab) 계정을 위한 오브젝트 컨트롤러. memcg에서 slab 오브젝트를 개별적으로 계정할 때 사용됩니다.
// include/linux/memcontrol.h:174-182
struct obj_cgroup {
struct percpu_ref refcnt;
struct mem_cgroup *memcg;
atomic_t nr_charged_bytes;
union {
struct list_head list; /* objcg_lock으로 보호 */
struct rcu_head rcu;
};
};
메모리 통계를 관리하는 구조체. per-CPU 로컬 카운터와 aggregate 카운터를 분리하여 성능을 최적화합니다.
// mm/memcontrol.c:504-538
struct memcg_vmstats_percpu {
/* 마지막 flush 이후 통계 갱신 횟수 */
unsigned int stats_updates;
/* memcg_rstat_updated() 빠른 순회를 위한 캐시 포인터 */
struct memcg_vmstats_percpu __percpu *parent_pcpu;
struct memcg_vmstats *vmstats;
/* 위 필드는 memcg_rstat_updated()용 단일 캐시라인 안에 들어가야 한다. */
/* 로컬 CPU와 cgroup의 페이지 상태 및 이벤트 */
long state[MEMCG_VMSTAT_SIZE];
unsigned long events[NR_MEMCG_EVENTS];
/* lockless 상향 전파를 위한 delta 계산 */
long state_prev[MEMCG_VMSTAT_SIZE];
unsigned long events_prev[NR_MEMCG_EVENTS];
} ____cacheline_aligned;
struct memcg_vmstats {
/* 집계된 CPU와 하위 트리 페이지 상태 및 이벤트 */
long state[MEMCG_VMSTAT_SIZE];
unsigned long events[NR_MEMCG_EVENTS];
/* 계층화하지 않은 CPU 집계 페이지 상태 및 이벤트 */
long state_local[MEMCG_VMSTAT_SIZE];
unsigned long events_local[NR_MEMCG_EVENTS];
/* tree 전파 중인 자식 카운트 */
long state_pending[MEMCG_VMSTAT_SIZE];
unsigned long events_pending[NR_MEMCG_EVENTS];
/* 마지막 flush 이후 통계 갱신 횟수 */
atomic_t stats_updates;
};
// include/linux/memcontrol.h:34-57
/* 범용 노드 페이지 상태 위에 얹는 cgroup별 페이지 상태 */
enum memcg_stat_item {
MEMCG_SWAP = NR_VM_NODE_STAT_ITEMS,
MEMCG_SOCK,
MEMCG_PERCPU_B,
MEMCG_VMALLOC,
MEMCG_KMEM,
MEMCG_ZSWAP_B,
MEMCG_ZSWAPPED,
MEMCG_NR_STAT,
};
enum memcg_memory_event {
MEMCG_LOW,
MEMCG_HIGH,
MEMCG_MAX,
MEMCG_OOM,
MEMCG_OOM_KILL,
MEMCG_OOM_GROUP_KILL,
MEMCG_SWAP_HIGH,
MEMCG_SWAP_MAX,
MEMCG_SWAP_FAIL,
MEMCG_SOCK_THROTTLED,
MEMCG_NR_MEMORY_EVENTS,
};
// mm/memcontrol.c:3822-3873
static struct cgroup_subsys_state * __ref
mem_cgroup_css_alloc(struct cgroup_subsys_state *parent_css)
{
struct mem_cgroup *parent = mem_cgroup_from_css(parent_css);
struct mem_cgroup *memcg, *old_memcg;
bool memcg_on_dfl = cgroup_subsys_on_dfl(memory_cgrp_subsys);
old_memcg = set_active_memcg(parent);
memcg = mem_cgroup_alloc(parent);
set_active_memcg(old_memcg);
if (IS_ERR(memcg))
return ERR_CAST(memcg);
page_counter_set_high(&memcg->memory, PAGE_COUNTER_MAX);
memcg1_soft_limit_reset(memcg);
#ifdef CONFIG_ZSWAP
memcg->zswap_max = PAGE_COUNTER_MAX;
WRITE_ONCE(memcg->zswap_writeback, true);
#endif
page_counter_set_high(&memcg->swap, PAGE_COUNTER_MAX);
if (parent) {
WRITE_ONCE(memcg->swappiness, mem_cgroup_swappiness(parent));
page_counter_init(&memcg->memory, &parent->memory, memcg_on_dfl);
page_counter_init(&memcg->swap, &parent->swap, false);
#ifdef CONFIG_MEMCG_V1
memcg->memory.track_failcnt = !memcg_on_dfl;
WRITE_ONCE(memcg->oom_kill_disable, READ_ONCE(parent->oom_kill_disable));
page_counter_init(&memcg->kmem, &parent->kmem, false);
page_counter_init(&memcg->tcpmem, &parent->tcpmem, false);
#endif
} else {
init_memcg_stats();
init_memcg_events();
page_counter_init(&memcg->memory, NULL, true);
page_counter_init(&memcg->swap, NULL, false);
#ifdef CONFIG_MEMCG_V1
page_counter_init(&memcg->kmem, NULL, false);
page_counter_init(&memcg->tcpmem, NULL, false);
#endif
root_mem_cgroup = memcg;
return &memcg->css;
}
if (memcg_on_dfl && !cgroup_memory_nosocket)
static_branch_inc(&memcg_sockets_enabled_key);
if (!cgroup_memory_nobpf)
static_branch_inc(&memcg_bpf_enabled_key);
return &memcg->css;
}
역할: 새 memcg를 생성하고 초기화합니다. page_counter를 부모와 연결하고, 통계 구조체를 할당합니다.
분기 로직:
parent == NULL → 루트 memcg 생성 (통계 초기화, root_mem_cgroup 설정)parent != NULL → 자식 memcg 생성 (부모의 page_counter에 연결, swappiness 상속)cgroup_subsys_on_dfl(memory_cgrp_subsys) → v2 모드에서 소켓/BPF 키 활성화// mm/memcontrol.c:2355-2548
static int try_charge_memcg(struct mem_cgroup *memcg, gfp_t gfp_mask,
unsigned int nr_pages)
역할: folio 할당 시 memcg에 페이지를 계정(charge)합니다. 한계 초과 시 회수, OOM, force charge 등의 분기 처리를 수행합니다.
분기 로직:
1. consume_stock() → per-CPU stock에서 즉시 계정 성공 시 리턴
2. page_counter_try_charge(&memcg->memory) → 한계 내 계정 성공 → done_restock
3. 한계 초과 시:
- PF_MEMALLOC 플래그 → 즉시 force charge (goto force)
- task_in_memcg_oom() → OOM 중이면 nomem
- gfpflags_allow_blocking() 불가 → nomem
- try_to_free_mem_cgroup_pages() → 직접 회수 시도
- 회수 후 margin 확보 가능하면 retry
- mem_cgroup_oom() → memcg OOM killer 발동
4. done_restock: stock 잔여 분량 충전 + memory.high 초과 시 __mem_cgroup_handle_over_high() 예약
// mm/memcontrol.c:2265-2353
void __mem_cgroup_handle_over_high(gfp_t gfp_mask)
역할: memory.high 초과 시 할당자를 throttle합니다. 직접 회수 + schedule_timeout_killable로 지연을 적용합니다.
분기 로직:
1. task_is_dying() → 종료 중이면 즉시 리턴
2. reclaim_high() → 회수 시도 (첫 번째는 nr_pages, 이후 SWAP_CLUSTER_MAX)
3. 회수 성공 or 재시도 가능 → retry_reclaim
4. calculate_high_delay() → 초과량에 비례한 지연 시간 계산
5. penalty_jiffies <= HZ / 100 → 지연이 너무 적으면 스킵
6. schedule_timeout_killable() → 유저 모드 복귀 시 실제 throttle
// mm/memcontrol.c:4739-4765
static int charge_memcg(struct folio *folio, struct mem_cgroup *memcg,
gfp_t gfp)
{
int ret;
ret = try_charge(memcg, gfp, folio_nr_pages(folio));
if (ret)
goto out;
css_get(&memcg->css);
commit_charge(folio, memcg);
memcg1_commit_charge(folio, memcg);
out:
return ret;
}
int __mem_cgroup_charge(struct folio *folio, struct mm_struct *mm, gfp_t gfp)
{
struct mem_cgroup *memcg;
int ret;
memcg = get_mem_cgroup_from_mm(mm);
ret = charge_memcg(folio, memcg, gfp);
css_put(&memcg->css);
return ret;
}
역할: folio를 memcg에 계정하는 상위 API. __mem_cgroup_charge는 mm에서 memcg를 찾아 charge_memcg를 호출합니다.
흐름:
__mem_cgroup_charge()
└─ get_mem_cgroup_from_mm(mm) // mm의 memcg 획득
└─ charge_memcg(folio, memcg, gfp)
├─ try_charge(memcg, gfp, nr_pages) // 계정 시도
├─ css_get(&memcg->css) // 참조 카운트 증가
├─ commit_charge(folio, memcg) // folio->memcg_data 설정
└─ memcg1_commit_charge(folio, memcg) // v1 전용 후처리
└─ css_put(&memcg->css)
// mm/memcontrol.c:3919-3939
static void mem_cgroup_css_offline(struct cgroup_subsys_state *css)
{
struct mem_cgroup *memcg = mem_cgroup_from_css(css);
memcg1_css_offline(memcg);
page_counter_set_min(&memcg->memory, 0);
page_counter_set_low(&memcg->memory, 0);
zswap_memcg_offline_cleanup(memcg);
memcg_offline_kmem(memcg);
reparent_deferred_split_queue(memcg);
reparent_shrinker_deferred(memcg);
wb_memcg_offline(memcg);
lru_gen_offline_memcg(memcg);
drain_all_stock(memcg);
mem_cgroup_private_id_put(memcg);
}
역할: memcg가 오프라인될 때 리소스를 정리합니다. 부모에게 리소스를 상속하고, 관련 워커를 드레인합니다.
흐름:
mem_cgroup_css_offline()
├─ memcg1_css_offline(memcg) // v1 레거시 정리
├─ page_counter_set_min/low(&memcg->memory, 0) // 보호 제거
├─ zswap_memcg_offline_cleanup(memcg) // zswap 정리
├─ memcg_offline_kmem(memcg) // 커널 메모리 정리
├─ reparent_deferred_split_queue(memcg) // THP 분할 큐 상속
├─ reparent_shrinker_deferred(memcg) // shrinker 상속
├─ wb_memcg_offline(memcg) // 쓰기 되돌림 정리
├─ lru_gen_offline_memcg(memcg) // MGLRU 정리
├─ drain_all_stock(memcg) // per-CPU stock 드레인
└─ mem_cgroup_private_id_put(memcg) // ID 해제
// mm/memcontrol.c:4624-4696
static struct cftype memory_files[] = {
{
.name = "current",
.flags = CFTYPE_NOT_ON_ROOT,
.read_u64 = memory_current_read,
},
{
.name = "peak",
.flags = CFTYPE_NOT_ON_ROOT,
.open = peak_open,
.release = peak_release,
.seq_show = memory_peak_show,
.write = memory_peak_write,
},
{
.name = "min",
.flags = CFTYPE_NOT_ON_ROOT,
.seq_show = memory_min_show,
.write = memory_min_write,
},
{
.name = "low",
.flags = CFTYPE_NOT_ON_ROOT,
.seq_show = memory_low_show,
.write = memory_low_write,
},
{
.name = "high",
.flags = CFTYPE_NOT_ON_ROOT,
.seq_show = memory_high_show,
.write = memory_high_write,
},
{
.name = "max",
.flags = CFTYPE_NOT_ON_ROOT,
.seq_show = memory_max_show,
.write = memory_max_write,
},
{
.name = "events",
.flags = CFTYPE_NOT_ON_ROOT,
.file_offset = offsetof(struct mem_cgroup, events_file),
.seq_show = memory_events_show,
},
{
.name = "events.local",
.flags = CFTYPE_NOT_ON_ROOT,
.file_offset = offsetof(struct mem_cgroup, events_local_file),
.seq_show = memory_events_local_show,
},
{
.name = "stat",
.seq_show = memory_stat_show,
},
#ifdef CONFIG_NUMA
{
.name = "numa_stat",
.seq_show = memory_numa_stat_show,
},
#endif
{
.name = "oom.group",
.flags = CFTYPE_NOT_ON_ROOT | CFTYPE_NS_DELEGATABLE,
.seq_show = memory_oom_group_show,
.write = memory_oom_group_write,
},
{
.name = "reclaim",
.flags = CFTYPE_NS_DELEGATABLE,
.write = memory_reclaim,
},
{ } /* 종료 */
};
// mm/memcontrol.c:4698-4714
struct cgroup_subsys memory_cgrp_subsys = {
.css_alloc = mem_cgroup_css_alloc,
.css_online = mem_cgroup_css_online,
.css_offline = mem_cgroup_css_offline,
.css_released = mem_cgroup_css_released,
.css_free = mem_cgroup_css_free,
.css_reset = mem_cgroup_css_reset,
.css_rstat_flush = mem_cgroup_css_rstat_flush,
.attach = mem_cgroup_attach,
.fork = mem_cgroup_fork,
.exit = mem_cgroup_exit,
.dfl_cftypes = memory_files,
#ifdef CONFIG_MEMCG_V1
.legacy_cftypes = mem_cgroup_legacy_files,
#endif
.early_init = 0,
};
역할: memory.current, memory.min, memory.low, memory.high, memory.max, memory.events, memory.numa_stat, memory.oom.group, memory.reclaim 같은 cgroup v2 파일을 memory controller에 연결합니다. 이 배열은 memory_cgrp_subsys.dfl_cftypes로 등록되어 루트가 아닌 cgroup에서 제어 파일로 보입니다.
파일 캐시: filemap_add_folio() [mm/filemap.c:949-969]
└─ mem_cgroup_charge(folio, NULL, gfp)
└─ __mem_cgroup_charge(folio, mm, gfp)
익명 fault: alloc_anon_folio() [mm/memory.c:5179-5209]
└─ vma_alloc_folio(gfp, order, vma, addr)
└─ mem_cgroup_charge(folio, vma->vm_mm, gfp)
└─ __mem_cgroup_charge(folio, mm, gfp)
공통 계정: __mem_cgroup_charge() [mm/memcontrol.c:4755-4765]
├─ get_mem_cgroup_from_mm(mm)
│ └─ mem_cgroup_from_task(mm->owner)
└─ charge_memcg(folio, memcg, gfp)
├─ try_charge(memcg, gfp, folio_nr_pages(folio))
│ └─ try_charge_memcg(memcg, gfp_mask, nr_pages)
│ ├─ consume_stock() [빠른 경로]
│ ├─ page_counter_try_charge(&memcg->memory) [계정 시도]
│ ├─ try_to_free_mem_cgroup_pages() [직접 회수]
│ ├─ mem_cgroup_oom() [memcg OOM]
│ └─ done_restock [memory.high / memory.swap.high 확인]
├─ css_get(&memcg->css)
├─ commit_charge(folio, memcg)
│ └─ folio->memcg_data = (unsigned long)memcg
└─ memcg1_commit_charge(folio, memcg)
try_charge_memcg() → mem_cgroup_oom()
├─ memcg_memory_event(memcg, MEMCG_OOM)
├─ memcg1_oom_prepare(memcg, &locked)
└─ mem_cgroup_out_of_memory(memcg, mask, order)
├─ mutex_lock_killable(&oom_lock)
├─ mem_cgroup_margin(memcg) 재확인
└─ out_of_memory(&oc)
└─ oom_kill_process()
└─ mem_cgroup_get_oom_group(victim, oom_domain)
folio_memcg_free()
└─ __mem_cgroup_uncharge(folio)
└─ uncharge_folio(folio, &ug)
├─ folio_memcg_kmem() → objcg 경로
├─ __folio_memcg() → 일반 메모리 경로
└─ uncharge_batch(ug)
└─ memcg_uncharge(memcg, nr_pages)
└─ page_counter_uncharge(&memcg->memory, nr_pages)
| 항목 | cgroup v1 | cgroup v2 |
|---|---|---|
| 한계 설정 | `memory.limit_in_bytes` | `memory.max` |
| 소프트 한계 | `memory.soft_limit_in_bytes` | `memory.high` |
| 보호 수준 | 없음 | `memory.min`, `memory.low` |
| 스왑 계정 | `memory.memsw.limit_in_bytes` (합산) | `memory.swap.max` (독립) |
| 커널 메모리 | `memory.kmem.limit_in_bytes` | 기본 계정 (별도 제한 없음) |
| 소켓 메모리 | `memory.kmem.tcp.limit_in_bytes` | 기본 계정 |
| OOM 제어 | `memory.oom_control` | `memory.oom.group` |
| 이벤트 | `memory.oom_control`의 `oom_kill` | `memory.events`의 `oom_kill` |
| 통계 | `memory.stat` (다소 상이) | `memory.stat` (확장) |
| PSI | 없음 | `memory.pressure` |
memory.min <= memory.low <= memory.high <= memory.max 순서로 이해하면 운영 정책을 잡기 쉽습니다. 부모 cgroup의 실제 한계가 최종 상한이므로, 자식 cgroup의 설정 합계가 부모보다 크게 잡힐 수는 있어도 실제 사용량은 부모의 계정과 제한을 함께 통과해야 합니다.
| 파일 | 내부 필드 | 의미 | 초과 또는 부족 시 동작 |
|---|---|---|---|
| `memory.min` | `memcg->memory.min` | 절대 보호량 | 보호 범위 아래의 메모리는 원칙적으로 회수하지 않음 |
| `memory.low` | `memcg->memory.low` | 노력 기반 보호량 | 전역 압박이 강하면 회수될 수 있지만 우선순위가 낮음 |
| `memory.high` | `memcg->memory.high` | throttle 경고선 | 초과 시 회수 유도, 유저 모드 복귀 경로에서 지연 적용 |
| `memory.max` | `memcg->memory.max` | 하드 제한 | 회수 실패 시 memcg OOM 경로로 진입 |
| `memory.current` | `memcg->memory.usage` | 현재 사용량 | `page_counter_read(&memcg->memory)` 기반 출력 |
| `memory.events` | `memory_events[]` | low/high/max/OOM 이벤트 | 이벤트 파일 알림과 운영 모니터링에 사용 |
| cgroup | 대표 설정 | 해석 |
|---|---|---|
| `system.slice` | `memory.max=4G`, `memory.high=3.5G`, `memory.low=1G` | 시스템 서비스가 최소 1G 보호를 받되 3.5G부터 속도 제한을 받음 |
| `app.slice` | `memory.max=8G`, `memory.high=7G`, `memory.swap.max=2G` | 애플리케이션 RSS와 page cache를 제한하고 swap 사용량을 별도 관리 |
| `guest.slice` | `memory.max=16G`, `memory.oom.group=1` | VM 또는 컨테이너 묶음을 하나의 OOM 도메인으로 취급 |
| 경로 | 트리거 | 동작 |
|---|---|---|
| 직접 회수 (direct reclaim) | `try_charge_memcg()`에서 한계 초과 | `try_to_free_mem_cgroup_pages()` 호출 |
| 유저 모드 복귀 시 throttle | `memory.high` 초과, `done_restock`에서 감지 | `__mem_cgroup_handle_over_high()` → `schedule_timeout_killable()` |
| periodic flush | 2초마다 deferred work | `flush_memcg_stats_dwork()` → 통계 동기화 |
| per-CPU stock drain | memcg 오프라인 시 | `drain_all_stock()` → 모든 CPU의 stock 비우기 |
| 조건 | memory.max 초과 시 동작 |
|---|---|
| 일반 할당 (`GFP_KERNEL`) | 회수 시도 → 실패 시 memcg OOM killer 발동 |
| `__GFP_NOFAIL` 할당 | 한계를 일시적으로 초과하여 force charge |
| `__GFP_HIGH` 할당 | force charge 허용 (커널 내부 긴급 할당) |
| `PF_MEMALLOC` 플래그 | 즉시 force charge (시스템 전체 메모리 관리 중) |
| `oom_kill_disable=1` (v1) | OOM killer 비활성화, 회수만 시도 |
penalty_jiffies = calculate_high_delay(memcg, nr_pages, max_overage)
+ calculate_high_delay(memcg, nr_pages, swap_max_overage)
| 초과 비율 | 예상 지연 | 효과 |
|---|---|---|
| 약간 초과 (<= HZ/100) | 0 (스킵) | 프로세스에 영향 없음 |
| 중간 초과 | 수백 ms | 점진적 속도 저하 |
| 크게 초과 | 최대 `MEMCG_MAX_HIGH_DELAY_JIFFIES` | 강한 throttle |
커널은 memcg 통계를 효율적으로 동기화하기 위해 2-tier flush 구조를 사용합니다:
1. Per-CPU 로컬 업데이트: memcg_vmstats_percpu에 즉시 기록 (lockless)
2. Periodic flush: 2초마다 flush_memcg_stats_dwork()가 aggregate로 병합
3. Reader-side flush: MEMCG_CHARGE_BATCH * nr_cpus 이상 업데이트 시 동기 flush
Per-CPU state 업데이트
└─ memcg_rstat_updated()
└─ stats_updates >= MEMCG_CHARGE_BATCH → vmstats->stats_updates에 atomic_add
Reader-side flush
└─ memcg_vmstats_needs_flush(vmstats) → true 시 css_rstat_flush()