Reclaim은 무엇을 기억할까요: scan_control과 working set

cache를 많이 버렸는데 곧바로 같은 data를 다시 읽는다면 free page 수만으로는 reclaim이 잘됐다고 할 수 없습니다. hot page를 계속 쫓아내는 thrashing일 수 있기 때문입니다. Linux는 한 번의 scan 결과와 여러 pass에 걸친 refault 정보를 서로 다른 장소에 보관합니다.

한 번의 회수 요청을 담는 scan_control

struct scan_control은 node에서 lruvec, 개별 list, folio로 내려가는 동안 원래 요청을 전달합니다. nr_to_reclaim은 목표 base page 수이고 order는 allocation의 order입니다. nodemask, reclaim_idx, gfp_mask는 어디서 어떤 제약으로 memory가 필요한지를 나타냅니다. memcg reclaim이라면 대상 cgroup도 필요합니다.

priority는 scan할 list 크기를 right shift하는 기준입니다. nr_scanned와 nr_reclaimed는 각각 검사와 회수의 진전을 누적합니다. 이 값은 folio 개수가 아니라 base page 단위로 읽어야 합니다. slab reclaim의 결과는 reclaim_state를 통해 별도로 합류할 수 있으므로 LRU에서 빠진 항목 수만 세어서는 전체 결과를 설명할 수 없습니다.

dirty·writeback 관련 세부 통계도 목적이 다릅니다. dirty이거나 writeback 중인 후보, 아직 writeback에 queue되지 않은 dirty 후보, writeback 중 다시 reclaim을 만난 후보, 격리한 file page와 전체 page를 구분합니다. 이를 비교해야 CPU가 scan을 못하는 상황인지, backing I/O가 따라오지 못하는 상황인지 알 수 있습니다.

counter의 type도 다릅니다. 목표·누적량·cost는 unsigned long, order·priority·reclaim_idx는 s8, nr 안의 세부 통계는 unsigned int입니다. gfp_mask는 gfp_t, nodemask는 nodemask_t *입니다. 이들의 폭이 모두 같다고 가정하는 외부 도구나 dump 해석은 잘못될 수 있습니다. nodemask == NULL은 별도 node mask를 걸지 않는다는 뜻이며, zone·memcg·GFP 제약까지 없어지는 뜻은 아닙니다.

세부 counter는 다음처럼 읽습니다. folio가 여러 base page를 포함할 수 있고, 검사 중 분기가 갈라지므로 단순 합계로 전체 folio 수를 복원할 수 있는 독립 범주들이 아닙니다.

Field 관찰하는 상태
nr.dirty dirty이거나 writeback 중인 후보입니다.
nr.unqueued_dirty dirty이면서 아직 writeback 중이 아닌 후보입니다.
nr.congested writeback과 reclaim 표시가 겹쳐 재순회 지연을 드러내는 후보입니다.
nr.writeback writeback 처리 branch에서 세는 양입니다. immediate branch와 구분합니다.
nr.immediate kswapd가 reclaim 표시와 node writeback 압력 상태를 가진 writeback 후보를 다시 만난 양입니다. 완료된 I/O 수가 아닙니다.
nr.file_taken LRU에서 isolate한 file-family base page 수입니다.
nr.taken isolate한 전체 base page 수입니다.

아래는 data field와 flag를 분리하지 않은 v6.12의 struct scan_control 전체 선언입니다. reclaim_state는 slab 등의 reclaim accounting을 위한 별도 구조이고, 위 nr의 각 항목과 같은 구조체는 아닙니다. cgroup·proactive reclaim·hibernation·demotion 관련 member도 실제 선언 그대로 포함했습니다.

v6.12 원본 · 전체 선언 · GPL-2.0

v6_12_scan_control.h

// 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:74-184.
 * Full struct declaration; surrounding kernel context is required.
 * Source: https://github.com/torvalds/linux/blob/v6.12/mm/vmscan.c#L74
 * License text: GPL-2.0.txt (in this directory).
 */
struct scan_control {
	/* How many pages shrink_list() should reclaim */
	unsigned long nr_to_reclaim;

	/*
	 * Nodemask of nodes allowed by the caller. If NULL, all nodes
	 * are scanned.
	 */
	nodemask_t	*nodemask;

	/*
	 * The memory cgroup that hit its limit and as a result is the
	 * primary target of this reclaim invocation.
	 */
	struct mem_cgroup *target_mem_cgroup;

	/*
	 * Scan pressure balancing between anon and file LRUs
	 */
	unsigned long	anon_cost;
	unsigned long	file_cost;

#ifdef CONFIG_MEMCG
	/* Swappiness value for proactive reclaim. Always use sc_swappiness()! */
	int *proactive_swappiness;
#endif

	/* Can active folios be deactivated as part of reclaim? */
#define DEACTIVATE_ANON 1
#define DEACTIVATE_FILE 2
	unsigned int may_deactivate:2;
	unsigned int force_deactivate:1;
	unsigned int skipped_deactivate:1;

	/* Writepage batching in laptop mode; RECLAIM_WRITE */
	unsigned int may_writepage:1;

	/* Can mapped folios be reclaimed? */
	unsigned int may_unmap:1;

	/* Can folios be swapped as part of reclaim? */
	unsigned int may_swap:1;

	/* Not allow cache_trim_mode to be turned on as part of reclaim? */
	unsigned int no_cache_trim_mode:1;

	/* Has cache_trim_mode failed at least once? */
	unsigned int cache_trim_mode_failed:1;

	/* Proactive reclaim invoked by userspace through memory.reclaim */
	unsigned int proactive:1;

	/*
	 * Cgroup memory below memory.low is protected as long as we
	 * don't threaten to OOM. If any cgroup is reclaimed at
	 * reduced force or passed over entirely due to its memory.low
	 * setting (memcg_low_skipped), and nothing is reclaimed as a
	 * result, then go back for one more cycle that reclaims the protected
	 * memory (memcg_low_reclaim) to avert OOM.
	 */
	unsigned int memcg_low_reclaim:1;
	unsigned int memcg_low_skipped:1;

	/* Shared cgroup tree walk failed, rescan the whole tree */
	unsigned int memcg_full_walk:1;

	unsigned int hibernation_mode:1;

	/* One of the zones is ready for compaction */
	unsigned int compaction_ready:1;

	/* There is easily reclaimable cold cache in the current node */
	unsigned int cache_trim_mode:1;

	/* The file folios on the current node are dangerously low */
	unsigned int file_is_tiny:1;

	/* Always discard instead of demoting to lower tier memory */
	unsigned int no_demotion:1;

	/* Allocation order */
	s8 order;

	/* Scan (total_size >> priority) pages at once */
	s8 priority;

	/* The highest zone to isolate folios for reclaim from */
	s8 reclaim_idx;

	/* This context's GFP mask */
	gfp_t gfp_mask;

	/* Incremented by the number of inactive pages that were scanned */
	unsigned long nr_scanned;

	/* Number of pages freed so far during a call to shrink_zones() */
	unsigned long nr_reclaimed;

	struct {
		unsigned int dirty;
		unsigned int unqueued_dirty;
		unsigned int congested;
		unsigned int writeback;
		unsigned int immediate;
		unsigned int file_taken;
		unsigned int taken;
	} nr;

	/* for recording the reclaimed slab by now */
	struct reclaim_state reclaim_state;
};

허용 작업과 관찰 결과를 섞지 않습니다

may_writepage, may_swap, may_unmap은 허용 작업입니다. may_deactivate는 anon/file active list를 inactive로 내릴 수 있는지를 나눠 나타냅니다. 반면 skipped_deactivate는 앞선 실행에서 그 작업을 생략했다는 결과입니다. 이 기록을 보고 caller가 force_deactivate를 켜 재시도할 수 있습니다.

compaction_ready는 reclaim을 계속하기보다 compaction으로 돌아가도 된다는 제어 신호입니다. cache_trim_mode는 충분한 inactive file cache를 먼저 줄일 때, file_is_tiny는 작은 file cache만 계속 괴롭히는 상황을 피할 때 사용합니다. field 하나를 독립적인 전역 정책으로 보면 안 됩니다. version에 따라 준비 작업이 shrink_node() 내부에 있거나 prepare_scan_control() 같은 helper로 분리되지만, 뒤의 folio 검사에 앞서 정책을 결정한다는 역할은 같습니다.

C bitfield의 폭은 comma 뒤가 아니라 colon 뒤에 적습니다. may_deactivate:2는 DEACTIVATE_ANON=1과 DEACTIVATE_FILE=2를 각각 나타내는 2 bit이고, force_deactivate:1·skipped_deactivate:1은 별도 상태입니다. force가 설정되어서 deactivation이 생략되었다는 뜻은 아닙니다. shrink_list()가 허용 bit가 없는 active list를 건너뛴 기록을 남기고, caller는 그 결과를 보고 force를 켤 수 있습니다.

Refault는 잘못 버린 page를 알려줍니다

working set은 현재 작업이 실질적으로 사용하는 memory입니다. reclaim 직후 같은 data가 반복해서 돌아온다면 그 집합 안의 page를 너무 빨리 버린 것일 수 있습니다. 반대로 workload 자체가 cache 전체보다 크다면 모든 refault를 막을 수는 없습니다.

refault distance는 wall-clock 시간도, 단순한 PTE fault 횟수도 아닙니다. eviction 시 기록한 nonresident age와 돌아왔을 때의 age 차이를 통해 cache의 상대적인 교체 거리를 추정합니다. Linux의 age accounting에는 eviction·activation 같은 사건과 base page 수가 관련됩니다. 따라서 정확히 '그동안 scan한 folio 수'라고 고정해서도 안 됩니다.

distance가 유효한 resident working-set 범위 안에 들어오면, 더 보호했다면 피할 수 있었던 eviction으로 판단하여 refault한 folio를 활성화할 수 있습니다. 그러면 기존 active list에서도 오래된 보호를 줄여 새 working set에 자리를 내줄 필요가 있습니다. active 보호가 영구적인 특권이 아닌 이유입니다.

Snapshot과 cost는 다른 질문에 답합니다

snapshot_refaults()는 anonymous/file refault activation의 현재 통계를 저장합니다. 다음 pass에서는 이전 snapshot 이후 변화가 있었는지 보아 deactivation 필요성을 판단합니다. 이 값은 process의 모든 fault를 그대로 저장한 이력표가 아닙니다.

PG_workingset은 folio가 active list에 있었던 이력과 연결됩니다. refault한 내용이 기존 working set에 속했는지 판단하고, 이미 hot했던 내용을 다시 쫓아내는 비용을 추정할 때 사용합니다. dirty page를 backing으로 내보내는 비용도 lru_note_cost() 계열의 비용 accounting에 반영됩니다.

anon/file cost는 어느 쪽을 더 많이 scan할지 정하는 상대적 입력입니다. 한쪽에서 refault 비용이 커지면 그쪽을 계속 공격하기보다 다른 쪽을 회수하는 방향으로 압력을 조절합니다. v6.12·v6.18의 cost accounting에는 I/O뿐 아니라 회수하지 못하고 회전시킨 page의 CPU 비용도 들어가며, nr_io * SWAP_CLUSTER_MAX + nr_rotated로 가중합니다. 누적값은 오래된 workload에 영원히 지배되지 않도록 감쇠합니다. 합계가 LRU 규모의 1/4을 넘으면 양쪽을 절반으로 줄여 최근 사건에 더 의미를 줍니다. v6.18의 해당 helper 이름은 lru_note_cost_unlock_irq()입니다.

snapshot은 대상 node·memcg의 WORKINGSET_ACTIVATE_ANON과 WORKINGSET_ACTIVATE_FILE을 각각 refaults[WORKINGSET_ANON]·refaults[WORKINGSET_FILE]에 저장합니다. MGLRU가 켜졌을 때에는 이 classic snapshot을 하지 않는 v6.12 early return도 확인하실 수 있습니다.

v6.12 snapshot 함수 · 전체 code

v6_12_snapshot_refaults.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:6214-6227.
 * Full function definition; surrounding kernel context is required.
 * Source: https://github.com/torvalds/linux/blob/v6.12/mm/vmscan.c#L6214
 * License text: GPL-2.0.txt (in this directory).
 */
static void snapshot_refaults(struct mem_cgroup *target_memcg, pg_data_t *pgdat)
{
	struct lruvec *target_lruvec;
	unsigned long refaults;

	if (lru_gen_enabled())
		return;

	target_lruvec = mem_cgroup_lruvec(target_memcg, pgdat);
	refaults = lruvec_page_state(target_lruvec, WORKINGSET_ACTIVATE_ANON);
	target_lruvec->refaults[WORKINGSET_ANON] = refaults;
	refaults = lruvec_page_state(target_lruvec, WORKINGSET_ACTIVATE_FILE);
	target_lruvec->refaults[WORKINGSET_FILE] = refaults;
}

고전의 3-argument lru_note_cost(lruvec, file, nr_pages)는 입력 page 수를 누적했습니다. 아래 v6.12의 4-argument 함수는 nr_io·nr_rotated를 받으며 cost 단위를 단순 refault page 수로 고정할 수 없습니다. IRQ를 끈 lruvec lock 안에서 해당 family의 cost를 더하고, 네 evictable list의 합계 크기를 기준으로 양쪽 cost를 함께 감쇠합니다. parent_lruvec()을 따라 상위 cgroup에도 사건을 전달합니다. leaf에만 증가시킨 뒤 끝나는 함수가 아닙니다.

v6.12 cost 함수 · 전체 code

v6_12_lru_note_cost.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 *  linux/mm/swap.c
 *
 *  Copyright (C) 1991, 1992, 1993, 1994  Linus Torvalds
 */

/*
 * This file contains the default values for the operation of the
 * Linux VM subsystem. Fine-tuning documentation can be found in
 * Documentation/admin-guide/sysctl/vm.rst.
 * Started 18.12.91
 * Swap aging added 23.2.95, Stephen Tweedie.
 * Buffermem limits added 12.3.98, Rik van Riel.
 */

/* Extracted unchanged from Linux v6.12, mm/swap.c:271-321.
 * Full function definition; surrounding kernel context is required.
 * Source: https://github.com/torvalds/linux/blob/v6.12/mm/swap.c#L271
 * License text: GPL-2.0.txt (in this directory).
 */
void lru_note_cost(struct lruvec *lruvec, bool file,
		   unsigned int nr_io, unsigned int nr_rotated)
{
	unsigned long cost;

	/*
	 * Reflect the relative cost of incurring IO and spending CPU
	 * time on rotations. This doesn't attempt to make a precise
	 * comparison, it just says: if reloads are about comparable
	 * between the LRU lists, or rotations are overwhelmingly
	 * different between them, adjust scan balance for CPU work.
	 */
	cost = nr_io * SWAP_CLUSTER_MAX + nr_rotated;

	do {
		unsigned long lrusize;

		/*
		 * Hold lruvec->lru_lock is safe here, since
		 * 1) The pinned lruvec in reclaim, or
		 * 2) From a pre-LRU page during refault (which also holds the
		 *    rcu lock, so would be safe even if the page was on the LRU
		 *    and could move simultaneously to a new lruvec).
		 */
		spin_lock_irq(&lruvec->lru_lock);
		/* Record cost event */
		if (file)
			lruvec->file_cost += cost;
		else
			lruvec->anon_cost += cost;

		/*
		 * Decay previous events
		 *
		 * Because workloads change over time (and to avoid
		 * overflow) we keep these statistics as a floating
		 * average, which ends up weighing recent refaults
		 * more than old ones.
		 */
		lrusize = lruvec_page_state(lruvec, NR_INACTIVE_ANON) +
			  lruvec_page_state(lruvec, NR_ACTIVE_ANON) +
			  lruvec_page_state(lruvec, NR_INACTIVE_FILE) +
			  lruvec_page_state(lruvec, NR_ACTIVE_FILE);

		if (lruvec->file_cost + lruvec->anon_cost > lrusize / 4) {
			lruvec->file_cost /= 2;
			lruvec->anon_cost /= 2;
		}
		spin_unlock_irq(&lruvec->lru_lock);
	} while ((lruvec = parent_lruvec(lruvec)));
}

Active와 inactive의 균형을 조정합니다

inactive가 너무 작으면 실제 회수 후보가 모자라므로 active를 aging해야 합니다. classic 경로의 inactive_is_low()는 active/inactive의 base page 수와 전체 크기에 따른 비율을 사용합니다. 작은 집합에는 단순한 균형이 적합하지만 큰 집합에는 active를 더 넉넉하게 유지하는 식입니다. GiB 크기에서 제곱근 기반 비율을 계산하는 구현을 단순히 항상 1:1이라고 설명하면 틀립니다.

v6.18에서는 active+inactive의 크기를 GiB 단위로 줄인 gb가 0이면 ratio를 1로, 아니면 int_sqrt(10 * gb)로 구합니다. inactive * ratio < active이면 inactive가 작다고 봅니다. 예를 들어 합계가 약 1 GiB인 경우 ratio는 정수 제곱근 3이므로 active와 inactive가 같은 수가 될 때까지 내리는 정책이 아닙니다. 이 검사는 anon/file family별로 적용합니다.

refault activation이 늘었다면 기존 active 내용이 낡고 새 working set이 형성되는 상황일 수 있으므로 deactivation을 허용합니다. inactive 부족도 별도의 허용 이유입니다. 강제 모드에서는 양쪽 deactivation을 허용하여 앞선 pass에서 후보를 충분히 공급하지 못한 문제를 다시 시도합니다.

cache trim은 이 판단의 반대편을 활용합니다. inactive file이 현재 priority에서 충분하고 file 쪽 thrashing 신호가 없다면 이를 먼저 줄이는 것이 저렴할 수 있습니다. 그러나 file+free를 모두 합해도 high watermark 목표를 채울 수 없는데 작은 file cache만 반복 회수하면 anon을 방치하는 문제가 생깁니다. file_is_tiny는 inactive anon이 충분한지와 deactivation 상태까지 고려하여 이런 경우 anon scan을 선택하도록 돕습니다.

ratio는 inactive의 전체 memory 점유율 그 자체가 아니라 active:inactive의 목표 비교값입니다. ratio 3은 대략 3:1, 즉 inactive가 해당 family의 약 1/4이 되는 비교점입니다. 10 MiB·100 MiB 집합에서는 ratio 1이므로 비교점은 각각 inactive 약 5 MiB·50 MiB입니다. 1 GiB·10 GiB·100 GiB에서는 ratio가 각각 3·10·31, 1 TiB·10 TiB에서는 101·320이 됩니다. ideal inactive 크기는 전체 크기를 ratio+1로 나눈 값으로, 예를 들어 1 TiB에서는 약 10.04 GiB입니다. 실제 크기는 page 단위 정수이고 함수는 부족 여부만 반환하므로 강제로 이 양을 맞추는 할당 명령은 아닙니다.

큰 list일수록 inactive의 절대량은 커지지만 전체에서의 비중은 작아집니다. file inactive가 너무 작으면 readahead된 data가 다시 참조되기도 전에 쫓겨나고, 너무 크면 보호할 working set의 active 자리가 줄어듭니다. 함수는 그 자리에서 page를 이동하지 않습니다.

v6.12 inactive 비율 검사 · 전체 code

v6_12_inactive_is_low.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:2228-2245.
 * Full function definition; surrounding kernel context is required.
 * Source: https://github.com/torvalds/linux/blob/v6.12/mm/vmscan.c#L2228
 * License text: GPL-2.0.txt (in this directory).
 */
static bool inactive_is_low(struct lruvec *lruvec, enum lru_list inactive_lru)
{
	enum lru_list active_lru = inactive_lru + LRU_ACTIVE;
	unsigned long inactive, active;
	unsigned long inactive_ratio;
	unsigned long gb;

	inactive = lruvec_page_state(lruvec, NR_LRU_BASE + inactive_lru);
	active = lruvec_page_state(lruvec, NR_LRU_BASE + active_lru);

	gb = (inactive + active) >> (30 - PAGE_SHIFT);
	if (gb)
		inactive_ratio = int_sqrt(10 * gb);
	else
		inactive_ratio = 1;

	return inactive * inactive_ratio < active;
}

Scan 준비의 조건을 식으로 읽습니다

v6.12에서는 이 준비를 prepare_scan_control()로 분리했습니다. MGLRU early return 뒤, 대상 lruvec의 stats를 rate-limited flush하고 lock 아래에서 anon/file cost를 복사합니다. force_deactivate가 아니면 현재 activation counter가 snapshot과 다른지, 또는 해당 inactive가 부족한지를 family별로 검사하여 허용 bit를 세우거나 지웁니다. 한 family에서 변화가 있었다고 양쪽을 무조건 deactivate하지 않습니다.

cache trim 조건의 file >> priority는 현재 priority에서 scan할 inactive file 양이 0이 아닌지를 봅니다. !DEACTIVATE_FILE과 v6.12의 !no_cache_trim_mode도 함께 필요합니다. 반대로 global reclaim의 tiny-file 판정은 active_file + inactive_file + free <= managed-zone high watermark 합계, anon deactivation 불허, 그리고 inactive_anon >> priority가 0이 아니라는 세 조건입니다. free page를 'free file page'라고 분류하는 식이 아니며, 둘 중 하나만 참이라고 tiny-file mode가 되지도 않습니다. mode 간 우선순위는 뒤의 get_scan_count()에서 정합니다.

v6.12 scan 준비 · 전체 code

v6_12_prepare_scan_control.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:2254-2360.
 * Full function definition; surrounding kernel context is required.
 * Source: https://github.com/torvalds/linux/blob/v6.12/mm/vmscan.c#L2254
 * License text: GPL-2.0.txt (in this directory).
 */
static void prepare_scan_control(pg_data_t *pgdat, struct scan_control *sc)
{
	unsigned long file;
	struct lruvec *target_lruvec;

	if (lru_gen_enabled())
		return;

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

	/*
	 * Flush the memory cgroup stats in rate-limited way as we don't need
	 * most accurate stats here. We may switch to regular stats flushing
	 * in the future once it is cheap enough.
	 */
	mem_cgroup_flush_stats_ratelimited(sc->target_mem_cgroup);

	/*
	 * Determine the scan balance between anon and file LRUs.
	 */
	spin_lock_irq(&target_lruvec->lru_lock);
	sc->anon_cost = target_lruvec->anon_cost;
	sc->file_cost = target_lruvec->file_cost;
	spin_unlock_irq(&target_lruvec->lru_lock);

	/*
	 * Target desirable inactive:active list ratios for the anon
	 * and file LRU lists.
	 */
	if (!sc->force_deactivate) {
		unsigned long refaults;

		/*
		 * When refaults are being observed, it means a new
		 * workingset is being established. Deactivate to get
		 * rid of any stale active pages quickly.
		 */
		refaults = lruvec_page_state(target_lruvec,
				WORKINGSET_ACTIVATE_ANON);
		if (refaults != target_lruvec->refaults[WORKINGSET_ANON] ||
			inactive_is_low(target_lruvec, LRU_INACTIVE_ANON))
			sc->may_deactivate |= DEACTIVATE_ANON;
		else
			sc->may_deactivate &= ~DEACTIVATE_ANON;

		refaults = lruvec_page_state(target_lruvec,
				WORKINGSET_ACTIVATE_FILE);
		if (refaults != target_lruvec->refaults[WORKINGSET_FILE] ||
		    inactive_is_low(target_lruvec, LRU_INACTIVE_FILE))
			sc->may_deactivate |= DEACTIVATE_FILE;
		else
			sc->may_deactivate &= ~DEACTIVATE_FILE;
	} else
		sc->may_deactivate = DEACTIVATE_ANON | DEACTIVATE_FILE;

	/*
	 * If we have plenty of inactive file pages that aren't
	 * thrashing, try to reclaim those first before touching
	 * anonymous pages.
	 */
	file = lruvec_page_state(target_lruvec, NR_INACTIVE_FILE);
	if (file >> sc->priority && !(sc->may_deactivate & DEACTIVATE_FILE) &&
	    !sc->no_cache_trim_mode)
		sc->cache_trim_mode = 1;
	else
		sc->cache_trim_mode = 0;

	/*
	 * Prevent the reclaimer from falling into the cache trap: as
	 * cache pages start out inactive, every cache fault will tip
	 * the scan balance towards the file LRU.  And as the file LRU
	 * shrinks, so does the window for rotation from references.
	 * This means we have a runaway feedback loop where a tiny
	 * thrashing file LRU becomes infinitely more attractive than
	 * anon pages.  Try to detect this based on file LRU size.
	 */
	if (!cgroup_reclaim(sc)) {
		unsigned long total_high_wmark = 0;
		unsigned long free, anon;
		int z;

		free = sum_zone_node_page_state(pgdat->node_id, NR_FREE_PAGES);
		file = node_page_state(pgdat, NR_ACTIVE_FILE) +
			   node_page_state(pgdat, NR_INACTIVE_FILE);

		for (z = 0; z < MAX_NR_ZONES; z++) {
			struct zone *zone = &pgdat->node_zones[z];

			if (!managed_zone(zone))
				continue;

			total_high_wmark += high_wmark_pages(zone);
		}

		/*
		 * Consider anon: if that's low too, this isn't a
		 * runaway file reclaim problem, but rather just
		 * extreme pressure. Reclaim as per usual then.
		 */
		anon = node_page_state(pgdat, NR_INACTIVE_ANON);

		sc->file_is_tiny =
			file + free <= total_high_wmark &&
			!(sc->may_deactivate & DEACTIVATE_ANON) &&
			anon >> sc->priority;
	}
}

Node 결과는 다음 pass의 행동을 바꿉니다

shrink_node_memcgs()와 slab reclaim을 수행한 뒤에는 이번 pass의 회수량과 dirty·writeback 통계를 평가합니다. kswapd에서는 nr.writeback이 0이 아니고 전체 nr.taken과 같을 때 PGDAT_WRITEBACK을 세웁니다. nr.unqueued_dirty == nr.file_taken 비교는 PGDAT_DIRTY와 연결됩니다. 이 두 비교의 대상과 nonzero guard가 같지는 않습니다. writeback을 다시 만난 후보가 있으면 별도의 immediate counter를 통해 reclaim throttle도 고려합니다.

이 상태들은 folio를 즉시 free하라는 명령이 아닙니다. backing이 따라오지 않는 상황에서 같은 list를 CPU로 더 빨리 돌지 않도록 하는 feedback입니다. direct reclaim과 kswapd의 대기 조건이 다른 것도 caller의 진행 책임이 다르기 때문입니다.

higher-order reclaim/compaction에서는 should_continue_reclaim()이 추가 회수의 의미를 따집니다. 관련 mode가 아니거나 이번 batch에서 아무것도 회수하지 못했다면 멈출 수 있습니다. 이미 allocation·compaction이 가능한 zone이 있다면 allocator로 돌아가고, 그렇지 않더라도 compact_gap(order)를 채울 inactive 후보가 충분할 때에만 더 회수할 이유가 있습니다. inactive anon도 swap 또는 가능한 demotion 조건 없이 무조건 회수 가능량으로 더해서는 안 됩니다.

구체적인 equality 조건, congestion flag의 version 차이, compaction으로 돌아가는 판단은 Node reclaim feedback에서 전체 함수와 함께 이어서 살펴봅니다.

참고 자료