Ryotta's Linux 7.0 MM

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

OOM Killer (Out-of-Memory Killer)

개요 (Overview)

OOM Killer는 Linux 커널이 물리 메모리를 모두 소진하고 회수 가능한 페이지도 남아 있지 않을 때, 가장 적합한 프로세스를 골라 SIGKILL로 종료하여 시스템 전체가 멈추는 것을 방지하는 최후의 수단입니다. __alloc_pages()에서 메모리 할당이 실패하고 모든 reclaim 경로가 소진되면 out_of_memory()가 호출되며, 이 함수가 OOM Killer의 진입점입니다. OOM Killer는 단순히 "가장 큰 프로세스"를 죽이는 것이 아니라, oom_badness() 휴리스틱을 통해 RSS + swap + page table 크기와 oom_score_adj 가중치를 결합하여 점수를 산출하고, 가장 높은 점수를 가진 프로세스를 희생자(victim)로 선택합니다.

OOM Killer는 글로벌 OOM과 memcg(메모리 cgroup) OOM 두 가지 경로로 동작합니다. 글로벌 OOM은 시스템 전체 메모리가 부족할 때 발동하고, memcg OOM은 특정 컨테이너나 cgroup의 메모리 한도(memory.max)가 초과될 때 해당 cgroup 내부에서만 동작합니다. 또한 oom_reaper 커널 스레드가 희생자의 주소 공간을 즉시 회수(reaping)하여 메모리 확보 시간을 단축하는 역할을 합니다.

소스 파일:

mm/oom_kill.c                    ← OOM Killer 핵심 로직 (1273줄)
mm/page_alloc.c                  ← 할당 slowpath에서 OOM 진입 조건
mm/memcontrol.c                  ← memory.oom.group, memcg OOM 그룹 처리
include/linux/oom.h              ← oom_control, oom_constraint, API 선언
include/uapi/linux/oom.h         ← OOM_SCORE_ADJ_MIN/MAX 상수
include/linux/mm_types.h         ← struct mm_struct, struct page 정의
fs/proc/base.c                   ← /proc/<pid>/oom_score 표시값

일상 비유로 보면 OOM Killer는 건물의 비상 차단기와 같습니다. 먼저 불필요한 전등을 끄고(페이지 회수), 창고로 옮기고(swap/zswap), 공간을 다시 배치한 뒤(compaction)에도 전력이 부족하면 건물 전체 정전을 막기 위해 가장 영향이 작거나 전력을 많이 쓰는 구역 하나를 강제로 차단합니다. 이때 oom_score_adj는 병원 응급실이나 관제실처럼 절대 꺼지면 안 되는 구역을 보호하거나, 임시 작업처럼 먼저 종료해도 되는 구역을 지정하는 우선순위 노브입니다.

OOM Killer 호출 흐름

OOM Killer 호출 흐름

핵심 자료구조 관계도

핵심 자료구조 관계도

빠른 점검 명령

# 1. 시스템 전체 메모리 현황
cat /proc/meminfo | grep -E 'MemTotal|MemFree|MemAvailable|SwapTotal|SwapFree'

# 2. PSI 메모리 압력 확인
cat /proc/pressure/memory

# 3. OOM Killer 관련 sysctl 설정 확인
sysctl vm.panic_on_oom vm.oom_kill_allocating_task vm.oom_dump_tasks

# 4. 현재 프로세스의 oom_score와 oom_score_adj 확인
cat /proc/self/oom_score /proc/self/oom_adj /proc/self/oom_score_adj

# 5. 시스템 전체 프로세스의 OOM 점수 정렬
for pid in /proc/[0-9]*/oom_score; do echo "$(cat $pid 2>/dev/null) $(dirname $pid | xargs basename) $(cat $(dirname $pid)/comm 2>/dev/null)"; done 2>/dev/null | sort -rn | head -10

# 6. 중요 서비스 보호 설정값 확인
pid=$(pidof sshd 2>/dev/null | awk '{print $1}'); [ -n "$pid" ] && cat /proc/$pid/oom_score_adj

# 7. 중요 서비스 OOM 보호 설정 예시
pid=$(pidof sshd 2>/dev/null | awk '{print $1}'); [ -n "$pid" ] && echo -1000 | sudo tee /proc/$pid/oom_score_adj

# 8. dmesg에서 OOM Killer 발동 이력 확인
dmesg | grep -E 'oom-kill|Out of memory|Killed process'

# 9. cgroup v2 OOM 이벤트 확인
grep -H 'oom' /sys/fs/cgroup/memory.events /sys/fs/cgroup/*/memory.events 2>/dev/null

# 10. cgroup v2 그룹 kill 설정 확인
find /sys/fs/cgroup -name memory.oom.group -exec sh -c 'for f; do printf "%s " "$f"; cat "$f"; done' sh {} + 2>/dev/null | head

# 11. 회수/compaction/OOM 관련 vmstat 카운터 확인
cat /proc/vmstat | grep -E 'allocstall|pgscan|pgsteal|compact|oom_kill'

핵심 자료구조

struct oom_control

OOM Killer의 전체 동작 상태를 담는 제어 구조체입니다.

소스: include/linux/oom.h:24-54

/*
 * OOM killer를 발생시킨 페이지 할당의 세부 정보이며,
 * 무엇을 kill할지 결정하는 데 사용된다.
 */
struct oom_control {
    /* cpuset 판단에 사용 */
    struct zonelist *zonelist;

    /* mempolicy 판단에 사용 */
    nodemask_t *nodemask;

    /* OOM이 호출된 memory cgroup이며, global OOM이면 NULL */
    struct mem_cgroup *memcg;

    /* cpuset과 node locality 요구사항 판단에 사용 */
    const gfp_t gfp_mask;

    /*
     * order == -1은 sysrq에 의해 OOM kill이 필요하다는 뜻이고,
     * 그 밖의 값은 표시 목적으로만 사용된다.
     */
    const int order;

    /* OOM 구현에서 사용하며, 호출자가 설정하지 않는다 */
    unsigned long totalpages;
    struct task_struct *chosen;
    long chosen_points;

    /* constraint 정보를 출력하는 데 사용 */
    enum oom_constraint constraint;
};

enum oom_constraint

할당 실패의 원인이 제약 조건에 의한 것인지 판단합니다.

소스: include/linux/oom.h:17-22

enum oom_constraint {
    CONSTRAINT_NONE,
    CONSTRAINT_CPUSET,
    CONSTRAINT_MEMORY_POLICY,
    CONSTRAINT_MEMCG,
};

CONSTRAINT_NONE은 일반적인 시스템 전체 메모리 부족, CONSTRAINT_CPUSET은 cpuset이 허용한 노드 범위 제한, CONSTRAINT_MEMORY_POLICY는 NUMA mempolicy 제약, CONSTRAINT_MEMCG는 memory cgroup 한도 초과를 나타냅니다.

oom_badness() 점수 계산 공식

OOM Killer가 각 프로세스에 대해 산출하는 점수입니다.

소스: mm/oom_kill.c:193-240

/**
 * oom_badness - 어떤 후보 task를 kill할지 결정하는 휴리스틱 함수
 * @p: 계산할 task 구조체
 * @totalpages: 페이지 할당에 허용된 전체 present RAM
 *
 * kill 대상 task를 판단하는 휴리스틱은 가능한 단순하고 예측 가능해야 한다.
 * 목표는 가장 많은 메모리를 소비하는 task에 가장 큰 값을 반환하여
 * 연쇄 OOM 실패를 피하는 것이다.
 */
long oom_badness(struct task_struct *p, unsigned long totalpages)
{
    long points;
    long adj;

    if (oom_unkillable_task(p))
        return LONG_MIN;

    p = find_lock_task_mm(p);
    if (!p)
        return LONG_MIN;

    /*
     * 명시적으로 OOM kill 불가 표시가 있거나 이미 OOM reaping되었거나
     * vfork 도중인 task는 후보로 고려하지 않는다.
     */
    adj = (long)p->signal->oom_score_adj;
    if (adj == OOM_SCORE_ADJ_MIN ||
            mm_flags_test(MMF_OOM_SKIP, p->mm) ||
            in_vfork(p)) {
        task_unlock(p);
        return LONG_MIN;
    }

    /*
     * badness 점수의 기준값은 각 task의 RSS, page table,
     * swap 공간 사용량이 RAM에서 차지하는 비율이다.
     */
    points = get_mm_rss_sum(p->mm) + get_mm_counter_sum(p->mm, MM_SWAPENTS) +
        mm_pgtables_bytes(p->mm) / PAGE_SIZE;
    task_unlock(p);

    /* oom_score_adj 단위로 정규화 */
    adj *= totalpages / 1000;
    points += adj;

    return points;
}

점수 계산 요약:

  • 기본값: (RSS_anon + RSS_file + RSS_shmem) + SwapUsed + PageTablePages
  • oom_score_adj가 양수(예: 1000)이면 점수가 크게 증가 → 먼저 죽음
  • oom_score_adj가 음수(예: -500)이면 점수가 감소 → 보호됨
  • OOM_SCORE_ADJ_MIN(-1000)은 사실상 OOM에서 완전 보호
  • oom_badness()는 커널 내부에서 후보를 비교하기 위한 raw 페이지 점수를 반환합니다. 사용자 공간의 /proc//oom_scorefs/proc/base.c:585-601에서 이 값을 totalram_pages() + total_swap_pages 기준으로 다시 스케일링해 표시하므로, 운영 중에는 oom_scoreoom_score_adj를 함께 봐야 실제 kill 우선순위를 해석할 수 있습니다.


    핵심 함수

    1. `out_of_memory()` — OOM Killer 진입점

    // mm/oom_kill.c:1119-1188
    bool out_of_memory(struct oom_control *oc)

    역할: 페이지 할당자가 메모리를 확보하지 못했을 때 호출되는 메인 함수.

    분기 로직:

    1. oom_killer_disabled → true면 즉시 반환 (cgroup freeze 등)

    2. 글로벌 OOM이면 notifier 체인 호출 → 메모리 회수 가능하면 반환

    3. task_will_free_mem(current) → 현재 프로세스가 이미 종료 중이면 자신을 희생자로 지정

    4. __GFP_FS가 없는 할당 실패이고 memcg OOM이 아니면 true 반환 (NOFS/NOIO 경로는 OOM kill로 해결하기 어렵기 때문)

    5. constrained_alloc()으로 제약 조건 판단 → check_panic_on_oom() 호출

    6. oom_kill_allocating_task sysctl이 켜져 있으면 현재 프로세스를 직접 희생자로 선택

    7. select_bad_process()로 최적 희생자 선택

    8. 희생자가 없으면 panic("System is deadlocked on memory")

    9. 희생자가 있으면 oom_kill_process() 호출

    2. `oom_kill_process()` — 희생자 프로세스 처리

    // mm/oom_kill.c:1024-1070
    static void oom_kill_process(struct oom_control *oc, const char *message)

    역할: 선택된 희생자를 실제로 종료시키는 함수.

    분기 로직:

    1. task_will_free_mem(victim) → 이미 종료 중이면 mark_oom_victim()만 하고 종료

    2. dump_header()dump_oom_victim()으로 시스템 상태 출력

    3. mem_cgroup_get_oom_group()memory.oom.group=1인 상위 cgroup 확인 → 그룹 전체를 죽여야 하는지 판단

    4. __oom_kill_process()으로 실제로 SIGKILL 전송

    5. OOM 그룹이 있으면 mem_cgroup_scan_tasks()로 그룹 내 모든 프로세스에 SIGKILL

    3. `__oom_kill_process()` — SIGKILL 전송과 reaper 큐잉

    // mm/oom_kill.c:928-1008
    static void __oom_kill_process(struct task_struct *victim, const char *message)

    역할: 희생자와 공유 mm를 가진 모든 프로세스에 SIGKILL을 보내고 oom_reaper를 큐잉합니다.

    분기 로직:

    1. find_lock_task_mm()로 유효한 mm를 가진 스레드 찾음

    2. do_send_sig_info(SIGKILL)로 희생자에 시그널 전송

    3. mark_oom_victim()으로 TIF_MEMDIE 플래그 설정 → 메모리 예약 접근 권한 부여

    4. for_each_process()로 동일 mm를 공유하는 다른 프로세스에도 SIGKILL

    5. is_global_init(p)이면 oom_reaper가 reaping하지 않도록 플래그 설정

    6. queue_oom_reaper()로 2초 타이머 후 reaping 시작

    4. `select_bad_process()` — 최적 희생자 선택

    // mm/oom_kill.c:365-380
    static void select_bad_process(struct oom_control *oc)

    역할: 모든 후보 프로세스를 평가하여 oom_badness 점수가 가장 높은 프로세스를 선택합니다.

    분기 로직:

    1. memcg OOM이면 mem_cgroup_scan_tasks()로 해당 cgroup 내부만 스캔

    2. 글로벌 OOM이면 for_each_process()로 전체 프로세스 스캔

    3. 각 프로세스에 대해 oom_evaluate_task() 호출 → oom_badness 점수 비교

    4. oom_task_origin(task)가 true면 바로 선택 (현재 할당 중인 프로세스가 OOM을 유발)

    5. 이전 희생자(tsk_is_oom_victim)는 건너뜀 → 중복_kill 방지

    5. `oom_reap_task()` — 메모리 즉시 회수

    // mm/oom_kill.c:619-648
    static void oom_reap_task(struct task_struct *tsk)

    역할: 희생자의 주소 공간 중 회수 가능한 페이지를 즉시 unmap하여 메모리를 확보합니다.

    분기 로직:

    1. mmap_read_trylock(mm) 실패 → 재시도 (최대 10회, 100ms 간격)

    2. MMF_OOM_SKIP 플래그가 설정되어 있으면 건너뜀

    3. __oom_reap_task_mm()로 VMA 순회 → 익명 페이지와 비-공유 파일 페이지를 unmap

    4. VM_HUGETLB 또는 VM_PFNMAP VMA는 건너뜀

    5. 최종적으로 MMF_OOM_SKIP 설정 → 향후 OOM에서 이 mm 무시


    호출 흐름

    할당 slowpath는 OOM Killer를 바로 호출하지 않고 direct reclaim과 compaction을 먼저 시도합니다. 두 경로가 모두 실패하고, 재시도 조건도 더 이상 남지 않았을 때 __alloc_pages_may_oom()으로 넘어가며, 여기서 oom_lock을 잡고 마지막 watermark 확인 후 out_of_memory()를 호출합니다.

    소스: mm/page_alloc.c:4844-4855

    /* 직접 회수를 시도한 뒤 할당한다 */
    if (!compact_first) {
        page = __alloc_pages_direct_reclaim(gfp_mask, order, alloc_flags,
                                ac, &did_some_progress);
        if (page)
            goto got_pg;
    }
    
    /* 직접 compaction을 시도한 뒤 할당한다 */
    page = __alloc_pages_direct_compact(gfp_mask, order, alloc_flags, ac,
                    compact_priority, &compact_result);
    if (page)
        goto got_pg;

    소스: mm/page_alloc.c:4928-4937

    /*
     * 불필요한 OOM kill을 피하려고 cpuset/zonelist 갱신 경쟁을 처리한다.
     */
    if (check_retry_cpuset(cpuset_mems_cookie, ac) ||
        check_retry_zonelist(zonelist_iter_cookie))
        goto restart;
    
    /* Reclaim이 실패했으므로 kill을 시작한다 */
    page = __alloc_pages_may_oom(gfp_mask, order, ac, &did_some_progress);
    __alloc_pages() (할당 실패)
      └─ __alloc_pages_slowpath()
           ├─ __alloc_pages_direct_reclaim()
           ├─ __alloc_pages_direct_compact()
           └─ __alloc_pages_may_oom()
                └─ out_of_memory(oc)
                     ├─ blocking_notifier_call_chain()    ← notifier로 회수 시도
                     ├─ task_will_free_mem(current)       ← 이미 종료 중이면 자신을 희생자로
                     ├─ constrained_alloc(oc)             ← 제약 조건 판단 (MEMCG/CPUSET/POLICY)
                     ├─ check_panic_on_oom(oc)            ← panic_on_oom sysctl 확인
                     ├─ select_bad_process(oc)
                     │    └─ oom_evaluate_task(task, oc)
                     │         └─ oom_badness(task, totalpages)  ← 점수 계산
                     ├─ oom_kill_process(oc, message)
                     │    ├─ task_will_free_mem(victim)   ← 이미 종료 중이면 reaping만
                     │    ├─ dump_header(oc)              ← 시스템 상태 출력
                     │    ├─ dump_oom_victim(oc, victim)  ← 희생자 정보 출력
                     │    ├─ __oom_kill_process(victim, message)
                     │    │    ├─ do_send_sig_info(SIGKILL, victim)  ← 시그널 전송
                     │    │    ├─ mark_oom_victim(victim)             ← TIF_MEMDIE 설정
                     │    │    ├─ for_each_process → SIGKILL           ← 공유 mm 프로세스
                     │    │    └─ queue_oom_reaper(victim)            ← reaper 타이머 큐잉
                     │    └─ mem_cgroup_scan_tasks → oom_kill_memcg_member (그룹 kill)
                     └─ return true/false

    조건별 비교

    조건동작 방식확인 방법
    **글로벌 OOM**시스템 전체 메모리 부족. 모든 프로세스 후보.`dmesg \grep "Out of memory"`
    **memcg OOM**특정 컨테이너/cgroup 한도 초과. 해당 cgroup 내부만 스캔.`cat /sys/fs/cgroup/memory.events \grep oom`
    **memory.oom.group = 1**cgroup v2에서 희생자 하나가 아니라 해당 cgroup의 kill 가능한 task 전체를 종료.`cat /sys/fs/cgroup//memory.oom.group`
    **SysRq OOM**`echo f > /proc/sysrq-trigger`로 수동 발동. `order == -1`.`dmesg \grep "SysRq"`
    **Constrained: CPUSET**cpuset이 허용하는 메모리 노드만 사용 가능. 해당 노드 내 프로세스만 후보.`cat /proc/self/cpuset`
    **Constrained: MEMPOLICY**NUMA mempolicy가 특정 노드로 제한. 해당 노드 프로세스만 후보.`numactl --show`
    **panic_on_oom = 0**기본값. OOM Killer가 희생자를 찾아 kill.`sysctl vm.panic_on_oom`
    **panic_on_oom = 1**CONSTRAINT_NONE일 때만 panic. cpuset/mempolicy/memcg 실패는 kill.`sysctl vm.panic_on_oom`
    **panic_on_oom = 2**모든 조건에서 panic.`sysctl vm.panic_on_oom`
    **oom_kill_allocating_task = 1**OOM을 유발한 할당 프로세스 자신을 직접 kill.`sysctl vm.oom_kill_allocating_task`
    **OOM_SCORE_ADJ_MIN(-1000)**oom_badness()가 LONG_MIN 반환 → 사실상 보호.`cat /proc//oom_score_adj`
    **/proc//oom_score**사용자 공간에 보이는 정규화된 OOM 우선순위. 내부 raw 점수와 함께 `oom_score_adj` 영향이 반영됨.`cat /proc//oom_score`
    **PSI memory pressure**OOM 직전의 stall을 조기 감지. `some/full`이 누적되면 reclaim 지연이 커진 상태.`cat /proc/pressure/memory`
    **oom_reaper (MMU 있음)**희생자 SIGKILL 후 2초 뒤 주소 공간을 즉시 reaping.`dmesg \grep "oom_reaper"`
    **OOM victim 재진입 방지**`MMF_OOM_SKIP` 플래그가 설정된 mm는 다음 OOM에서 무시.커널 내부 플래그

    증상별 진단 기준

    증상먼저 볼 지표의미다음 확인
    간헐적 OOM`dmesg`, `/proc/meminfo`, `/proc/vmstat`의 `oom_kill`회수나 swap으로 버티다가 특정 순간에 후보가 kill됨.`oom_score_adj`, RSS 상위 프로세스, `memory.events`
    컨테이너만 종료cgroup v2 `memory.events`, `memory.max`시스템 전체가 아니라 해당 cgroup의 hard limit 초과.`memory.oom.group`, `memory.current`, `memory.high`
    회수 지연 후 OOM`/proc/pressure/memory`, `allocstall`, `pgscan`, `pgsteal`direct reclaim이 길어지고 할당자가 stall을 겪음.페이지 회수 정책, swap/zswap 상태, slab 증가
    고차 페이지 할당 실패`/proc/buddyinfo`, `compact_*` vmstatfree 총량은 있어도 연속 페이지가 부족할 수 있음.THP defrag, compaction, CMA/Movable 영역
    swap I/O 급증 뒤 OOM`vmstat 1`, `SwapFree`, `pgmajfault`working set이 RAM을 넘고 swap도 부족해지는 흐름.swappiness, zswap/zram, 익명 페이지 증가
    특정 서비스가 반복 희생`/proc//oom_score`, `/proc//oom_score_adj`큰 RSS 또는 높은 adj로 우선순위가 높아짐.adj 정책, cgroup 분리, 서비스별 memory.max

    관련 문서

  • 00-overview.html — 메모리 관리 개요
  • 01-page_alloc.html — Buddy Allocator (OOM 호출원)
  • 05-page_reclaim.html — 페이지 회수 (OOM 선행 단계)
  • 07-swap_zswap.html — Swap / zswap (OOM 회수 경로)
  • 11-compaction.html — Compaction (OOM 전 고차 할당 회복 시도)
  • 03-vma_mmap.html — VMA / mmap (OOM reaper 대상)
  • 14-memcontrol.html — Memory Cgroup (memcg OOM과 memory.oom.group)

  • 참고자료

  • mm/oom_kill.c — Linux 7.0 소스
  • mm/page_alloc.c — OOM 진입 전 할당 slowpath
  • mm/memcontrol.c — cgroup v2 OOM 그룹 처리
  • include/linux/oom.h — oom_control 정의
  • fs/proc/base.c/proc//oom_score 표시값
  • minzkn.com 메모리 관리 개요 페이지 — https://www.minzkn.com/linuxkernel/pages/memory.html