Direct reclaim: 할당한 task가 회수 비용을 부담할 때

작은 memory allocation이 느려졌을 때 기다리는 상대가 늘 disk인 것은 아닙니다. 요청한 task가 직접 다른 folio를 검사하고, unmap하거나 writeback 가능성을 판단하고 있을 수 있습니다. direct reclaim은 background worker에 일을 맡기고 기다리는 기능이 아니라 caller의 context에서 수행되는 reclaim입니다.

회수한 수와 반환할 page는 다릅니다

__alloc_pages_direct_reclaim()은 reclaim을 수행한 뒤 원래 allocation을 다시 시도합니다. 회수 함수가 진전을 보고해도 즉시 page를 반환할 수 있는 것은 아닙니다. 다른 CPU가 free page를 먼저 가져갔을 수 있고, higher-order 요청이라면 필요한 연속 block이 아직 없을 수도 있습니다.

v6.12의 이 경로에서는 진전이 없으면 page를 얻지 못한 채 돌아갑니다. 진전이 있었지만 할당에 실패하면 highatomic reserve를 일부 풀고 PCP page를 buddy 쪽으로 drain한 뒤 한 번 더 시도할 수 있습니다. drain을 반복한다고 memory가 무한히 생기는 것은 아니므로 이 보완은 한 차례로 제한합니다.

아래는 v6.12의 __alloc_pages_direct_reclaim() 전체입니다. drained가 한 번의 보완만 허용하며, PSI의 memstall 진입·종료도 reclaim 결과나 최종 allocation 성공과 별도로 유지됩니다. 반환값은 실제로 얻은 page pointer 또는 NULL이고, reclaim 진전은 출력 인자로 전달합니다.

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

v6_12___alloc_pages_direct_reclaim.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:3939-3971.
 * Complete named function; surrounding kernel context is required.
 * Source: https://github.com/torvalds/linux/blob/v6.12/mm/page_alloc.c#L3939-L3971
 * License text: GPL-2.0.txt (in this directory).
 */
static inline struct page *
__alloc_pages_direct_reclaim(gfp_t gfp_mask, unsigned int order,
		unsigned int alloc_flags, const struct alloc_context *ac,
		unsigned long *did_some_progress)
{
	struct page *page = NULL;
	unsigned long pflags;
	bool drained = false;

	psi_memstall_enter(&pflags);
	*did_some_progress = __perform_reclaim(gfp_mask, order, ac);
	if (unlikely(!(*did_some_progress)))
		goto out;

retry:
	page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);

	/*
	 * If an allocation failed after direct reclaim, it could be because
	 * pages are pinned on the per-cpu lists or in high alloc reserves.
	 * Shrink them and try again
	 */
	if (!page && !drained) {
		unreserve_highatomic_pageblock(ac, false);
		drain_all_pages(NULL);
		drained = true;
		goto retry;
	}
out:
	psi_memstall_leave(&pflags);

	return page;
}

Reclaim 안의 allocation은 재귀로 들어가지 않습니다

__perform_reclaim()은 task의 reclaim context를 설정한 뒤 try_to_free_pages()를 호출합니다. 내부에서는 memalloc_noreclaim_save()와 restore를 사용해 PF_MEMALLOC 상태를 관리합니다. reclaim에 필요한 metadata allocation이 다시 같은 reclaim으로 들어가면 끝없는 재귀나 deadlock이 생길 수 있기 때문입니다.

save/restore는 바깥 상태를 되살리는 방식이어야 합니다. 중첩된 caller가 이미 reserve 접근이나 noreclaim 상태를 설정했을 수 있으므로 종료 시 무조건 flag를 clear해서는 안 됩니다. memory-pressure accounting이나 scheduling 관련 처리도 실제 caller context에서 이뤄집니다.

v6.12의 전체 __perform_reclaim()에서는 cpuset pressure 기록과 filesystem reclaim의 lockdep scope도 볼 수 있습니다. fs_reclaim_acquire()·fs_reclaim_release()를 실제 filesystem mutex를 얻고 놓는 동작으로 읽지는 않습니다. 이 code는 reclaim의 lock 의존성 검사를 위한 문맥을 표시합니다. reclaim 바깥과 안의 reschedule 지점, 이전 noreclaim 상태의 복구를 생략하지 않았습니다.

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

v6_12___perform_reclaim.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:3913-3936.
 * Complete named function; surrounding kernel context is required.
 * Source: https://github.com/torvalds/linux/blob/v6.12/mm/page_alloc.c#L3913-L3936
 * License text: GPL-2.0.txt (in this directory).
 */
static unsigned long
__perform_reclaim(gfp_t gfp_mask, unsigned int order,
					const struct alloc_context *ac)
{
	unsigned int noreclaim_flag;
	unsigned long progress;

	cond_resched();

	/* We now go into synchronous reclaim */
	cpuset_memory_pressure_bump();
	fs_reclaim_acquire(gfp_mask);
	noreclaim_flag = memalloc_noreclaim_save();

	progress = try_to_free_pages(ac->zonelist, order, gfp_mask,
								ac->nodemask);

	memalloc_noreclaim_restore(noreclaim_flag);
	fs_reclaim_release(gfp_mask);

	cond_resched();

	return progress;
}

scan_control이 원래 요청의 제약을 전달합니다

try_to_free_pages()는 struct scan_control을 만들고 allocation의 order·GFP mask·nodemask·최고 zone index를 전달합니다. nr_to_reclaim의 기본 batch는 SWAP_CLUSTER_MAX이며 설명하는 구현에서는 32 base pages입니다. 이것은 항상 32개의 folio를 free한다는 보장이 아닙니다. large folio는 여러 base page로 세고, 일부 후보는 회수에 실패합니다.

GFP mask도 caller가 전달한 정수값을 무조건 그대로 쓰지는 않습니다. current_gfp_context()로 task의 NOFS·NOIO 같은 제한을 반영합니다. 실제 pass를 수행하는 동안에는 set_task_reclaim_state()로 현재 task에 scan_control의 reclaim state를 연결하고, 반환 전에 그 연결을 해제합니다. 이 상태는 이번 회수의 accounting을 위한 임시 연결이며 task에 영구히 남기는 설정이 아닙니다.

unmap과 swap 허용 여부, dirty page를 writeback할 수 있는지도 설정합니다. laptop mode는 회전 disk를 불필요하게 깨우지 않도록 처음에는 writeback을 억제하는 정책이지만, 심한 pressure에서도 무조건 I/O를 금지한다는 뜻은 아닙니다. reclaim이 충분히 진행하지 못하면 뒤의 priority loop에서 이를 완화할 수 있습니다.

throttle_direct_reclaim()이 reserve 회복을 기다리는 상황도 있습니다. 그 결과는 다음 allocation 시도를 유도하는 제어 신호일 수 있으므로, 반환된 양을 실제 free된 page 수라고 무조건 해석해서는 안 됩니다.

특히 reserve를 기다리는 동안 fatal signal이 전달되면 이 helper가 중단을 알려 주고 try_to_free_pages()는 1을 반환할 수 있습니다. 한 page를 실제로 회수했다는 보고가 아니라, 이 지점에서 OOM kill로 밀어붙이지 않고 allocator가 다시 판단하도록 하는 신호입니다. 단순히 잠시 throttling했다는 사실만으로 이 조기 반환이 발생하는 것은 아닙니다.

아래는 v6.12의 try_to_free_pages() 전체입니다. scan_control의 작은 정수 field에 최대 order·priority·zone 값이 들어가는지 확인하는 BUILD_BUG_ON()과 task state의 연결·해제를 함께 보실 수 있습니다.

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

v6_12_try_to_free_pages.c

// SPDX-License-Identifier: GPL-2.0
/*
 *  Copyright (C) 1991, 1992, 1993, 1994  Linus Torvalds
 *
 *  Swap reorganised 29.12.95, Stephen Tweedie.
 *  kswapd added: 7.1.96  sct
 *  Removed kswapd_ctl limits, and swap out as many pages as needed
 *  to bring the system back to freepages.high: 2.4.97, Rik van Riel.
 *  Zone aware kswapd started 02/00, Kanoj Sarcar (kanoj@sgi.com).
 *  Multiqueue VM started 5.8.00, Rik van Riel.
 */

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

/* Extracted unchanged from Linux v6.12, mm/vmscan.c:6478-6519.
 * Complete named function; surrounding kernel context is required.
 * Source: https://github.com/torvalds/linux/blob/v6.12/mm/vmscan.c#L6478-L6519
 * License text: GPL-2.0.txt (in this directory).
 */
unsigned long try_to_free_pages(struct zonelist *zonelist, int order,
				gfp_t gfp_mask, nodemask_t *nodemask)
{
	unsigned long nr_reclaimed;
	struct scan_control sc = {
		.nr_to_reclaim = SWAP_CLUSTER_MAX,
		.gfp_mask = current_gfp_context(gfp_mask),
		.reclaim_idx = gfp_zone(gfp_mask),
		.order = order,
		.nodemask = nodemask,
		.priority = DEF_PRIORITY,
		.may_writepage = !laptop_mode,
		.may_unmap = 1,
		.may_swap = 1,
	};

	/*
	 * scan_control uses s8 fields for order, priority, and reclaim_idx.
	 * Confirm they are large enough for max values.
	 */
	BUILD_BUG_ON(MAX_PAGE_ORDER >= S8_MAX);
	BUILD_BUG_ON(DEF_PRIORITY > S8_MAX);
	BUILD_BUG_ON(MAX_NR_ZONES > S8_MAX);

	/*
	 * Do not enter reclaim if fatal signal was delivered while throttled.
	 * 1 is returned so that the page allocator does not OOM kill at this
	 * point.
	 */
	if (throttle_direct_reclaim(sc.gfp_mask, zonelist, nodemask))
		return 1;

	set_task_reclaim_state(current, &sc.reclaim_state);
	trace_mm_vmscan_direct_reclaim_begin(order, sc.gfp_mask);

	nr_reclaimed = do_try_to_free_pages(zonelist, &sc);

	trace_mm_vmscan_direct_reclaim_end(nr_reclaimed);
	set_task_reclaim_state(current, NULL);

	return nr_reclaimed;
}

Priority 숫자를 줄이면 검사 범위를 넓힙니다

do_try_to_free_pages()는 DEF_PRIORITY에서 시작해 더 적극적으로 scan합니다. 기본값 12는 list 크기를 12 bit right shift하는 기준, 즉 대략 1/4096 수준을 뜻합니다. 숫자가 내려갈수록 더 많이 검사하므로 scheduler의 task priority와 혼동하기 쉽습니다.

아래는 실행 가능한 kernel 구현이 아니라 direct reclaim의 개념적 호출 관계입니다.

__alloc_pages_direct_reclaim()
  __perform_reclaim()
    try_to_free_pages()
      do_try_to_free_pages()
        shrink_zones()
          shrink_node()

목표량을 회수하면 loop를 마칠 수 있습니다. compaction이 가능한 상태가 되면 compaction_ready를 통해 더 회수하기보다 allocator의 compaction을 시도하도록 돌아갑니다. higher-order 문제를 reclaim만으로 풀지 않는 것입니다. priority가 여러 단계 내려가도 어려움이 지속되면 laptop mode의 writeback 제한을 완화하여 선택지를 넓힙니다.

각 pass에서는 nr_scanned를 0으로 되돌리지만 nr_reclaimed는 이번 요청에서 누적합니다. laptop mode의 제한을 푸는 비교식은 priority < DEF_PRIORITY - 2입니다. 기본값 12라면 priority 9 이하에서 이 조건을 만나 may_writepage를 켤 수 있습니다. 이를 시작 직후부터 무조건 writeback한다거나, 정확히 세 번의 함수 호출 뒤에 I/O가 발생한다는 뜻으로 바꾸면 안 됩니다.

이 설정은 그 pass의 shrink_zones()가 끝난 뒤 이뤄집니다. 따라서 막 끝난 scan에 소급해 writeback을 허용하는 것이 아니라 다음 scan이 더 넓은 선택지를 갖도록 합니다.

Zone을 순회하되 reclaim은 node 단위로 합니다

shrink_zones()는 allocation이 사용할 수 있는 zone을 순회합니다. order와 free 상태상 compaction을 먼저 시도하는 것이 맞으면 해당 의도를 기록합니다. 실제 core reclaim은 shrink_node()에서 수행하므로 같은 node의 zone을 연달아 만날 때마다 node 전체를 중복 회수하지 않도록 합니다.

여기서 추가 회수를 건너뛰는 costly-order 검사는 allocator의 early compaction과 구분해야 합니다. 이 경로는 compaction 지원이 있고 sc->order > PAGE_ALLOC_COSTLY_ORDER이며 해당 zone이 compaction-ready일 때 sc->compaction_ready를 표시하고 다음 zone으로 갑니다. 모든 nonzero order에서 이 생략이 적용되는 것은 아닙니다.

일반적인 zonelist ordering에서는 node마다 한 차례 처리하지만, ordering이 달라진 경우 같은 node를 다시 방문할 여지는 있습니다. 이 구조를 system의 모든 node를 무조건 한 번씩 scan한다고 설명해서는 안 됩니다. 원래 allocation의 nodemask와 zone 제한은 계속 중요합니다. 적절한 지점에서는 consider_reclaim_throttle()로 진전 없는 scan을 잠시 조절합니다.

이 함수는 실제로 다룬 첫 node를 first_pgdat에 남기고, zone 순회를 마친 뒤 그 node를 기준으로 consider_reclaim_throttle()을 검사합니다. 모든 zone을 방문할 때마다 같은 throttle을 호출하는 구조는 아닙니다.

Pinned page에 대한 주석은 별도의 pin scan 구현이 아닙니다

고전 shrink_zones() 앞에는 pinned page가 많은 zone을 가볍게 scan하고 포기한다는 주석이 있습니다. 그러나 v6.12의 함수 본문에는 zone의 pin 수를 세어 특별한 light-scan 함수를 고르는 branch가 없습니다. 이 문장을 독립적인 pin-detection 알고리즘으로 읽어서는 안 됩니다. 실제 후보의 reference·mapping·writeback 조건과 회수 진전은 아래쪽 reclaim 함수에서 검사하고, 진전 없는 반복을 제한하는 정책은 여러 계층에 걸쳐 있습니다. pin된 folio가 섞였다고 그 zone 전체를 즉시 회수 불가로 확정하지도 않습니다.

아래는 v6.12의 함수 전체입니다. highmem의 buffer head가 lowmem을 붙잡는 특수 상황에서는 __GFP_HIGHMEM과 reclaim_idx를 넓혀 검사할 수 있으며, 끝에서 원래 GFP mask를 복구합니다. global reclaim에서는 cpuset hardwall과 cgroup v1 soft-limit reclaim도 적용합니다. 이 branch와 실제 folio의 pin·reference 판정은 다른 수준의 제약입니다.

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

v6_12_shrink_zones.c

// SPDX-License-Identifier: GPL-2.0
/*
 *  Copyright (C) 1991, 1992, 1993, 1994  Linus Torvalds
 *
 *  Swap reorganised 29.12.95, Stephen Tweedie.
 *  kswapd added: 7.1.96  sct
 *  Removed kswapd_ctl limits, and swap out as many pages as needed
 *  to bring the system back to freepages.high: 2.4.97, Rik van Riel.
 *  Zone aware kswapd started 02/00, Kanoj Sarcar (kanoj@sgi.com).
 *  Multiqueue VM started 5.8.00, Rik van Riel.
 */

/* Extracted unchanged from Linux v6.12, mm/vmscan.c:6122-6212.
 * Full function definition; surrounding kernel context is required.
 * Source: https://github.com/torvalds/linux/blob/v6.12/mm/vmscan.c#L6122
 * License text: GPL-2.0.txt (in this directory).
 */
static void shrink_zones(struct zonelist *zonelist, struct scan_control *sc)
{
	struct zoneref *z;
	struct zone *zone;
	unsigned long nr_soft_reclaimed;
	unsigned long nr_soft_scanned;
	gfp_t orig_mask;
	pg_data_t *last_pgdat = NULL;
	pg_data_t *first_pgdat = NULL;

	/*
	 * If the number of buffer_heads in the machine exceeds the maximum
	 * allowed level, force direct reclaim to scan the highmem zone as
	 * highmem pages could be pinning lowmem pages storing buffer_heads
	 */
	orig_mask = sc->gfp_mask;
	if (buffer_heads_over_limit) {
		sc->gfp_mask |= __GFP_HIGHMEM;
		sc->reclaim_idx = gfp_zone(sc->gfp_mask);
	}

	for_each_zone_zonelist_nodemask(zone, z, zonelist,
					sc->reclaim_idx, sc->nodemask) {
		/*
		 * Take care memory controller reclaiming has small influence
		 * to global LRU.
		 */
		if (!cgroup_reclaim(sc)) {
			if (!cpuset_zone_allowed(zone,
						 GFP_KERNEL | __GFP_HARDWALL))
				continue;

			/*
			 * If we already have plenty of memory free for
			 * compaction in this zone, don't free any more.
			 * Even though compaction is invoked for any
			 * non-zero order, only frequent costly order
			 * reclamation is disruptive enough to become a
			 * noticeable problem, like transparent huge
			 * page allocations.
			 */
			if (IS_ENABLED(CONFIG_COMPACTION) &&
			    sc->order > PAGE_ALLOC_COSTLY_ORDER &&
			    compaction_ready(zone, sc)) {
				sc->compaction_ready = true;
				continue;
			}

			/*
			 * Shrink each node in the zonelist once. If the
			 * zonelist is ordered by zone (not the default) then a
			 * node may be shrunk multiple times but in that case
			 * the user prefers lower zones being preserved.
			 */
			if (zone->zone_pgdat == last_pgdat)
				continue;

			/*
			 * This steals pages from memory cgroups over softlimit
			 * and returns the number of reclaimed pages and
			 * scanned pages. This works for global memory pressure
			 * and balancing, not for a memcg's limit.
			 */
			nr_soft_scanned = 0;
			nr_soft_reclaimed = memcg1_soft_limit_reclaim(zone->zone_pgdat,
								      sc->order, sc->gfp_mask,
								      &nr_soft_scanned);
			sc->nr_reclaimed += nr_soft_reclaimed;
			sc->nr_scanned += nr_soft_scanned;
			/* need some check for avoid more shrink_zone() */
		}

		if (!first_pgdat)
			first_pgdat = zone->zone_pgdat;

		/* See comment about same check for global reclaim above */
		if (zone->zone_pgdat == last_pgdat)
			continue;
		last_pgdat = zone->zone_pgdat;
		shrink_node(zone->zone_pgdat, sc);
	}

	if (first_pgdat)
		consider_reclaim_throttle(first_pgdat, sc);

	/*
	 * Restore to original mask to avoid the impact on the caller if we
	 * promoted it to __GFP_HIGHMEM.
	 */
	sc->gfp_mask = orig_mask;
}

종료 후에도 실패 이유를 구분합니다

reclaim pass 후에는 refault 관련 통계를 snapshot하여 다음 pass의 active/inactive 균형 판단에 활용합니다. 일부라도 회수했다면 그 진전을 반환할 수 있지만, compaction 때문에 중단한 경우와 전혀 회수할 수 없었던 경우는 다릅니다.

회수량이 0이어도 compaction-ready라면 do_try_to_free_pages()는 1을 반환할 수 있습니다. 이 역시 allocator가 compaction을 시도하게 하는 제어값이며 성공한 physical reclaim 한 장과 같지 않습니다. 목표 32장에 미달했더라도 실제 회수량이 양수라면 그 누적량을 먼저 반환합니다.

active list의 deactivation을 일부러 건너뛰어 inactive 후보가 부족했던 경우에는 force_deactivate로 다시 시도할 수 있습니다. 이때 skipped_deactivate는 앞선 pass에서 deactivation이 생략되었다는 기록이지, force 상태가 켜져 있으므로 생략했다는 뜻이 아닙니다. 조건을 바꾸어 한 번 더 시도하는 것과 같은 scan을 무작정 반복하는 것을 구분해야 합니다.

그 재시도에서는 priority를 시작값으로 복구하고 force_deactivate를 설정하며 skipped_deactivate 기록을 지운 뒤 다시 scan합니다. zone 제한이나 memory.low 보호 때문에 node 전체의 active/inactive 비율이 실제 회수 후보를 과대평가할 수 있기 때문입니다.

v6.12에도 이보다 앞서 memcg_full_walk를 켜는 재시도가 있습니다. 여러 direct reclaimer가 memcg iterator 상태를 공유하면 각 caller가 너무 적은 cgroup만 보고 진전이 없다고 판단할 수 있습니다. 실제 회수량과 compaction-ready 반환을 먼저 검사한 뒤, 아직 full walk가 아니면 priority를 복구해 전체 순회를 시도합니다. 그 뒤에 deactivation 보완이 오며, 그래도 진전이 없고 memcg_low_skipped가 남아 있으면 memory.low 보호를 완화하는 별도 pass를 시도합니다. 마지막 pass는 force_deactivate를 다시 끄고 memcg_low_reclaim을 켜며 skipped 기록을 지웁니다. 서로 다른 원인과 조건의 재시도를 한 개의 무조건 반복으로 합치지 않습니다.

아래 v6.12 함수 전체에는 retry 사이의 delay accounting과 node별 refault snapshot, cgroup congestion flag 정리도 포함되어 있습니다. 현대 구현의 이 조건들을 고전 설명에서 생략된 cgroup 내부 동작과 구분해 읽으시면 됩니다.

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

v6_12_do_try_to_free_pages.c

// SPDX-License-Identifier: GPL-2.0
/*
 *  Copyright (C) 1991, 1992, 1993, 1994  Linus Torvalds
 *
 *  Swap reorganised 29.12.95, Stephen Tweedie.
 *  kswapd added: 7.1.96  sct
 *  Removed kswapd_ctl limits, and swap out as many pages as needed
 *  to bring the system back to freepages.high: 2.4.97, Rik van Riel.
 *  Zone aware kswapd started 02/00, Kanoj Sarcar (kanoj@sgi.com).
 *  Multiqueue VM started 5.8.00, Rik van Riel.
 */

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

/* Extracted unchanged from Linux v6.12, mm/vmscan.c:6245-6347.
 * Complete named function; surrounding kernel context is required.
 * Source: https://github.com/torvalds/linux/blob/v6.12/mm/vmscan.c#L6245-L6347
 * License text: GPL-2.0.txt (in this directory).
 */
static unsigned long do_try_to_free_pages(struct zonelist *zonelist,
					  struct scan_control *sc)
{
	int initial_priority = sc->priority;
	pg_data_t *last_pgdat;
	struct zoneref *z;
	struct zone *zone;
retry:
	delayacct_freepages_start();

	if (!cgroup_reclaim(sc))
		__count_zid_vm_events(ALLOCSTALL, sc->reclaim_idx, 1);

	do {
		if (!sc->proactive)
			vmpressure_prio(sc->gfp_mask, sc->target_mem_cgroup,
					sc->priority);
		sc->nr_scanned = 0;
		shrink_zones(zonelist, sc);

		if (sc->nr_reclaimed >= sc->nr_to_reclaim)
			break;

		if (sc->compaction_ready)
			break;

		/*
		 * If we're getting trouble reclaiming, start doing
		 * writepage even in laptop mode.
		 */
		if (sc->priority < DEF_PRIORITY - 2)
			sc->may_writepage = 1;
	} while (--sc->priority >= 0);

	last_pgdat = NULL;
	for_each_zone_zonelist_nodemask(zone, z, zonelist, sc->reclaim_idx,
					sc->nodemask) {
		if (zone->zone_pgdat == last_pgdat)
			continue;
		last_pgdat = zone->zone_pgdat;

		snapshot_refaults(sc->target_mem_cgroup, zone->zone_pgdat);

		if (cgroup_reclaim(sc)) {
			struct lruvec *lruvec;

			lruvec = mem_cgroup_lruvec(sc->target_mem_cgroup,
						   zone->zone_pgdat);
			clear_bit(LRUVEC_CGROUP_CONGESTED, &lruvec->flags);
		}
	}

	delayacct_freepages_end();

	if (sc->nr_reclaimed)
		return sc->nr_reclaimed;

	/* Aborted reclaim to try compaction? don't OOM, then */
	if (sc->compaction_ready)
		return 1;

	/*
	 * In most cases, direct reclaimers can do partial walks
	 * through the cgroup tree to meet the reclaim goal while
	 * keeping latency low. Since the iterator state is shared
	 * among all direct reclaim invocations (to retain fairness
	 * among cgroups), though, high concurrency can result in
	 * individual threads not seeing enough cgroups to make
	 * meaningful forward progress. Avoid false OOMs in this case.
	 */
	if (!sc->memcg_full_walk) {
		sc->priority = initial_priority;
		sc->memcg_full_walk = 1;
		goto retry;
	}

	/*
	 * We make inactive:active ratio decisions based on the node's
	 * composition of memory, but a restrictive reclaim_idx or a
	 * memory.low cgroup setting can exempt large amounts of
	 * memory from reclaim. Neither of which are very common, so
	 * instead of doing costly eligibility calculations of the
	 * entire cgroup subtree up front, we assume the estimates are
	 * good, and retry with forcible deactivation if that fails.
	 */
	if (sc->skipped_deactivate) {
		sc->priority = initial_priority;
		sc->force_deactivate = 1;
		sc->skipped_deactivate = 0;
		goto retry;
	}

	/* Untapped cgroup reserves?  Don't OOM, retry. */
	if (sc->memcg_low_skipped) {
		sc->priority = initial_priority;
		sc->force_deactivate = 0;
		sc->memcg_low_reclaim = 1;
		sc->memcg_low_skipped = 0;
		goto retry;
	}

	return 0;
}

마지막으로 caller는 반환된 진전과 별개로 allocation을 재시도합니다. direct reclaim이 있었음을 관찰했다는 사실만으로 특정 application의 latency 전체를 설명할 수는 없습니다. compaction, scheduling, filesystem I/O, lock contention이 같은 요청에 함께 나타날 수 있습니다.

참고 자료

  • Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 11, Direct Reclaim.
  • Linux v6.12 mm/page_alloc.c: __alloc_pages_direct_reclaim(), __perform_reclaim().
  • Linux v6.18 mm/vmscan.c: try_to_free_pages(), do_try_to_free_pages(), shrink_zones().