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
// 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 이하인 상황입니다. 호출 횟수를 세는 조건은 아닙니다.
// 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_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의 성공을 미리 보장하지는 않습니다.
참고 자료
- Linux v6.12 mm/vmscan.c: scan 준비, node reclaim, writeback feedback, compaction 연계입니다.
- Linux v6.18 mm/vmscan.c: 이후 version의 비교 기준입니다.
- Code provenance, GPL-2.0 전문.