작은 anonymous page를 처음 건드렸는데 전혀 다른 process가 OOM victim으로 선택될 수 있습니다. fault를 일으킨 task가 memory pressure를 만든 주체와 같지 않기 때문입니다. mapping을 만든 시점과 physical page가 실제로 필요한 시점 사이에는 긴 간격이 있을 수 있습니다.
ordinary demand-paged mapping은 VMA를 준비하고 실제 backing을 나중 fault에서 할당할 수 있습니다. 모든 mmap이 physical allocation을 전혀 하지 않는다는 뜻은 아닙니다. population·locking·mapping 종류에 따른 예외가 있고, overcommit 정책도 mapping 시점의 admission에 관여합니다. 중요한 점은 과거 mapping 성공이 미래의 모든 fault 성공을 보장하지 않는다는 것입니다.
fault 시점에는 VMA 존재뿐 아니라 요청한 접근 권한과 mapping의 fault protocol도 확인합니다. 유효한 주소의 backing allocation 실패와 잘못된 주소·권한의 SIGSEGV는 같은 사건이 아닙니다. 일반 memory access instruction에는 과거 mmap() 호출의 반환값을 나중에 바꾸어 allocation 실패를 알리는 통로도 없습니다. 그렇다고 모든 fault 실패가 반드시 새 OOM kill 하나로 끝나는 것은 아닙니다.
부족한 것은 요청이 사용할 수 있는 memory입니다
allocator는 node·zone·order·GFP 조건을 만족하는 page를 찾아야 합니다. 다른 node의 free memory나 높은 zone의 page가 있어도 제한된 요청에는 도움이 안 될 수 있습니다. watermark와 reserve를 고려한 reclaim, compaction, 재시도 끝에도 답을 찾지 못할 때 OOM을 검토합니다.
clean file cache를 버리거나 dirty data를 writeback하고, anon 내용을 swap으로 옮기거나 slab cache를 줄이는 것은 process를 죽이지 않는 회수입니다. 이런 동작이 실패했다고 모든 allocation에서 바로 OOM killer를 호출하는 것은 아닙니다. non-blocking context나 fragmentation 중심의 costly request는 다른 실패 정책을 가집니다.
zone의 WMARK_MIN·WMARK_LOW·WMARK_HIGH는 각각 reserve를 고려한 allocation과 reclaim 판단, background reclaim의 시작·균형 판단에 쓰입니다. NUMA memory tiering에는 WMARK_PROMO도 있습니다. 흔히 low 근처에서 kswapd를 깨우고 더 심한 부족에 direct reclaim을 수행한다고 설명하지만, 실제 검사는 order·허용 zone·allocation flags·boost까지 반영합니다. min 미만이라는 숫자 하나를 OOM의 필요충분조건으로 사용할 수는 없습니다.
마지막 allocation 시도에도 kill을 피할 기회가 있습니다
allocation 측 __alloc_pages_may_oom()은 oom_lock으로 OOM 처리를 직렬화합니다. lock을 얻지 못하면 다른 task가 처리 중일 수 있으므로 진전이 있다고 보고 caller를 재시도로 돌릴 수 있습니다. OOM victim을 하나도 고르지 않았어도 이런 control-flow 의미의 progress가 가능합니다.
lock을 얻은 뒤에도 free list를 다시 확인합니다. v6.12·v6.18의 이 마지막 시도는 ALLOC_WMARK_HIGH | ALLOC_CPUSET을 사용하고, GFP mask에서 __GFP_DIRECT_RECLAIM을 제거합니다. 그 사이 다른 CPU가 돌려준 page를 찾되 oom_lock을 가진 상태에서 다시 direct reclaim 의존을 만들지 않으려는 것입니다.
그래도 실패하면 core dump, costly order, __GFP_RETRY_MAYFAIL, __GFP_THISNODE, 낮은 zone만 허용하는 요청, storage가 suspended인 상황 등 OOM이 도움이 되지 않거나 위험한 조건을 거릅니다. 각 version의 allocator branch로 확인해야 합니다.
특히 process를 죽여도 원하는 order의 연속 block이 생긴다는 보장은 없습니다. DMA처럼 제한된 zone의 부족도 큰 RSS process를 죽이는 것만으로 해결될지 알 수 없습니다. OOM은 모든 allocation failure를 대체하는 보편적인 recovery routine이 아닙니다.
oom_control이 이번 사건의 범위를 담습니다
struct oom_control에는 zonelist, nodemask, memcg, GFP mask, order가 들어갑니다. chosen task와 chosen_points는 선택 결과이고 totalpages는 점수 조정의 기준량입니다. global OOM, mempolicy·cpuset에 제한된 OOM, memcg OOM은 후보와 기준량이 다를 수 있습니다.
totalpages를 항상 system 전체 RAM+swap이라고 고정하면 constrained OOM 점수를 잘못 해석합니다. nodemask 역시 process RSS의 실제 물리적 분포를 직접 추적한 목록이 아닙니다. allocation의 정책적 허용 범위와 candidate eligibility를 판단하는 입력입니다.
정책은 kill 이외의 결과도 허용합니다
panic_on_oom=0은 일반적인 victim 선택을 사용합니다. 1은 일반적으로 system-wide unconstrained OOM에서 panic을 선택하고, 2는 constrained OOM까지 포함하는 강한 정책입니다. manual SysRq OOM은 panic 경로의 예외입니다. 무조건 Boolean on/off로만 이해하면 constraint별 차이를 놓칩니다.
oom_kill_allocating_task는 global 경로에서 current를 우선 대상으로 삼는 정책입니다. 하지만 current가 valid mm를 갖고 killable·eligible해야 하며 oom_score_adj=-1000 같은 제외 조건도 적용됩니다. 어떤 faulting task든 조건 없이 즉시 죽인다는 뜻은 아닙니다.
이런 정책은 application 하나가 아니라 system의 가용성과 장애 복구 방식에 영향을 줍니다. OOM 사건을 해석할 때는 당시 kernel version과 log의 constraint·GFP·order를 먼저 확인하는 것이 순서입니다.
SysRq-f는 pressure가 없어도 위험한 동작입니다
SysRq-f는 work를 예약해 OOM handler를 호출하며 order=-1로 manual invocation을 표시합니다. keyboard SysRq의 enable mask와 권한 있는 /proc/sysrq-trigger 경로의 접근 제어는 구분해야 합니다. manual이라는 이유로 안전한 관찰 기능이 되는 것은 아닙니다.
manual 경로는 panic-on-oom과 no-killable-task panic 등의 일부 pressure-specific 정책을 피하지만, OOM disabled나 eligibility 같은 조건은 여전히 존재합니다. '반드시 가장 큰 process 하나를 죽인다'는 보장도 아닙니다. pressure 유무와 무관하게 실제 task를 종료할 수 있는 관리 동작이라는 점이 핵심입니다.
oom_control과 마지막 allocation의 전체 code
아래는 official Linux v6.18의 전체 struct와 함수입니다. 각 code는 upstream notice를 유지하며 GPL-2.0을 함께 제공합니다. OOM을 재현하는 실험이 아니라 해당 tag의 제어 흐름을 읽는 예입니다.
/* SPDX-License-Identifier: GPL-2.0 */
#ifndef __INCLUDE_LINUX_OOM_H
#define __INCLUDE_LINUX_OOM_H
/*
* Official Linux v6.18: include/linux/oom.h
* https://github.com/torvalds/linux/blob/v6.18/include/linux/oom.h#L28
* Complete upstream scope, not an elided book-code reconstruction.
* See GPL-2.0.txt. Not a standalone translation unit.
*/
struct oom_control {
/* Used to determine cpuset */
struct zonelist *zonelist;
/* Used to determine mempolicy */
nodemask_t *nodemask;
/* Memory cgroup in which oom is invoked, or NULL for global oom */
struct mem_cgroup *memcg;
/* Used to determine cpuset and node locality requirement */
const gfp_t gfp_mask;
/*
* order == -1 means the oom kill is required by sysrq, otherwise only
* for display purposes.
*/
const int order;
/* Used by oom implementation, do not set */
unsigned long totalpages;
struct task_struct *chosen;
long chosen_points;
/* Used to print the constraint info. */
enum oom_constraint constraint;
};
// SPDX-License-Identifier: GPL-2.0-only
/*
* linux/mm/page_alloc.c
*
* Manages the free list, the system allocates free pages here.
* Note that kmalloc() lives in slab.c
*
* Copyright (C) 1991, 1992, 1993, 1994 Linus Torvalds
* Swap reorganised 29.12.95, Stephen Tweedie
* Support of BIGMEM added by Gerhard Wichert, Siemens AG, July 1999
* Reshaped it to be a zoned allocator, Ingo Molnar, Red Hat, 1999
* Discontiguous memory support, Kanoj Sarcar, SGI, Nov 1999
* Zone balancing, Kanoj Sarcar, SGI, Jan 2000
* Per cpu hot/cold page lists, bulk allocation, Martin J. Bligh, Sept 2002
* (lots of bits borrowed from Ingo Molnar & Andrew Morton)
*/
/*
* Official Linux v6.18: mm/page_alloc.c
* https://github.com/torvalds/linux/blob/v6.18/mm/page_alloc.c#L3985
* Complete upstream scope, not an elided book-code reconstruction.
* See GPL-2.0.txt. Not a standalone translation unit.
*/
static inline struct page *
__alloc_pages_may_oom(gfp_t gfp_mask, unsigned int order,
const struct alloc_context *ac, unsigned long *did_some_progress)
{
struct oom_control oc = {
.zonelist = ac->zonelist,
.nodemask = ac->nodemask,
.memcg = NULL,
.gfp_mask = gfp_mask,
.order = order,
};
struct page *page;
*did_some_progress = 0;
/*
* Acquire the oom lock. If that fails, somebody else is
* making progress for us.
*/
if (!mutex_trylock(&oom_lock)) {
*did_some_progress = 1;
schedule_timeout_uninterruptible(1);
return NULL;
}
/*
* Go through the zonelist yet one more time, keep very high watermark
* here, this is only to catch a parallel oom killing, we must fail if
* we're still under heavy pressure. But make sure that this reclaim
* attempt shall not depend on __GFP_DIRECT_RECLAIM && !__GFP_NORETRY
* allocation which will never fail due to oom_lock already held.
*/
page = get_page_from_freelist((gfp_mask | __GFP_HARDWALL) &
~__GFP_DIRECT_RECLAIM, order,
ALLOC_WMARK_HIGH|ALLOC_CPUSET, ac);
if (page)
goto out;
/* Coredumps can quickly deplete all memory reserves */
if (current->flags & PF_DUMPCORE)
goto out;
/* The OOM killer will not help higher order allocs */
if (order > PAGE_ALLOC_COSTLY_ORDER)
goto out;
/*
* We have already exhausted all our reclaim opportunities without any
* success so it is time to admit defeat. We will skip the OOM killer
* because it is very likely that the caller has a more reasonable
* fallback than shooting a random task.
*
* The OOM killer may not free memory on a specific node.
*/
if (gfp_mask & (__GFP_RETRY_MAYFAIL | __GFP_THISNODE))
goto out;
/* The OOM killer does not needlessly kill tasks for lowmem */
if (ac->highest_zoneidx < ZONE_NORMAL)
goto out;
if (pm_suspended_storage())
goto out;
/*
* XXX: GFP_NOFS allocations should rather fail than rely on
* other request to make a forward progress.
* We are in an unfortunate situation where out_of_memory cannot
* do much for this context but let's try it to at least get
* access to memory reserved if the current task is killed (see
* out_of_memory). Once filesystems are ready to handle allocation
* failures more gracefully we should just bail out here.
*/
/* Exhausted what can be done so it's blame time */
if (out_of_memory(&oc) ||
WARN_ON_ONCE_GFP(gfp_mask & __GFP_NOFAIL, gfp_mask)) {
*did_some_progress = 1;
/*
* Help non-failing allocations by giving them access to memory
* reserves
*/
if (gfp_mask & __GFP_NOFAIL)
page = __alloc_pages_cpuset_fallback(gfp_mask, order,
ALLOC_NO_WATERMARKS, ac);
}
out:
mutex_unlock(&oom_lock);
return page;
}
zonelist는 node 자체가 아니라 allocation이 살펴볼 zoneref 순서입니다. 각 zone은 node의 memory를 용도와 addressability에 따라 나눈 범위입니다. NUMA는 CPU에 따른 memory 접근 비용이 균일하지 않은 구조이고, zone 제약과 node 선호는 다른 축입니다. 예를 들어 x86의 낮은 DMA 범위에서 필요한 page를 다른 높은 zone의 page로 대신할 수는 없습니다.
nodemask는 mempolicy 제약을, memcg는 이번 OOM의 group 범위를 전달합니다. gfp_mask는 allocation context이고 order는 요청 크기이지만 OOM helper 안에서는 주로 진단 및 manual 호출 구별에 사용합니다. totalpages·chosen·chosen_points·constraint는 OOM 내부 결과이므로 caller가 임의의 전역 점수표로 채우지 않습니다.
마지막 allocation이 성공하면 task를 고르지 않고 lock을 풀어 반환합니다. core dump 또는 order가 PAGE_ALLOC_COSTLY_ORDER보다 큰 경우에는 그 다음 OOM 호출을 건너뜁니다. 낮은 zone·THISNODE 등에서도 NULL 반환은 가능한 결과입니다. __GFP_NOFAIL의 예외 처리는 OOM 호출 뒤 progress 표시와 reserve 접근을 돕는 것이며, 위의 모든 early exit를 없애는 마법의 flag가 아닙니다.
Manual OOM과 panic 검사를 code에서 구별합니다
// SPDX-License-Identifier: GPL-2.0
/*
* Linux Magic System Request Key Hacks
*
* (c) 1997 Martin Mares <mj@atrey.karlin.mff.cuni.cz>
* based on ideas by Pavel Machek <pavel@atrey.karlin.mff.cuni.cz>
*
* (c) 2000 Crutcher Dunnavant <crutcher+kernel@datastacks.com>
* overhauled to use key registration
* based upon discusions in irc://irc.openprojects.net/#kernelnewbies
*
* Copyright (c) 2010 Dmitry Torokhov
* Input handler conversion
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
/*
* Official Linux v6.18: drivers/tty/sysrq.c
* https://github.com/torvalds/linux/blob/v6.18/drivers/tty/sysrq.c#L404
* Complete upstream scope, not an elided book-code reconstruction.
* See GPL-2.0.txt. Not a standalone translation unit.
*/
static void sysrq_handle_moom(u8 key)
{
schedule_work(&moom_work);
}
// SPDX-License-Identifier: GPL-2.0
/*
* Linux Magic System Request Key Hacks
*
* (c) 1997 Martin Mares <mj@atrey.karlin.mff.cuni.cz>
* based on ideas by Pavel Machek <pavel@atrey.karlin.mff.cuni.cz>
*
* (c) 2000 Crutcher Dunnavant <crutcher+kernel@datastacks.com>
* overhauled to use key registration
* based upon discusions in irc://irc.openprojects.net/#kernelnewbies
*
* Copyright (c) 2010 Dmitry Torokhov
* Input handler conversion
*/
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
/*
* Official Linux v6.18: drivers/tty/sysrq.c
* https://github.com/torvalds/linux/blob/v6.18/drivers/tty/sysrq.c#L385
* Complete upstream scope, not an elided book-code reconstruction.
* See GPL-2.0.txt. Not a standalone translation unit.
*/
static void moom_callback(struct work_struct *ignored)
{
const gfp_t gfp_mask = GFP_KERNEL;
struct oom_control oc = {
.zonelist = node_zonelist(first_memory_node, gfp_mask),
.nodemask = NULL,
.memcg = NULL,
.gfp_mask = gfp_mask,
.order = -1,
};
mutex_lock(&oom_lock);
if (!out_of_memory(&oc))
pr_info("OOM request ignored. No task eligible\n");
mutex_unlock(&oom_lock);
}
// SPDX-License-Identifier: GPL-2.0-only
/*
* linux/mm/oom_kill.c
*
* Copyright (C) 1998,2000 Rik van Riel
* Thanks go out to Claus Fischer for some serious inspiration and
* for goading me into coding this file...
* Copyright (C) 2010 Google, Inc.
* Rewritten by David Rientjes
*
* The routines in this file are used to kill a process when
* we're seriously out of memory. This gets called from __alloc_pages()
* in mm/page_alloc.c when we really run out of memory.
*
* Since we won't call these routines often (on a well-configured
* machine) this file will double as a 'coding guide' and a signpost
* for newbie kernel hackers. It features several pointers to major
* kernel subsystems and hints as to where to find out what things do.
*/
/*
* Official Linux v6.18: mm/oom_kill.c
* https://github.com/torvalds/linux/blob/v6.18/mm/oom_kill.c#L157
* Complete upstream scope, not an elided book-code reconstruction.
* See GPL-2.0.txt. Not a standalone translation unit.
*/
static inline bool is_sysrq_oom(struct oom_control *oc)
{
return oc->order == -1;
}
// SPDX-License-Identifier: GPL-2.0-only
/*
* linux/mm/oom_kill.c
*
* Copyright (C) 1998,2000 Rik van Riel
* Thanks go out to Claus Fischer for some serious inspiration and
* for goading me into coding this file...
* Copyright (C) 2010 Google, Inc.
* Rewritten by David Rientjes
*
* The routines in this file are used to kill a process when
* we're seriously out of memory. This gets called from __alloc_pages()
* in mm/page_alloc.c when we really run out of memory.
*
* Since we won't call these routines often (on a well-configured
* machine) this file will double as a 'coding guide' and a signpost
* for newbie kernel hackers. It features several pointers to major
* kernel subsystems and hints as to where to find out what things do.
*/
/*
* Official Linux v6.18: mm/oom_kill.c
* https://github.com/torvalds/linux/blob/v6.18/mm/oom_kill.c#L1074
* Complete upstream scope, not an elided book-code reconstruction.
* See GPL-2.0.txt. Not a standalone translation unit.
*/
static void check_panic_on_oom(struct oom_control *oc)
{
if (likely(!sysctl_panic_on_oom))
return;
if (sysctl_panic_on_oom != 2) {
/*
* panic_on_oom == 1 only affects CONSTRAINT_NONE, the kernel
* does not panic for cpuset, mempolicy, or memcg allocation
* failures.
*/
if (oc->constraint != CONSTRAINT_NONE)
return;
}
/* Do not panic for oom kills triggered by sysrq */
if (is_sysrq_oom(oc))
return;
dump_header(oc);
panic("Out of memory: %s panic_on_oom is enabled\n",
sysctl_panic_on_oom == 2 ? "compulsory" : "system-wide");
}
키 처리는 work를 예약하고 callback은 oom_lock을 잡아 out_of_memory()를 호출합니다. GFP_KERNEL·NULL memcg·order=-1인 control을 사용합니다. false 반환이면 eligible task가 없었다는 진단을 남기지만, true도 새 SIGKILL을 보냈다는 보장은 아닙니다. 실제 handler의 다른 progress 경로가 남아 있기 때문입니다.
panic 검사는 0이면 돌아가고, 2가 아니라면 constraint가 NONE일 때만 panic 경로로 진행합니다. 그 뒤 manual OOM을 다시 제외합니다. 실제 allocation OOM에서 victim이 전혀 없는 경우의 deadlock panic은 이 정책 검사와 별도의 branch입니다. 그래서 manual OOM의 예외를 모든 kernel panic에 대한 절대 보증으로 확대하지 않습니다.
참고 자료
- Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 13, OOM Causes / Alternative Behaviours / Allocation Entry.
- Linux v6.12 mm/page_alloc.c, v6.18 mm/page_alloc.c:
__alloc_pages_may_oom(). - Linux v6.18 mm/oom_kill.c, include/linux/oom.h.
- Linux v6.18 SysRq 문서, VM sysctl 문서.