Ryotta's Linux 7.0 MM

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

Memory Cgroup (memcg)

개요 (Overview)

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 // 페이지 카운터 기반 계정
Memory Cgroup 호출 흐름
Memory Cgroup 자료구조 관계도

빠른 점검 명령

# 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

핵심 자료구조

struct mem_cgroup

메모리 컨트롤러의 핵심 구조체. 각 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 벡터, 회수 이터레이터 포함

struct page_counter

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;

struct mem_cgroup_per_node

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
};

struct obj_cgroup

커널 메모리(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;
    };
};

struct memcg_vmstats / memcg_vmstats_percpu

메모리 통계를 관리하는 구조체. 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;
};

memcg 이벤트/통계 enum

// 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,
};

핵심 함수

1. mem_cgroup_css_alloc (cgroup 생성)

// 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 키 활성화
  • 2. try_charge_memcg (페이지 계정)

    // 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() 예약

    3. __mem_cgroup_handle_over_high (high 초과 시 throttle)

    // 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

    4. charge_memcg / __mem_cgroup_charge (folio 계정 진입점)

    // 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)

    5. mem_cgroup_css_offline (cgroup 제거)

    // 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 해제

    6. memory_files (cgroup v2 파일 등록)

    // 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에서 제어 파일로 보입니다.


    호출 흐름

    folio 계정 진입 경로

    파일 캐시: 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)

    memcg OOM 경로

    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 해제 시 uncharge 경로

    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)

    조건별 비교

    memcg v1 vs v2 주요 차이

    항목cgroup v1cgroup 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`

    cgroup v2 보호/제한 파일

    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 도메인으로 취급

    memcg 회수 경로 비교

    경로트리거동작
    직접 회수 (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 flush2초마다 deferred work`flush_memcg_stats_dwork()` → 통계 동기화
    per-CPU stock drainmemcg 오프라인 시`drain_all_stock()` → 모든 CPU의 stock 비우기

    OOM 발생 조건

    조건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 비활성화, 회수만 시도

    스케줄러 연동: high throttle 지연 계산

    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 통계 flush 메커니즘

    커널은 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()

    관련 문서

  • 00-overview.html — 메모리 관리 개요
  • 01-page_alloc.html — Buddy Allocator (memcg 계정 대상)
  • 05-page_reclaim.html — 페이지 회수 (memcg별 LRU 스캔)
  • 07-swap_zswap.html — Swap / zswap (memcg 스왑 계정)
  • 08-oom.html — OOM Killer (memcg OOM 포함)
  • 13-numa.html — NUMA (per-node memcg 통계)
  • 15-mempolicy.html — 메모리 정책