Free page가 있는데도 할당이 느려지는 이유: allocator slow path

RAM에 free page가 남아 있는데 작은 할당 하나가 오래 걸릴 수 있습니다. allocator가 요구받는 것은 아무 위치의 빈 byte가 아니라, 특정 node와 zone에서 얻을 수 있는 특정 order의 block이기 때문입니다. reserve를 제외한 여유, 물리적 연속성, caller가 수행할 수 있는 작업까지 맞아야 합니다.

Linux는 남는 RAM을 page cache로 적극적으로 사용하고, overcommit과 demand paging도 허용합니다. virtual mapping을 만드는 시점과 실제 physical page가 필요한 시점이 다르므로 memory pressure는 나중의 fault나 kernel allocation에서 갑자기 드러날 수 있습니다. reclaim은 이미 쓰이는 memory 중에서 다른 backing으로 복구할 수 있거나 더 이상 필요하지 않은 부분을 돌려받는 과정입니다. 전체 RAM 사용률 하나만 보고 시작과 끝을 판단할 수는 없습니다.

Fast path에서 slow path로 넘어갈 때

physical allocation은 가능한 한 PCP와 buddy free list에서 요청을 해결합니다. 적합한 zone인지, 요청 order의 block이 있는지, watermark와 low-memory reserve를 고려해도 사용할 수 있는지를 확인합니다. eligible zone에서 보수적인 fast-path 조건을 만족하지 못하면 __alloc_pages_slowpath()가 더 많은 정책을 적용합니다.

NUMA node는 대체로 비슷한 접근 특성을 가진 memory의 단위이며, zone은 그 안에서 주소 범위와 allocation 제약을 구분합니다. 예를 들어 장치가 접근할 수 있는 physical address 범위가 제한되면 아무 node·zone의 free page나 대신 줄 수 없습니다. zone에 속한 PFN 범위와 그 안에서 buddy allocator가 실제 관리하는 page 수도 구분해야 합니다.

low watermark 아래에서는 background reclaim을 요청하고, minimum watermark를 만족시키기 어려운 요청에서는 direct reclaim을 검토한다는 구도가 출발점입니다. 다만 이것을 두 숫자만 비교하는 독립적인 스위치로 이해하면 안 됩니다. GFP, zone 제한, reserve, fragmentation에 따라 할 수 있는 작업과 성공 가능성이 달라집니다. slow path에서도 flag를 정밀하게 계산해 다시 할당해 보면 reclaim 실행 없이 성공할 수 있습니다.

N0만 허용된 order-2 요청을 가정했습니다. 다른 node의 page, 허용 zone 밖의 page, 서로 떨어진 page, watermark를 만족하지 못하는 block을 구분한 독립 예시입니다.
N0만 허용된 order-2 요청을 가정했습니다. 다른 node의 page, 허용 zone 밖의 page, 서로 떨어진 page, watermark를 만족하지 못하는 block을 구분한 독립 예시입니다.

예를 들어 order-2 요청에는 base page 네 장이 연속되고 적절히 정렬된 block이 필요합니다. 네 장을 따로 회수했다는 사실만으로 이 요청에 답할 수는 없습니다. reclaim은 사용 중인 page를 free 상태로 만들고, compaction은 movable page를 옮겨 연속 공간을 마련합니다. 두 과정이 협력하더라도 같은 작업은 아닙니다.

Caller가 허용한 작업부터 확인합니다

slow path는 먼저 __GFP_DIRECT_RECLAIM으로 direct reclaim 가능 여부를, order > PAGE_ALLOC_COSTLY_ORDER로 costly allocation 여부를 구분합니다. 기준값이 3인 구현에서 costly는 order-3 자체가 아니라 order-4 이상입니다. 높은 order는 회수한 page 총량이 늘어도 fragmentation 때문에 계속 실패할 수 있습니다.

sleep할 수 없는 context에서는 blocking reclaim을 실행할 수 없습니다. filesystem lock을 잡은 caller가 같은 filesystem의 writeback을 기다리는 경우에도 deadlock 위험이 있습니다. 따라서 NOFS·NOIO 같은 task context와 GFP의 filesystem·I/O 제약을 유지해야 합니다. 옛 kernel의 atomic flag 조합 검사 역시 이런 context 모순을 막기 위한 장치입니다.

__GFP_ATOMIC을 사용하던 고전 구현은 이 flag와 __GFP_DIRECT_RECLAIM이 동시에 들어오면 한 차례 경고하고 __GFP_ATOMIC을 지웠습니다. atomic reserve를 원한다는 표현과 잠들 수 있다는 표현의 모순을 정리하는 것입니다. 이 코드를 v6.12에 그대로 대입하면 안 됩니다. v6.12의 non-blocking 판단은 __GFP_DIRECT_RECLAIM이 없는지를 기준으로 합니다.

gfp_to_alloc_flags()는 slow path에서 사용할 watermark와 reserve 정책을 계산합니다. 기본 watermark를 minimum 수준으로 낮추고, background reclaim이 허용되면 ALLOC_KSWAPD를 반영합니다. 오래된 설명의 ALLOC_HIGH·ALLOC_HARDER를 최신 소스의 동일한 identifier로 찾으면 안 됩니다. v6.12에서는 ALLOC_MIN_RESERVE, ALLOC_NON_BLOCK, ALLOC_HIGHATOMIC 등으로 역할이 나뉘어 있습니다. non-blocking 요청인지, __GFP_NOMEMALLOC로 reserve 사용을 금지했는지, higher-order인지, task의 scheduling context가 어떤지를 함께 봅니다.

flag가 바뀌면 preferred zone도 다시 계산합니다. fast path에서 쓴 nodemask가 그대로 유효하리라고 가정하지 않으며, 조건에 맞는 zone이 아예 없으면 no-page 처리로 갑니다. background reclaim을 요청한 뒤 minimum 기준으로 free list를 다시 확인하는 단계가 있는 이유도 여기에 있습니다.

v6.12에서는 non-blocking 요청이고 __GFP_NOMEMALLOC이 없을 때 ALLOC_NON_BLOCK을, 그중 order > 0이면 ALLOC_HIGHATOMIC도 허용합니다. 이 경로에서 __GFP_HIGH에 대응하는 ALLOC_MIN_RESERVE가 있으면 cpuset 검사를 완화할 수 있습니다. 반대로 direct reclaim이 가능한 요청에서도 RT 또는 deadline task가 task context에서 실행 중이면 minimum reserve 접근을 추가로 허용합니다. reserve 정책은 단순히 atomic인지 아닌지 한 가지로 정해지지 않습니다.

Restart와 retry는 다른 상태를 초기화합니다

slow path의 local variable은 역할별로 읽으면 덜 헷갈립니다. page는 얻은 결과이고 처음에는 NULL입니다. did_some_progress는 direct reclaim이나 OOM 경로의 진전을 전달받습니다. compact_result는 가장 최근 compaction의 결과이며 compact_priority는 다음 compaction의 강도입니다. 이 enum 둘을 reclaim의 no_progress_loops와 같은 counter로 취급하면 안 됩니다. compaction_retries는 compaction 재시도 판단에 따로 전달됩니다.

고전 v6.1의 restart에서는 compaction_retries와 no_progress_loops를 각각 0으로 만들고 compact_priority를 DEF_COMPACT_PRIORITY로 돌립니다. cpuset과 zonelist cookie도 새로 읽습니다. 반면 retry는 같은 allocation 조건 안에서 진전을 이어 가는 label이므로 이 상태를 모두 초기화하지 않습니다. 조건 변경을 발견해 restart로 갈 때만 이전 범위에서 쌓은 실패 이력까지 새로 준비합니다. compact_result와 did_some_progress는 선언 시 임의의 성공값을 넣는 것이 아니라 해당 작업이 결과를 쓰는 순서에 맞춰 사용합니다.

아래는 비교 기준인 Linux v6.1의 gfp_to_alloc_flags() 전체입니다. 기본 ALLOC_WMARK_MIN | ALLOC_CPUSET에서 시작하고 __GFP_HIGH와 __GFP_KSWAPD_RECLAIM을 대응 flag로 옮깁니다. BUILD_BUG_ON()은 대응 bit 값이 같다는 가정을 compile time에 확인합니다. atomic 요청은 __GFP_NOMEMALLOC이 없을 때만 ALLOC_HARDER를 더하고, 이 v6.1 branch에서는 cpuset flag를 지웁니다. atomic이 아닌 RT task는 task context에서 ALLOC_HARDER를 더할 수 있습니다. 끝의 CMA helper도 생략하지 않았습니다. ALLOC_HIGH를 32-bit에만 쓰는 flag라고 제한하는 설명은 이 구현에 맞지 않습니다.

v6.1 원본 · 전체 code 파일 · GPL-2.0

v6_1_gfp_to_alloc_flags.c

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

/* Extracted unchanged from Linux v6.1, mm/page_alloc.c:4823-4863.
 * Full function definition; surrounding kernel context is required.
 * Source: https://github.com/torvalds/linux/blob/v6.1/mm/page_alloc.c#L4823
 * License text: GPL-2.0.txt (in this directory).
 */
static inline unsigned int
gfp_to_alloc_flags(gfp_t gfp_mask)
{
	unsigned int alloc_flags = ALLOC_WMARK_MIN | ALLOC_CPUSET;

	/*
	 * __GFP_HIGH is assumed to be the same as ALLOC_HIGH
	 * and __GFP_KSWAPD_RECLAIM is assumed to be the same as ALLOC_KSWAPD
	 * to save two branches.
	 */
	BUILD_BUG_ON(__GFP_HIGH != (__force gfp_t) ALLOC_HIGH);
	BUILD_BUG_ON(__GFP_KSWAPD_RECLAIM != (__force gfp_t) ALLOC_KSWAPD);

	/*
	 * The caller may dip into page reserves a bit more if the caller
	 * cannot run direct reclaim, or if the caller has realtime scheduling
	 * policy or is asking for __GFP_HIGH memory.  GFP_ATOMIC requests will
	 * set both ALLOC_HARDER (__GFP_ATOMIC) and ALLOC_HIGH (__GFP_HIGH).
	 */
	alloc_flags |= (__force int)
		(gfp_mask & (__GFP_HIGH | __GFP_KSWAPD_RECLAIM));

	if (gfp_mask & __GFP_ATOMIC) {
		/*
		 * Not worth trying to allocate harder for __GFP_NOMEMALLOC even
		 * if it can't schedule.
		 */
		if (!(gfp_mask & __GFP_NOMEMALLOC))
			alloc_flags |= ALLOC_HARDER;
		/*
		 * Ignore cpuset mems for GFP_ATOMIC rather than fail, see the
		 * comment for __cpuset_node_allowed().
		 */
		alloc_flags &= ~ALLOC_CPUSET;
	} else if (unlikely(rt_task(current)) && in_task())
		alloc_flags |= ALLOC_HARDER;

	alloc_flags = gfp_to_alloc_flags_cma(gfp_mask, alloc_flags);

	return alloc_flags;
}

Compaction을 먼저 시도하는 조건

v6.12의 early direct compaction은 direct reclaim과 compaction이 모두 가능하고, costly order이거나 order > 0인 non-movable 요청일 때 검토합니다. 여기에 !gfp_pfmemalloc_allowed(gfp_mask) 조건도 필요합니다. watermark를 우회할 권한이 있는 caller라면 그 권한으로 할당을 시도하기도 전에 compaction부터 수행하지 않겠다는 순서입니다. non-movable 쪽의 경계는 order-2가 아니라 order-1부터입니다.

compaction은 이미 free인 base page가 충분해도 연속 block이 없을 때 의미가 있습니다. movable folio를 옮겨 buddy가 합쳐질 공간을 만들며, non-movable 할당이 pageblock을 더 잘게 파편화하기 전에 시도할 수도 있습니다. 이후 retry loop에서는 ALLOC_KSWAPD가 있으면 background thread가 중간에 잠들었을 가능성에 대비해 wakeup을 다시 요청합니다. 그 뒤 reserve와 nodemask를 재계산하고 free list를 다시 확인한 다음에 direct reclaim과 후속 compaction으로 진행합니다.

Reserve는 추가 RAM이 아니라 사용 권한입니다

emergency reserve를 쓴다는 것은 memory가 새로 생겼다는 뜻이 아닙니다. 일반 요청에서 남겨 둔 page를 특정 caller에게 허용한다는 의미입니다. __gfp_pfmemalloc_flags() 계열의 판단에서는 __GFP_NOMEMALLOC이 우선하고, __GFP_MEMALLOC이나 적절한 PF_MEMALLOC context는 watermark를 우회할 수 있습니다. OOM victim은 exit를 진행해 memory를 반환해야 하므로 별도의 reserve 접근이 허용됩니다.

구체적으로 v6.12에서는 __GFP_NOMEMALLOC이면 reserve flag를 반환하지 않습니다. __GFP_MEMALLOC은 ALLOC_NO_WATERMARKS로 이어지며, task의 PF_MEMALLOC은 serving-softirq context이거나 interrupt context 밖일 때 같은 우회를 허용합니다. OOM victim을 위한 ALLOC_OOM도 interrupt context 밖에서 판단합니다. task flag가 설정됐다는 이유만으로 모든 interrupt allocation에 같은 권한이 생기는 것은 아니며, ALLOC_OOM을 모든 watermark를 무시하는 권한과 같다고 보아서도 안 됩니다.

아래는 이 판단을 수행하는 v6.12의 __gfp_pfmemalloc_flags() 전체입니다. 첫 번째 __GFP_NOMEMALLOC return이 뒤의 모든 reserve 허용보다 우선한다는 점, task의 OOM 상태를 보는 branch가 interrupt 바깥에 있다는 점을 함께 확인할 수 있습니다. 앞의 고전 v6.1 slow path와 구별한 현대 비교 code이며 kernel context를 필요로 합니다.

v6.12 원본 · 전체 code · GPL-2.0

v6_12___gfp_pfmemalloc_flags.c

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

/* Extracted unchanged from Linux v6.12, mm/page_alloc.c:4060-4076.
 * Complete named function; surrounding kernel context is required.
 * Source: https://github.com/torvalds/linux/blob/v6.12/mm/page_alloc.c#L4060-L4076
 * License text: GPL-2.0.txt (in this directory).
 */
static inline int __gfp_pfmemalloc_flags(gfp_t gfp_mask)
{
	if (unlikely(gfp_mask & __GFP_NOMEMALLOC))
		return 0;
	if (gfp_mask & __GFP_MEMALLOC)
		return ALLOC_NO_WATERMARKS;
	if (in_serving_softirq() && (current->flags & PF_MEMALLOC))
		return ALLOC_NO_WATERMARKS;
	if (!in_interrupt()) {
		if (current->flags & PF_MEMALLOC)
			return ALLOC_NO_WATERMARKS;
		else if (oom_reserves_allowed(current))
			return ALLOC_OOM;
	}

	return 0;
}

reserve flag가 있거나 ALLOC_CPUSET 제약을 적용하지 않는 경로에서는 ac->nodemask를 비우고 preferred zone을 다시 찾습니다. 일반 caller의 memory policy를 무조건 지운다는 뜻이 아니라, 이러한 우회가 허용된 시스템 allocation의 예외입니다.

reserve flag를 적용할 때도 다른 allocation 성격을 함께 보존해야 합니다. v6.12의 gfp_to_alloc_flags_cma()는 CMA가 켜져 있고 movable 요청이면 ALLOC_CMA를 추가할 수 있으므로 입력 flag를 언제나 그대로 돌려주는 함수가 아닙니다. slow path는 이 결과에 기존 ALLOC_KSWAPD도 보존하여, reserve 정책을 바꾸었다고 background reclaim 허용 여부까지 잃지 않도록 합니다.

이때 PF_MEMALLOC은 무조건 좋은 allocation 옵션이 아닙니다. reclaim을 수행하는 task가 내부 allocation 때문에 다시 reclaim으로 재귀 진입하지 않도록 하는 역할도 합니다. memalloc_noreclaim_save()와 restore helper는 이전 task 상태를 보존해야 하며, 일을 마쳤다고 flag를 무조건 지우면 바깥 caller의 상태까지 훼손합니다.

적절한 reserve 정책으로 재시도해도 실패하고 direct reclaim이 허용되지 않으면 더 진행할 수 없습니다. 이미 PF_MEMALLOC인 task도 재귀 reclaim에 들어가지 않습니다. direct compaction을 먼저 시도하거나 reclaim 뒤에 다시 시도하는 분기도 있지만, 반환된 page가 있을 때에만 원래 할당이 성공한 것입니다.

Retry는 횟수뿐 아니라 가능성을 평가합니다

__GFP_NORETRY는 과도한 reclaim·retry를 피하도록 하고, costly request는 __GFP_RETRY_MAYFAIL 여부도 중요합니다. 더 시도할 수 있는 경우에는 should_reclaim_retry()가 최근 진전과 zone의 상황을 살펴봅니다. 작은 order에서 진전이 있으면 no-progress count를 초기화할 수 있지만 costly allocation에서는 같은 회수가 연속 block 확보를 보장하지 않으므로 더 보수적으로 셉니다.

v6.12에서는 costly retry에 __GFP_RETRY_MAYFAIL뿐 아니라 compaction 가능 여부도 필요합니다. reclaim retry가 유망하지 않아도 실제 회수 진전이 있었고 compaction이 가능하다면 should_compact_retry()를 별도로 검사합니다. MAX_RECLAIM_RETRIES가 16인 이 구현에서 counter 비교는 > 16이며, 이를 모든 allocation의 총 시도 횟수가 정확히 16회라는 뜻으로 읽으면 안 됩니다. 낮은 order에서 진전이 있으면 counter가 다시 0이 되기 때문입니다.

이 함수는 eligible zone의 free page와 reclaimable page를 더해, 모두 회수할 수 있다고 가정해도 minimum watermark를 만족할 가능성이 있는지 계산합니다. 이것조차 불가능하다면 같은 일을 반복할 근거가 약합니다. 반대로 reclaimable page가 있다는 사실은 실제로 모두 즉시 회수된다는 약속은 아닙니다. dirty, writeback, pin, mapping, zone 제약이 뒤의 실제 처리에 남아 있습니다.

v6.12에서는 no-progress 한계를 넘거나 재시도할 zone을 찾지 못한 경우 highatomic reserve를 해제할 수 있는지도 마지막으로 살핍니다. workqueue worker가 잠들지 않고 반복하면 concurrency 관리에 악영향을 줄 수 있어 짧은 sleep을 넣는 처리도 있습니다. 일반 task의 reschedule과 같은 것으로 뭉뚱그리면 이 예외의 목적을 놓칩니다.

다음은 v6.12의 should_reclaim_retry() 전체입니다. cpuset이 적용되는 zone을 거르는 검사와 trace hook, worker의 sleep도 포함합니다. 한계를 넘은 경로는 중간 sleep 없이 out으로 가며, 그 밖의 scan 경로는 worker 여부에 맞게 양보한 뒤 재시도 가능성을 반환합니다. 마지막 highatomic reserve 해제는 counter 초과뿐 아니라 적합한 zone을 찾지 못한 경우에도 실행될 수 있습니다.

v6.12 원본 · 전체 code · GPL-2.0

v6_12_should_reclaim_retry.c

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

/* Extracted unchanged from Linux v6.12, mm/page_alloc.c:4093-4168.
 * Complete named function; surrounding kernel context is required.
 * Source: https://github.com/torvalds/linux/blob/v6.12/mm/page_alloc.c#L4093-L4168
 * License text: GPL-2.0.txt (in this directory).
 */
static inline bool
should_reclaim_retry(gfp_t gfp_mask, unsigned order,
		     struct alloc_context *ac, int alloc_flags,
		     bool did_some_progress, int *no_progress_loops)
{
	struct zone *zone;
	struct zoneref *z;
	bool ret = false;

	/*
	 * Costly allocations might have made a progress but this doesn't mean
	 * their order will become available due to high fragmentation so
	 * always increment the no progress counter for them
	 */
	if (did_some_progress && order <= PAGE_ALLOC_COSTLY_ORDER)
		*no_progress_loops = 0;
	else
		(*no_progress_loops)++;

	if (*no_progress_loops > MAX_RECLAIM_RETRIES)
		goto out;


	/*
	 * Keep reclaiming pages while there is a chance this will lead
	 * somewhere.  If none of the target zones can satisfy our allocation
	 * request even if all reclaimable pages are considered then we are
	 * screwed and have to go OOM.
	 */
	for_each_zone_zonelist_nodemask(zone, z, ac->zonelist,
				ac->highest_zoneidx, ac->nodemask) {
		unsigned long available;
		unsigned long reclaimable;
		unsigned long min_wmark = min_wmark_pages(zone);
		bool wmark;

		if (cpusets_enabled() &&
			(alloc_flags & ALLOC_CPUSET) &&
			!__cpuset_zone_allowed(zone, gfp_mask))
				continue;

		available = reclaimable = zone_reclaimable_pages(zone);
		available += zone_page_state_snapshot(zone, NR_FREE_PAGES);

		/*
		 * Would the allocation succeed if we reclaimed all
		 * reclaimable pages?
		 */
		wmark = __zone_watermark_ok(zone, order, min_wmark,
				ac->highest_zoneidx, alloc_flags, available);
		trace_reclaim_retry_zone(z, order, reclaimable,
				available, min_wmark, *no_progress_loops, wmark);
		if (wmark) {
			ret = true;
			break;
		}
	}

	/*
	 * Memory allocation/reclaim might be called from a WQ context and the
	 * current implementation of the WQ concurrency control doesn't
	 * recognize that a particular WQ is congested if the worker thread is
	 * looping without ever sleeping. Therefore we have to do a short sleep
	 * here rather than calling cond_resched().
	 */
	if (current->flags & PF_WQ_WORKER)
		schedule_timeout_uninterruptible(1);
	else
		cond_resched();
out:
	/* Before OOM, exhaust highatomic_reserve */
	if (!ret)
		return unreserve_highatomic_pageblock(ac, true);

	return ret;
}

OOM 검토와 최종 실패는 마지막 단계입니다

reclaim·compaction 재시도가 유망하지 않을 때 OOM 경로를 검토합니다. 다른 task가 먼저 memory를 만들었을 수 있으므로 그 경로에서도 마지막 할당을 시도합니다. OOM 처리에 진전이 있으면 no-progress 상태를 초기화하고 재시도할 수 있지만, 이미 victim인 task가 종료 직전에 같은 loop를 끝없이 도는 일도 피해야 합니다.

실패 직전에는 cpuset과 zonelist가 도중에 바뀌었는지도 확인합니다. 저장해 둔 cookie와 현재 상태가 달라졌다면 낡은 범위에서의 실패를 OOM으로 굳히지 않고 restart로 돌아가 allocation 조건과 retry 상태를 새로 준비합니다. OOM 경로 뒤에는 tsk_is_oom_victim(current)이고 ALLOC_OOM을 사용했거나 __GFP_NOMEMALLOC로 reserve를 금지한 경우 no-page 처리로 빠져, 종료해야 할 task의 무의미한 반복을 막습니다.

일반 실패는 page를 얻지 못한 상태를 반환하고, __GFP_NOWARN이 아니라면 allocation warning을 남길 수 있습니다. __GFP_NOFAIL은 실패 반환을 허용하지 않는 강한 계약입니다. 지원되지 않는 큰 order나 direct reclaim 없는 사용은 경고 대상이며, memory를 공짜로 보장하는 옵션이 아닙니다. caller가 오래 기다리는 것을 받아들일 수 있는지까지 포함한 설계 조건입니다.

no-fail의 마지막 보완도 reserve를 무제한으로 소비하는 방식은 아닙니다. v6.12는 __alloc_pages_cpuset_fallback()에 ALLOC_MIN_RESERVE를 전달하고, 실패하면 reschedule 기회를 준 뒤 다시 시도합니다. 고전 구현의 같은 자리에 보이는 이름은 ALLOC_HARDER입니다. 어느 쪽도 이 단계에서 ALLOC_NO_WATERMARKS로 전체 reserve를 소진하도록 만들지는 않습니다. direct reclaim이 불가능한 잘못된 no-fail 요청은 실패 반환 경로로 갈 수 있습니다.

따라서 allocation latency를 해석할 때는 free 총량보다 요청의 order·zone·GFP context를 먼저 놓아야 합니다. background reclaim 요청, direct reclaim 실행, compaction, reserve 재시도, OOM 검토는 서로 다른 단계입니다. 어느 단계에서 진전이 있었다는 사실과 원래 요청을 실제로 만족했다는 사실도 구분해야 합니다.

고전 slow path의 전체 제어 흐름

아래는 Linux v6.1의 __alloc_pages_slowpath() 전체 정의입니다. 위에서 설명한 v6.12 정책을 이 code의 identifier와 그대로 대응시키지 마십시오. 특히 __GFP_ATOMIC, ALLOC_HARDER, compaction 허용 조건은 version에 따라 달라졌습니다. 이 파일은 upstream 함수의 선언부터 마지막 반환까지를 보존한 kernel code이며 독립 실행 예제는 아닙니다.

읽을 때에는 먼저 restart에서 조건을 세우는 경로, retry에서 reclaim과 compaction을 이어 가는 경로, nopage에서 조건 변경과 no-fail 계약을 다시 보는 경로를 구분해 보십시오. got_pg는 정상 page뿐 아니라 최종 NULL 반환도 모이는 반환 지점입니다. label 이름 자체가 allocation 성공을 보증하지는 않습니다.

v6.1 원본 · 전체 code 파일 · GPL-2.0

v6_1_alloc_pages_slowpath.c

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

/* Extracted unchanged from Linux v6.1, mm/page_alloc.c:5022-5304.
 * Full function definition; surrounding kernel context is required.
 * Source: https://github.com/torvalds/linux/blob/v6.1/mm/page_alloc.c#L5022
 * License text: GPL-2.0.txt (in this directory).
 */
static inline struct page *
__alloc_pages_slowpath(gfp_t gfp_mask, unsigned int order,
						struct alloc_context *ac)
{
	bool can_direct_reclaim = gfp_mask & __GFP_DIRECT_RECLAIM;
	const bool costly_order = order > PAGE_ALLOC_COSTLY_ORDER;
	struct page *page = NULL;
	unsigned int alloc_flags;
	unsigned long did_some_progress;
	enum compact_priority compact_priority;
	enum compact_result compact_result;
	int compaction_retries;
	int no_progress_loops;
	unsigned int cpuset_mems_cookie;
	unsigned int zonelist_iter_cookie;
	int reserve_flags;

	/*
	 * We also sanity check to catch abuse of atomic reserves being used by
	 * callers that are not in atomic context.
	 */
	if (WARN_ON_ONCE((gfp_mask & (__GFP_ATOMIC|__GFP_DIRECT_RECLAIM)) ==
				(__GFP_ATOMIC|__GFP_DIRECT_RECLAIM)))
		gfp_mask &= ~__GFP_ATOMIC;

restart:
	compaction_retries = 0;
	no_progress_loops = 0;
	compact_priority = DEF_COMPACT_PRIORITY;
	cpuset_mems_cookie = read_mems_allowed_begin();
	zonelist_iter_cookie = zonelist_iter_begin();

	/*
	 * The fast path uses conservative alloc_flags to succeed only until
	 * kswapd needs to be woken up, and to avoid the cost of setting up
	 * alloc_flags precisely. So we do that now.
	 */
	alloc_flags = gfp_to_alloc_flags(gfp_mask);

	/*
	 * We need to recalculate the starting point for the zonelist iterator
	 * because we might have used different nodemask in the fast path, or
	 * there was a cpuset modification and we are retrying - otherwise we
	 * could end up iterating over non-eligible zones endlessly.
	 */
	ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
					ac->highest_zoneidx, ac->nodemask);
	if (!ac->preferred_zoneref->zone)
		goto nopage;

	/*
	 * Check for insane configurations where the cpuset doesn't contain
	 * any suitable zone to satisfy the request - e.g. non-movable
	 * GFP_HIGHUSER allocations from MOVABLE nodes only.
	 */
	if (cpusets_insane_config() && (gfp_mask & __GFP_HARDWALL)) {
		struct zoneref *z = first_zones_zonelist(ac->zonelist,
					ac->highest_zoneidx,
					&cpuset_current_mems_allowed);
		if (!z->zone)
			goto nopage;
	}

	if (alloc_flags & ALLOC_KSWAPD)
		wake_all_kswapds(order, gfp_mask, ac);

	/*
	 * The adjusted alloc_flags might result in immediate success, so try
	 * that first
	 */
	page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
	if (page)
		goto got_pg;

	/*
	 * For costly allocations, try direct compaction first, as it's likely
	 * that we have enough base pages and don't need to reclaim. For non-
	 * movable high-order allocations, do that as well, as compaction will
	 * try prevent permanent fragmentation by migrating from blocks of the
	 * same migratetype.
	 * Don't try this for allocations that are allowed to ignore
	 * watermarks, as the ALLOC_NO_WATERMARKS attempt didn't yet happen.
	 */
	if (can_direct_reclaim &&
			(costly_order ||
			   (order > 0 && ac->migratetype != MIGRATE_MOVABLE))
			&& !gfp_pfmemalloc_allowed(gfp_mask)) {
		page = __alloc_pages_direct_compact(gfp_mask, order,
						alloc_flags, ac,
						INIT_COMPACT_PRIORITY,
						&compact_result);
		if (page)
			goto got_pg;

		/*
		 * Checks for costly allocations with __GFP_NORETRY, which
		 * includes some THP page fault allocations
		 */
		if (costly_order && (gfp_mask & __GFP_NORETRY)) {
			/*
			 * If allocating entire pageblock(s) and compaction
			 * failed because all zones are below low watermarks
			 * or is prohibited because it recently failed at this
			 * order, fail immediately unless the allocator has
			 * requested compaction and reclaim retry.
			 *
			 * Reclaim is
			 *  - potentially very expensive because zones are far
			 *    below their low watermarks or this is part of very
			 *    bursty high order allocations,
			 *  - not guaranteed to help because isolate_freepages()
			 *    may not iterate over freed pages as part of its
			 *    linear scan, and
			 *  - unlikely to make entire pageblocks free on its
			 *    own.
			 */
			if (compact_result == COMPACT_SKIPPED ||
			    compact_result == COMPACT_DEFERRED)
				goto nopage;

			/*
			 * Looks like reclaim/compaction is worth trying, but
			 * sync compaction could be very expensive, so keep
			 * using async compaction.
			 */
			compact_priority = INIT_COMPACT_PRIORITY;
		}
	}

retry:
	/* Ensure kswapd doesn't accidentally go to sleep as long as we loop */
	if (alloc_flags & ALLOC_KSWAPD)
		wake_all_kswapds(order, gfp_mask, ac);

	reserve_flags = __gfp_pfmemalloc_flags(gfp_mask);
	if (reserve_flags)
		alloc_flags = gfp_to_alloc_flags_cma(gfp_mask, reserve_flags) |
					  (alloc_flags & ALLOC_KSWAPD);

	/*
	 * Reset the nodemask and zonelist iterators if memory policies can be
	 * ignored. These allocations are high priority and system rather than
	 * user oriented.
	 */
	if (!(alloc_flags & ALLOC_CPUSET) || reserve_flags) {
		ac->nodemask = NULL;
		ac->preferred_zoneref = first_zones_zonelist(ac->zonelist,
					ac->highest_zoneidx, ac->nodemask);
	}

	/* Attempt with potentially adjusted zonelist and alloc_flags */
	page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
	if (page)
		goto got_pg;

	/* Caller is not willing to reclaim, we can't balance anything */
	if (!can_direct_reclaim)
		goto nopage;

	/* Avoid recursion of direct reclaim */
	if (current->flags & PF_MEMALLOC)
		goto nopage;

	/* Try direct reclaim and then allocating */
	page = __alloc_pages_direct_reclaim(gfp_mask, order, alloc_flags, ac,
							&did_some_progress);
	if (page)
		goto got_pg;

	/* Try direct compaction and then allocating */
	page = __alloc_pages_direct_compact(gfp_mask, order, alloc_flags, ac,
					compact_priority, &compact_result);
	if (page)
		goto got_pg;

	/* Do not loop if specifically requested */
	if (gfp_mask & __GFP_NORETRY)
		goto nopage;

	/*
	 * Do not retry costly high order allocations unless they are
	 * __GFP_RETRY_MAYFAIL
	 */
	if (costly_order && !(gfp_mask & __GFP_RETRY_MAYFAIL))
		goto nopage;

	if (should_reclaim_retry(gfp_mask, order, ac, alloc_flags,
				 did_some_progress > 0, &no_progress_loops))
		goto retry;

	/*
	 * It doesn't make any sense to retry for the compaction if the order-0
	 * reclaim is not able to make any progress because the current
	 * implementation of the compaction depends on the sufficient amount
	 * of free memory (see __compaction_suitable)
	 */
	if (did_some_progress > 0 &&
			should_compact_retry(ac, order, alloc_flags,
				compact_result, &compact_priority,
				&compaction_retries))
		goto retry;


	/*
	 * Deal with possible cpuset update races or zonelist updates to avoid
	 * a unnecessary OOM kill.
	 */
	if (check_retry_cpuset(cpuset_mems_cookie, ac) ||
	    check_retry_zonelist(zonelist_iter_cookie))
		goto restart;

	/* Reclaim has failed us, start killing things */
	page = __alloc_pages_may_oom(gfp_mask, order, ac, &did_some_progress);
	if (page)
		goto got_pg;

	/* Avoid allocations with no watermarks from looping endlessly */
	if (tsk_is_oom_victim(current) &&
	    (alloc_flags & ALLOC_OOM ||
	     (gfp_mask & __GFP_NOMEMALLOC)))
		goto nopage;

	/* Retry as long as the OOM killer is making progress */
	if (did_some_progress) {
		no_progress_loops = 0;
		goto retry;
	}

nopage:
	/*
	 * Deal with possible cpuset update races or zonelist updates to avoid
	 * a unnecessary OOM kill.
	 */
	if (check_retry_cpuset(cpuset_mems_cookie, ac) ||
	    check_retry_zonelist(zonelist_iter_cookie))
		goto restart;

	/*
	 * Make sure that __GFP_NOFAIL request doesn't leak out and make sure
	 * we always retry
	 */
	if (gfp_mask & __GFP_NOFAIL) {
		/*
		 * All existing users of the __GFP_NOFAIL are blockable, so warn
		 * of any new users that actually require GFP_NOWAIT
		 */
		if (WARN_ON_ONCE_GFP(!can_direct_reclaim, gfp_mask))
			goto fail;

		/*
		 * PF_MEMALLOC request from this context is rather bizarre
		 * because we cannot reclaim anything and only can loop waiting
		 * for somebody to do a work for us
		 */
		WARN_ON_ONCE_GFP(current->flags & PF_MEMALLOC, gfp_mask);

		/*
		 * non failing costly orders are a hard requirement which we
		 * are not prepared for much so let's warn about these users
		 * so that we can identify them and convert them to something
		 * else.
		 */
		WARN_ON_ONCE_GFP(costly_order, gfp_mask);

		/*
		 * Help non-failing allocations by giving them access to memory
		 * reserves but do not use ALLOC_NO_WATERMARKS because this
		 * could deplete whole memory reserves which would just make
		 * the situation worse
		 */
		page = __alloc_pages_cpuset_fallback(gfp_mask, order, ALLOC_HARDER, ac);
		if (page)
			goto got_pg;

		cond_resched();
		goto retry;
	}
fail:
	warn_alloc(gfp_mask, ac->nodemask,
			"page allocation failure: order:%u", order);
got_pg:
	return page;
}

참고 자료

  • Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 11, Physical Allocation Slow Path.
  • Linux v6.1 mm/page_alloc.c: 고전 __GFP_ATOMIC 조합 검사와 ALLOC_HARDER 이름을 비교할 기준입니다.
  • Linux v6.12 mm/page_alloc.c: gfp_to_alloc_flags(), __gfp_pfmemalloc_flags(), should_reclaim_retry(), __alloc_pages_slowpath().
  • Linux v6.18 mm/vmscan.c: reclaim 쪽의 caller와 context.