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도 실제 선언 그대로 포함했습니다.
// 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도 확인하실 수 있습니다.
// 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에만 증가시킨 뒤 끝나는 함수가 아닙니다.
// 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
// 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()에서 정합니다.
// 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에서 전체 함수와 함께 이어서 살펴봅니다.
참고 자료
- Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 11, Scan Control, Working Set, Shrinking the Node.
- Linux v6.12 mm/vmscan.c, mm/swap.c: 이 글에 포함한 전체 선언·함수의 기준입니다.
- Linux v6.18 mm/vmscan.c:
scan_control,prepare_scan_control(),inactive_is_low(),shrink_node(). - Linux v6.18 mm/workingset.c: nonresident age, refault, cost accounting.