Node reclaim은 왜 같은 folio를 더 빨리 돌기보다 기다릴까요

writeback이 끝나기도 전에 같은 folio를 다시 검사해도 RAM이 더 빨리 생기지는 않습니다. reclaim은 회수량뿐 아니라 격리한 후보가 무엇에 막혔는지 기록합니다. 그 결과는 다음 scan의 허용 작업, kswapd의 대기, direct reclaimer의 congestion 대기로 이어집니다. 단순한 LRU 순회보다 feedback loop에 가깝습니다.

이 글은 classic reclaim을 중심으로 Linux v6.12의 전체 함수와 조건을 읽습니다. 고전 구현의 LRUVEC_CONGESTED 하나를 v6.12의 node/cgroup 분리 flag와 같다고 취급하지 않습니다. MGLRU가 활성화되고 root reclaim인 경우에는 shrink_node()의 첫 branch에서 별도 generation 경로로 빠질 수 있습니다.

한 pass의 결과와 누적 결과를 나눕니다

shrink_node()는 대상 node·memcg의 lruvec을 찾고 again마다 sc->nr을 0으로 만듭니다. 반면 sc->nr_scanned·nr_reclaimed 전체를 여기서 0으로 지우지 않습니다. 시작값을 local variable에 저장한 뒤 차이를 구하여 이번 pass의 결과를 따로 평가합니다. reclaimable도 처음에 거짓이고, 어느 pass에서든 실제 회수가 있으면 참을 유지합니다.

scan 준비를 마치면 shrink_node_memcgs()가 lruvec과 slab reclaim을 수행합니다. v6.12는 flush_reclaim_state()로 reclaim accounting을 합칩니다. 고전 code에서 reclaimed_slab을 더하고 0으로 만드는 작업과 대응하지만 member·helper 이름은 동일하지 않습니다. proactive가 아닌 경우의 vmpressure accounting도 whole function에는 포함됩니다.

Writeback 중인 후보는 즉시 writeback할 수 없습니다

kswapd branch의 첫 조건은 nr.writeback != 0 && nr.writeback == nr.taken입니다. 검사한 격리 후보가 모두 이 writeback 범주라면 PGDAT_WRITEBACK을 세웁니다. 다음 folio 검사에서 writeback·reclaim 표시와 이 node 상태가 겹치면 immediate counter를 올릴 수 있습니다. 이름의 immediate는 진행 중인 I/O를 중복 발행하라는 명령이 아닙니다. 이 pass의 nr.immediate가 있으면 kswapd는 VMSCAN_THROTTLE_WRITEBACK으로 기다릴 수 있습니다.

nr.unqueued_dirty == nr.file_taken이면 PGDAT_DIRTY를 세웁니다. 이 code에는 별도의 nonzero guard가 없으므로 0==0도 식 자체는 참입니다. 그러므로 이 flag 하나를 관찰하고 '방금 모든 file folio가 dirty였다'고 단정하면 안 됩니다. 또한 flag는 뒤쪽 folio 처리의 조건 중 하나이지 GFP·filesystem·writepage 허용 조건을 모두 무시하는 강제 I/O 명령이 아닙니다.

이 feedback은 원래 dirtying task를 balance_dirty_pages()와 똑같은 방식으로 붙잡는 것도 아닙니다. reclaim하는 task와 memory를 dirty하게 만든 task는 다를 수 있습니다. backing 속도에 비해 list가 너무 빨리 순환한다는 신호와, 실제 I/O 완료를 구분해야 합니다.

Node와 cgroup congestion을 따로 기록합니다

v6.12는 nr.dirty != 0 && nr.dirty == nr.congested일 때만 congestion 표시를 검토합니다. sane writeback throttling을 사용하는 cgroup reclaim에는 LRUVEC_CGROUP_CONGESTED, kswapd에는 LRUVEC_NODE_CONGESTED를 세웁니다. 고전의 단일 LRUVEC_CONGESTED 조건을 그대로 가져오는 대신 이 두 범위를 분리해서 읽어야 합니다.

direct reclaim은 !current_is_kswapd(), current_may_throttle(), !hibernation_mode, 그리고 두 congestion flag 중 하나라는 조건을 함께 만족할 때 VMSCAN_THROTTLE_CONGESTED로 대기합니다. kswapd의 immediate-writeback 대기와 동일한 branch가 아닙니다. 또한 이 flag들이 있다고 pinned folio나 추가 reference가 저절로 사라지지 않습니다. folio lock·mapping·reference·dirty/writeback의 실제 회수 조건은 아래쪽 처리에 남습니다.

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

v6_12_shrink_node.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:5936-6047.
 * Full function definition; surrounding kernel context is required.
 * Source: https://github.com/torvalds/linux/blob/v6.12/mm/vmscan.c#L5936
 * License text: GPL-2.0.txt (in this directory).
 */
static void shrink_node(pg_data_t *pgdat, struct scan_control *sc)
{
	unsigned long nr_reclaimed, nr_scanned, nr_node_reclaimed;
	struct lruvec *target_lruvec;
	bool reclaimable = false;

	if (lru_gen_enabled() && root_reclaim(sc)) {
		lru_gen_shrink_node(pgdat, sc);
		return;
	}

	target_lruvec = mem_cgroup_lruvec(sc->target_mem_cgroup, pgdat);

again:
	memset(&sc->nr, 0, sizeof(sc->nr));

	nr_reclaimed = sc->nr_reclaimed;
	nr_scanned = sc->nr_scanned;

	prepare_scan_control(pgdat, sc);

	shrink_node_memcgs(pgdat, sc);

	flush_reclaim_state(sc);

	nr_node_reclaimed = sc->nr_reclaimed - nr_reclaimed;

	/* Record the subtree's reclaim efficiency */
	if (!sc->proactive)
		vmpressure(sc->gfp_mask, sc->target_mem_cgroup, true,
			   sc->nr_scanned - nr_scanned, nr_node_reclaimed);

	if (nr_node_reclaimed)
		reclaimable = true;

	if (current_is_kswapd()) {
		/*
		 * If reclaim is isolating dirty pages under writeback,
		 * it implies that the long-lived page allocation rate
		 * is exceeding the page laundering rate. Either the
		 * global limits are not being effective at throttling
		 * processes due to the page distribution throughout
		 * zones or there is heavy usage of a slow backing
		 * device. The only option is to throttle from reclaim
		 * context which is not ideal as there is no guarantee
		 * the dirtying process is throttled in the same way
		 * balance_dirty_pages() manages.
		 *
		 * Once a node is flagged PGDAT_WRITEBACK, kswapd will
		 * count the number of pages under pages flagged for
		 * immediate reclaim and stall if any are encountered
		 * in the nr_immediate check below.
		 */
		if (sc->nr.writeback && sc->nr.writeback == sc->nr.taken)
			set_bit(PGDAT_WRITEBACK, &pgdat->flags);

		/* Allow kswapd to start writing pages during reclaim.*/
		if (sc->nr.unqueued_dirty == sc->nr.file_taken)
			set_bit(PGDAT_DIRTY, &pgdat->flags);

		/*
		 * If kswapd scans pages marked for immediate
		 * reclaim and under writeback (nr_immediate), it
		 * implies that pages are cycling through the LRU
		 * faster than they are written so forcibly stall
		 * until some pages complete writeback.
		 */
		if (sc->nr.immediate)
			reclaim_throttle(pgdat, VMSCAN_THROTTLE_WRITEBACK);
	}

	/*
	 * Tag a node/memcg as congested if all the dirty pages were marked
	 * for writeback and immediate reclaim (counted in nr.congested).
	 *
	 * Legacy memcg will stall in page writeback so avoid forcibly
	 * stalling in reclaim_throttle().
	 */
	if (sc->nr.dirty && sc->nr.dirty == sc->nr.congested) {
		if (cgroup_reclaim(sc) && writeback_throttling_sane(sc))
			set_bit(LRUVEC_CGROUP_CONGESTED, &target_lruvec->flags);

		if (current_is_kswapd())
			set_bit(LRUVEC_NODE_CONGESTED, &target_lruvec->flags);
	}

	/*
	 * Stall direct reclaim for IO completions if the lruvec is
	 * node is congested. Allow kswapd to continue until it
	 * starts encountering unqueued dirty pages or cycling through
	 * the LRU too quickly.
	 */
	if (!current_is_kswapd() && current_may_throttle() &&
	    !sc->hibernation_mode &&
	    (test_bit(LRUVEC_CGROUP_CONGESTED, &target_lruvec->flags) ||
	     test_bit(LRUVEC_NODE_CONGESTED, &target_lruvec->flags)))
		reclaim_throttle(pgdat, VMSCAN_THROTTLE_CONGESTED);

	if (should_continue_reclaim(pgdat, nr_node_reclaimed, sc))
		goto again;

	/*
	 * Kswapd gives up on balancing particular nodes after too
	 * many failures to reclaim anything from them and goes to
	 * sleep. On reclaim progress, reset the failure counter. A
	 * successful direct reclaim run will revive a dormant kswapd.
	 */
	if (reclaimable)
		pgdat->kswapd_failures = 0;
	else if (sc->cache_trim_mode)
		sc->cache_trim_mode_failed = 1;
}

더 회수할지 compaction으로 돌아갈지

in_reclaim_compaction()은 compaction이 구성되어 있고 order가 0이 아니어야 합니다. 여기에 order > PAGE_ALLOC_COSTLY_ORDER이거나 priority < DEF_PRIORITY - 2가 필요합니다. 기본값에서는 order-4 이상이거나, nonzero order에서 priority 9 이하인 상황입니다. 호출 횟수를 세는 조건은 아닙니다.

v6.12 mode 판정 · 전체 code

v6_12_in_reclaim_compaction.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:5788-5796.
 * Full function definition; surrounding kernel context is required.
 * Source: https://github.com/torvalds/linux/blob/v6.12/mm/vmscan.c#L5788
 * License text: GPL-2.0.txt (in this directory).
 */
static bool in_reclaim_compaction(struct scan_control *sc)
{
	if (gfp_compaction_allowed(sc->gfp_mask) && sc->order &&
			(sc->order > PAGE_ALLOC_COSTLY_ORDER ||
			 sc->priority < DEF_PRIORITY - 2))
		return true;

	return false;
}

should_continue_reclaim()은 이 mode가 아니면 거짓이고, 이번 pass에서 실제 회수한 양이 0이어도 거짓입니다. scan량이 0이라는 이유만으로 전체 LRU를 다 검사했다고 간주하지 않습니다. 과거의 그런 근사는 계속 작은 scan이 일어나는 corner case를 처리하지 못했습니다. retry-mayfail 요청이라고 이 함수를 무조건 무한 반복시키지도 않습니다.

다음으로 적격 managed zone 중 하나라도 minimum watermark에서 allocation이 가능하거나 compaction이 suitable하면 allocator로 돌아갑니다. 고전 COMPACT_SUCCESS·COMPACT_CONTINUE enum switch와 달리 v6.12는 watermark 검사와 boolean compaction_suitable() 호출을 분리합니다. 마지막에는 inactive file 양과 회수 가능한 inactive anon 양의 합이 compact_gap(order)보다 엄격히 큰지 봅니다. equality는 계속 조건이 아닙니다. anon은 swap 가능성 또는 demotion 가능성 검사 없이 무조건 합산하지 않습니다.

v6.12 계속 여부 판정 · 전체 code

v6_12_should_continue_reclaim.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:5805-5855.
 * Full function definition; surrounding kernel context is required.
 * Source: https://github.com/torvalds/linux/blob/v6.12/mm/vmscan.c#L5805
 * License text: GPL-2.0.txt (in this directory).
 */
static inline bool should_continue_reclaim(struct pglist_data *pgdat,
					unsigned long nr_reclaimed,
					struct scan_control *sc)
{
	unsigned long pages_for_compaction;
	unsigned long inactive_lru_pages;
	int z;

	/* If not in reclaim/compaction mode, stop */
	if (!in_reclaim_compaction(sc))
		return false;

	/*
	 * Stop if we failed to reclaim any pages from the last SWAP_CLUSTER_MAX
	 * number of pages that were scanned. This will return to the caller
	 * with the risk reclaim/compaction and the resulting allocation attempt
	 * fails. In the past we have tried harder for __GFP_RETRY_MAYFAIL
	 * allocations through requiring that the full LRU list has been scanned
	 * first, by assuming that zero delta of sc->nr_scanned means full LRU
	 * scan, but that approximation was wrong, and there were corner cases
	 * where always a non-zero amount of pages were scanned.
	 */
	if (!nr_reclaimed)
		return false;

	/* If compaction would go ahead or the allocation would succeed, stop */
	for (z = 0; z <= sc->reclaim_idx; z++) {
		struct zone *zone = &pgdat->node_zones[z];
		if (!managed_zone(zone))
			continue;

		/* Allocation can already succeed, nothing to do */
		if (zone_watermark_ok(zone, sc->order, min_wmark_pages(zone),
				      sc->reclaim_idx, 0))
			return false;

		if (compaction_suitable(zone, sc->order, sc->reclaim_idx))
			return false;
	}

	/*
	 * If we have not reclaimed enough pages for compaction and the
	 * inactive lists are large enough, continue reclaiming
	 */
	pages_for_compaction = compact_gap(sc->order);
	inactive_lru_pages = node_page_state(pgdat, NR_INACTIVE_FILE);
	if (can_reclaim_anon_pages(NULL, pgdat->node_id, sc))
		inactive_lru_pages += node_page_state(pgdat, NR_INACTIVE_ANON);

	return inactive_lru_pages > pages_for_compaction;
}

계속한다면 again에서 이번 pass 통계를 새로 모읍니다. 종료할 때 어느 pass에서든 실제 회수가 있었다면 kswapd_failures를 0으로 돌립니다. 성공한 direct reclaim도 잠들어 있던 kswapd의 실패 이력을 회복할 수 있습니다. 진전이 없고 cache trim이었다면 trim 실패를 기록하여 caller가 정책을 바꿀 근거를 남깁니다. 어느 branch도 다음 allocation의 성공을 미리 보장하지는 않습니다.

참고 자료