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는 병원 응급실이나 관제실처럼 절대 꺼지면 안 되는 구역을 보호하거나, 임시 작업처럼 먼저 종료해도 되는 구역을 지정하는 우선순위 노브입니다.
# 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'
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;
};
할당 실패의 원인이 제약 조건에 의한 것인지 판단합니다.
소스: 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 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 + PageTablePagesoom_score_adj가 양수(예: 1000)이면 점수가 크게 증가 → 먼저 죽음oom_score_adj가 음수(예: -500)이면 점수가 감소 → 보호됨OOM_SCORE_ADJ_MIN(-1000)은 사실상 OOM에서 완전 보호oom_badness()는 커널 내부에서 후보를 비교하기 위한 raw 페이지 점수를 반환합니다. 사용자 공간의 /proc/는 fs/proc/base.c:585-601에서 이 값을 totalram_pages() + total_swap_pages 기준으로 다시 스케일링해 표시하므로, 운영 중에는 oom_score와 oom_score_adj를 함께 봐야 실제 kill 우선순위를 해석할 수 있습니다.
// 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() 호출
// 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
// 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 시작
// 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 방지
// 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/ | |
| **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/ | |
| **/proc/ | 사용자 공간에 보이는 정규화된 OOM 우선순위. 내부 raw 점수와 함께 `oom_score_adj` 영향이 반영됨. | `cat /proc/ | |
| **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_*` vmstat | free 총량은 있어도 연속 페이지가 부족할 수 있음. | THP defrag, compaction, CMA/Movable 영역 |
| swap I/O 급증 뒤 OOM | `vmstat 1`, `SwapFree`, `pgmajfault` | working set이 RAM을 넘고 swap도 부족해지는 흐름. | swappiness, zswap/zram, 익명 페이지 증가 |
| 특정 서비스가 반복 희생 | `/proc/ | 큰 RSS 또는 높은 adj로 우선순위가 높아짐. | adj 정책, cgroup 분리, 서비스별 memory.max |
mm/oom_kill.c — Linux 7.0 소스mm/page_alloc.c — OOM 진입 전 할당 slowpathmm/memcontrol.c — cgroup v2 OOM 그룹 처리include/linux/oom.h — oom_control 정의fs/proc/base.c — /proc//oom_score 표시값