reclaim은 shared LRU lock을 잡은 채 disk I/O와 page-table 검사를 끝까지 수행하지 않습니다. 먼저 후보를 local list로 격리하고, 비싼 검사는 lock 밖에서 수행합니다. 이 isolation과 folio의 data lock, reference ownership을 나눠 보아야 동시성 처리가 이해됩니다. 고전적 이름인 isolate_lru_pages()에 대응하는 v6.12·v6.18 helper는 isolate_lru_folios()입니다. 아래에서는 같은 격리 알고리즘의 역할을 따라 설명합니다.
Active shrinking은 보호를 한 단계 낮춥니다
active list는 영구 보관함이 아닙니다. inactive가 작아지거나 refault 양상이 바뀌면 active의 일부를 내려 다음 회수 후보를 만듭니다. anon은 file보다 scan 기회가 적을 수 있으므로 kswapd와 lruvec 끝부분에서도 별도로 aging을 챙깁니다.
shrink_active_list()는 먼저 pending LRU add batch를 drain합니다. 아직 per-CPU batch에만 있는 folio까지 shared list 상태에 반영한 뒤 scan할 필요가 있기 때문입니다. lruvec lock을 잡고 후보를 l_hold에 isolate한 다음 lock을 신속히 놓습니다. 여기서 scanned와 실제 taken 수는 다르며 large folio는 base page 수로 반영됩니다.
Folio마다 다시 상태를 확인합니다
local list에 가져왔다고 evictable 상태가 영원히 고정되지는 않습니다. mlock되었거나 mapping 자체가 unevictable이면 일반 inactive 후보로 만들지 않고 적절히 putback합니다. isolation reference도 그 경로에서 정리해야 합니다.
사용 흔적이 있는 executable file folio는 active에 남겨 code를 보호할 수 있습니다. 다만 이번 관찰에서 보호한다는 뜻이지 앞으로 무조건 영원히 active라는 보장은 아닙니다. 나머지는 PG_active를 지우고 active 이력인 PG_workingset을 남겨 inactive 쪽 local list로 옮깁니다.
결과를 shared LRU에 반영할 때에는 다시 lruvec lock이 필요합니다. isolation을 위해 추가했던 reference를 내리면서 마지막 reference가 사라지는 folio도 있을 수 있습니다. 그런 항목은 LRU에 억지로 되돌리지 않고 free용 list로 모아 allocator에 넘깁니다. active aging의 주목적은 deactivation이지만 이 수명 정리까지 고려해야 합니다.
isolate_lru_pages는 tail에서 후보를 읽습니다
classic LRU는 일반적으로 head에 새 후보를 넣고 tail에서 오래된 후보를 읽습니다. isolate_lru_pages()는 caller가 lruvec lock을 가지고 있다는 조건에서 source list의 tail을 반복 검사합니다. target budget에 포함할 scan 수와 실제로 마주친 전체 scan 수를 나누는 이유는 부적격 후보만 만나다 아무것도 하지 못하는 일을 줄이기 위해서입니다.
예를 들어 실패한 allocation이 사용할 수 있는 최고 zone보다 높은 zone의 folio는 회수해도 요청에 도움이 되지 않습니다. 이런 skipped 항목은 따로 모으고 나중에 source list의 head 쪽으로 돌려 같은 batch 동안 tail에서 계속 재검사하지 않도록 합니다. scanned·skipped·isolated는 서로 바꿔 쓸 수 있는 counter가 아닙니다.
unmap이 금지된 reclaim에서 mapped folio를 발견한 경우에도 무리하게 가져오지 않습니다. 이미 LRU 상태가 바뀌었거나 안정적인 reference를 얻지 못한 경우 역시 보존합니다. 매번 '검사했으니 격리했다'고 계산하면 실제 scan 비용을 과소평가합니다.
Reference 획득은 folio lock 획득이 아닙니다
folio_try_get()은 죽어 가는 folio를 되살리지 않고 reference를 추가하려는 시도입니다. folio lock을 trylock하는 함수가 아닙니다. 이것이 성공했다고 folio의 내용이나 mapping을 마음대로 바꿀 수는 없습니다.
reference 확보 뒤에는 PG_lru를 원자적으로 test-and-clear하여 다른 isolator와 ownership을 경합합니다. 이미 다른 경로가 가져갔다면 방금 얻은 reference를 돌려놓고 진행하지 않습니다. list lock과 refcount와 LRU state bit가 각자 다른 문제를 해결하는 구조입니다.
성공한 folio는 caller의 destination list로 옮기고 taken 수를 늘립니다. 종료 시 skipped list를 복귀시키고 전체 scanned 수는 output parameter로, taken 수는 반환값으로 전달합니다. 이 두 결과를 구분해야 reclaim 효율을 올바르게 계산할 수 있습니다.
Local list를 원래 세계로 돌려놓습니다
move_folios_to_lru()의 복귀 처리는 각 folio의 현재 상태에 맞는 family를 다시 고릅니다. 옛 이름은 move_pages_to_lru()입니다. evictable 여부가 바뀌었으면 일반 list에 강제로 넣지 않고 putback helper로 적절히 처리합니다. PG_lru, list 연결, accounting을 함께 되살리고 isolation reference를 내립니다.
이 과정에서 마지막 reference가 사라지면 object를 list에 남겨서는 안 됩니다. v6.0의 move_pages_to_lru()는 free할 항목을 입력 list에 모아 caller에게 넘기지만, v6.18의 move_folios_to_lru()는 내부 free_folios batch를 직접 비웁니다. 현대 helper는 이때 lruvec lock을 잠시 놓고 uncharge·free한 뒤 다시 잡으며, 반환 시에는 caller가 요구한 lock 상태를 복구합니다. 반대로 외부 pin이나 mapping reference가 남아 있다면 isolation이 끝났어도 physical memory는 유지됩니다. list에서 빠짐, PTE가 사라짐, cache entry가 사라짐, 최종 folio free는 각각 다른 사건입니다.
복귀 순서는 다른 CPU의 마지막 put과도 맞아야 합니다
PG_lru는 isolation reference를 내리기 전에 설정해야 합니다. 순서를 바꾸면 다른 CPU의 마지막 put이 non-LRU folio로 판단하여 lruvec lock을 생략하는 동안, 이 CPU가 이미 해제된 folio를 list에 연결할 수 있습니다. 단순히 같은 최종 flag 값만 만들면 되는 문제가 아니라 상대 CPU가 어느 ownership protocol을 따르도록 할 것인지의 문제입니다.
마지막 reference가 남아 있으면 현재 active·inactive family에 다시 연결하고 base page 수를 nr_moved에 더합니다. 이미 마지막 reference가 사라진 항목은 이 값에 포함하지 않습니다. active list로 복귀한 항목은 nonresident age도 갱신할 수 있습니다. nr_activate·nr_deactivate는 이렇게 돌아온 양을 세는 값이지 이번에 새로 allocate한 양이 아닙니다.
active aging에서 executable 보호를 결정하는 reference 검사는 folio_referenced(..., 0, ...) != 0입니다. folio data lock을 가진 검사라는 뜻이 아니며, rmap 경합의 -1도 이 검사에서는 0이 아닌 결과입니다. 이 경우에도 file LRU와 VM_EXEC 조건을 함께 확인하므로 anonymous executable mapping을 같은 이유로 영구 보호하지는 않습니다. buffer head 사용량이 한도를 넘으면 folio trylock 아래 private metadata release를 시도하는 보조 경로도 있습니다.
v6.18 isolation에는 높은 zone의 skipped folio를 연속해서 만났을 때 SWAP_CLUSTER_MAX_SKIPPED로 skip 우회를 제한하는 장치도 있습니다. 앞의 설명처럼 높은 zone을 무한히 budget 밖으로 돌리기만 하는 것은 아닙니다. 이 한도 이후에는 scan budget이 진행되므로 lru_lock을 쥔 긴 loop를 제한할 수 있습니다.
전체 code로 ownership과 종료 경로를 확인합니다
고전 v6.0에서는 반환 list를 caller가 free하고, v6.18에서는 복귀 helper 내부가 free batch를 처리합니다. 같은 역할의 함수라도 lock을 잠시 놓는 위치와 caller에게 남기는 list의 의미를 비교해 보시면 좋습니다.
v6.0 shrink_active_list
// 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.0, mm/vmscan.c:2509-2604.
* Complete function definition; surrounding kernel context is required.
* Source: https://github.com/torvalds/linux/blob/v6.0/mm/vmscan.c#L2509
* License text: GPL-2.0.txt (in this directory).
*/
static void shrink_active_list(unsigned long nr_to_scan,
struct lruvec *lruvec,
struct scan_control *sc,
enum lru_list lru)
{
unsigned long nr_taken;
unsigned long nr_scanned;
unsigned long vm_flags;
LIST_HEAD(l_hold); /* The folios which were snipped off */
LIST_HEAD(l_active);
LIST_HEAD(l_inactive);
unsigned nr_deactivate, nr_activate;
unsigned nr_rotated = 0;
int file = is_file_lru(lru);
struct pglist_data *pgdat = lruvec_pgdat(lruvec);
lru_add_drain();
spin_lock_irq(&lruvec->lru_lock);
nr_taken = isolate_lru_pages(nr_to_scan, lruvec, &l_hold,
&nr_scanned, sc, lru);
__mod_node_page_state(pgdat, NR_ISOLATED_ANON + file, nr_taken);
if (!cgroup_reclaim(sc))
__count_vm_events(PGREFILL, nr_scanned);
__count_memcg_events(lruvec_memcg(lruvec), PGREFILL, nr_scanned);
spin_unlock_irq(&lruvec->lru_lock);
while (!list_empty(&l_hold)) {
struct folio *folio;
cond_resched();
folio = lru_to_folio(&l_hold);
list_del(&folio->lru);
if (unlikely(!folio_evictable(folio))) {
folio_putback_lru(folio);
continue;
}
if (unlikely(buffer_heads_over_limit)) {
if (folio_test_private(folio) && folio_trylock(folio)) {
if (folio_test_private(folio))
filemap_release_folio(folio, 0);
folio_unlock(folio);
}
}
/* Referenced or rmap lock contention: rotate */
if (folio_referenced(folio, 0, sc->target_mem_cgroup,
&vm_flags) != 0) {
/*
* Identify referenced, file-backed active folios and
* give them one more trip around the active list. So
* that executable code get better chances to stay in
* memory under moderate memory pressure. Anon folios
* are not likely to be evicted by use-once streaming
* IO, plus JVM can create lots of anon VM_EXEC folios,
* so we ignore them here.
*/
if ((vm_flags & VM_EXEC) && folio_is_file_lru(folio)) {
nr_rotated += folio_nr_pages(folio);
list_add(&folio->lru, &l_active);
continue;
}
}
folio_clear_active(folio); /* we are de-activating */
folio_set_workingset(folio);
list_add(&folio->lru, &l_inactive);
}
/*
* Move folios back to the lru list.
*/
spin_lock_irq(&lruvec->lru_lock);
nr_activate = move_pages_to_lru(lruvec, &l_active);
nr_deactivate = move_pages_to_lru(lruvec, &l_inactive);
/* Keep all free folios in l_active list */
list_splice(&l_inactive, &l_active);
__count_vm_events(PGDEACTIVATE, nr_deactivate);
__count_memcg_events(lruvec_memcg(lruvec), PGDEACTIVATE, nr_deactivate);
__mod_node_page_state(pgdat, NR_ISOLATED_ANON + file, -nr_taken);
spin_unlock_irq(&lruvec->lru_lock);
mem_cgroup_uncharge_list(&l_active);
free_unref_page_list(&l_active);
trace_mm_vmscan_lru_shrink_active(pgdat->node_id, nr_taken, nr_activate,
nr_deactivate, nr_rotated, sc->priority, file);
}
v6.0 isolate_lru_pages
// 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.0, mm/vmscan.c:2138-2229.
* Complete function definition; surrounding kernel context is required.
* Source: https://github.com/torvalds/linux/blob/v6.0/mm/vmscan.c#L2138
* License text: GPL-2.0.txt (in this directory).
*/
static unsigned long isolate_lru_pages(unsigned long nr_to_scan,
struct lruvec *lruvec, struct list_head *dst,
unsigned long *nr_scanned, struct scan_control *sc,
enum lru_list lru)
{
struct list_head *src = &lruvec->lists[lru];
unsigned long nr_taken = 0;
unsigned long nr_zone_taken[MAX_NR_ZONES] = { 0 };
unsigned long nr_skipped[MAX_NR_ZONES] = { 0, };
unsigned long skipped = 0;
unsigned long scan, total_scan, nr_pages;
LIST_HEAD(folios_skipped);
total_scan = 0;
scan = 0;
while (scan < nr_to_scan && !list_empty(src)) {
struct list_head *move_to = src;
struct folio *folio;
folio = lru_to_folio(src);
prefetchw_prev_lru_folio(folio, src, flags);
nr_pages = folio_nr_pages(folio);
total_scan += nr_pages;
if (folio_zonenum(folio) > sc->reclaim_idx) {
nr_skipped[folio_zonenum(folio)] += nr_pages;
move_to = &folios_skipped;
goto move;
}
/*
* Do not count skipped folios because that makes the function
* return with no isolated folios if the LRU mostly contains
* ineligible folios. This causes the VM to not reclaim any
* folios, triggering a premature OOM.
* Account all pages in a folio.
*/
scan += nr_pages;
if (!folio_test_lru(folio))
goto move;
if (!sc->may_unmap && folio_mapped(folio))
goto move;
/*
* Be careful not to clear the lru flag until after we're
* sure the folio is not being freed elsewhere -- the
* folio release code relies on it.
*/
if (unlikely(!folio_try_get(folio)))
goto move;
if (!folio_test_clear_lru(folio)) {
/* Another thread is already isolating this folio */
folio_put(folio);
goto move;
}
nr_taken += nr_pages;
nr_zone_taken[folio_zonenum(folio)] += nr_pages;
move_to = dst;
move:
list_move(&folio->lru, move_to);
}
/*
* Splice any skipped folios to the start of the LRU list. Note that
* this disrupts the LRU order when reclaiming for lower zones but
* we cannot splice to the tail. If we did then the SWAP_CLUSTER_MAX
* scanning would soon rescan the same folios to skip and waste lots
* of cpu cycles.
*/
if (!list_empty(&folios_skipped)) {
int zid;
list_splice(&folios_skipped, src);
for (zid = 0; zid < MAX_NR_ZONES; zid++) {
if (!nr_skipped[zid])
continue;
__count_zid_vm_events(PGSCAN_SKIP, zid, nr_skipped[zid]);
skipped += nr_skipped[zid];
}
}
*nr_scanned = total_scan;
trace_mm_vmscan_lru_isolate(sc->reclaim_idx, sc->order, nr_to_scan,
total_scan, skipped, nr_taken,
sc->may_unmap ? 0 : ISOLATE_UNMAPPED, lru);
update_lru_sizes(lruvec, lru, nr_zone_taken);
return nr_taken;
}
v6.0 move_pages_to_lru
// 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.0, mm/vmscan.c:2323-2385.
* Complete function definition; surrounding kernel context is required.
* Source: https://github.com/torvalds/linux/blob/v6.0/mm/vmscan.c#L2323
* License text: GPL-2.0.txt (in this directory).
*/
static unsigned int move_pages_to_lru(struct lruvec *lruvec,
struct list_head *list)
{
int nr_pages, nr_moved = 0;
LIST_HEAD(folios_to_free);
while (!list_empty(list)) {
struct folio *folio = lru_to_folio(list);
VM_BUG_ON_FOLIO(folio_test_lru(folio), folio);
list_del(&folio->lru);
if (unlikely(!folio_evictable(folio))) {
spin_unlock_irq(&lruvec->lru_lock);
folio_putback_lru(folio);
spin_lock_irq(&lruvec->lru_lock);
continue;
}
/*
* The folio_set_lru needs to be kept here for list integrity.
* Otherwise:
* #0 move_pages_to_lru #1 release_pages
* if (!folio_put_testzero())
* if (folio_put_testzero())
* !lru //skip lru_lock
* folio_set_lru()
* list_add(&folio->lru,)
* list_add(&folio->lru,)
*/
folio_set_lru(folio);
if (unlikely(folio_put_testzero(folio))) {
__folio_clear_lru_flags(folio);
if (unlikely(folio_test_large(folio))) {
spin_unlock_irq(&lruvec->lru_lock);
destroy_large_folio(folio);
spin_lock_irq(&lruvec->lru_lock);
} else
list_add(&folio->lru, &folios_to_free);
continue;
}
/*
* All pages were isolated from the same lruvec (and isolation
* inhibits memcg migration).
*/
VM_BUG_ON_FOLIO(!folio_matches_lruvec(folio, lruvec), folio);
lruvec_add_folio(lruvec, folio);
nr_pages = folio_nr_pages(folio);
nr_moved += nr_pages;
if (folio_test_active(folio))
workingset_age_nonresident(lruvec, nr_pages);
}
/*
* To save our caller's stack, now use input list for pages to free.
*/
list_splice(&folios_to_free, list);
return nr_moved;
}
v6.18 shrink_active_list
// 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.18, mm/vmscan.c:2126-2217.
* Complete function definition; surrounding kernel context is required.
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/vmscan.c#L2126
* License text: GPL-2.0.txt (in this directory).
*/
static void shrink_active_list(unsigned long nr_to_scan,
struct lruvec *lruvec,
struct scan_control *sc,
enum lru_list lru)
{
unsigned long nr_taken;
unsigned long nr_scanned;
vm_flags_t vm_flags;
LIST_HEAD(l_hold); /* The folios which were snipped off */
LIST_HEAD(l_active);
LIST_HEAD(l_inactive);
unsigned nr_deactivate, nr_activate;
unsigned nr_rotated = 0;
bool file = is_file_lru(lru);
struct pglist_data *pgdat = lruvec_pgdat(lruvec);
lru_add_drain();
spin_lock_irq(&lruvec->lru_lock);
nr_taken = isolate_lru_folios(nr_to_scan, lruvec, &l_hold,
&nr_scanned, sc, lru);
__mod_node_page_state(pgdat, NR_ISOLATED_ANON + file, nr_taken);
if (!cgroup_reclaim(sc))
__count_vm_events(PGREFILL, nr_scanned);
count_memcg_events(lruvec_memcg(lruvec), PGREFILL, nr_scanned);
spin_unlock_irq(&lruvec->lru_lock);
while (!list_empty(&l_hold)) {
struct folio *folio;
cond_resched();
folio = lru_to_folio(&l_hold);
list_del(&folio->lru);
if (unlikely(!folio_evictable(folio))) {
folio_putback_lru(folio);
continue;
}
if (unlikely(buffer_heads_over_limit)) {
if (folio_needs_release(folio) &&
folio_trylock(folio)) {
filemap_release_folio(folio, 0);
folio_unlock(folio);
}
}
/* Referenced or rmap lock contention: rotate */
if (folio_referenced(folio, 0, sc->target_mem_cgroup,
&vm_flags) != 0) {
/*
* Identify referenced, file-backed active folios and
* give them one more trip around the active list. So
* that executable code get better chances to stay in
* memory under moderate memory pressure. Anon folios
* are not likely to be evicted by use-once streaming
* IO, plus JVM can create lots of anon VM_EXEC folios,
* so we ignore them here.
*/
if ((vm_flags & VM_EXEC) && folio_is_file_lru(folio)) {
nr_rotated += folio_nr_pages(folio);
list_add(&folio->lru, &l_active);
continue;
}
}
folio_clear_active(folio); /* we are de-activating */
folio_set_workingset(folio);
list_add(&folio->lru, &l_inactive);
}
/*
* Move folios back to the lru list.
*/
spin_lock_irq(&lruvec->lru_lock);
nr_activate = move_folios_to_lru(lruvec, &l_active);
nr_deactivate = move_folios_to_lru(lruvec, &l_inactive);
__count_vm_events(PGDEACTIVATE, nr_deactivate);
count_memcg_events(lruvec_memcg(lruvec), PGDEACTIVATE, nr_deactivate);
__mod_node_page_state(pgdat, NR_ISOLATED_ANON + file, -nr_taken);
lru_note_cost_unlock_irq(lruvec, file, 0, nr_rotated);
trace_mm_vmscan_lru_shrink_active(pgdat->node_id, nr_taken, nr_activate,
nr_deactivate, nr_rotated, sc->priority, file);
}
v6.18 isolate_lru_folios
// 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.18, mm/vmscan.c:1738-1830.
* Complete function definition; surrounding kernel context is required.
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/vmscan.c#L1738
* License text: GPL-2.0.txt (in this directory).
*/
static unsigned long isolate_lru_folios(unsigned long nr_to_scan,
struct lruvec *lruvec, struct list_head *dst,
unsigned long *nr_scanned, struct scan_control *sc,
enum lru_list lru)
{
struct list_head *src = &lruvec->lists[lru];
unsigned long nr_taken = 0;
unsigned long nr_zone_taken[MAX_NR_ZONES] = { 0 };
unsigned long nr_skipped[MAX_NR_ZONES] = { 0, };
unsigned long skipped = 0, total_scan = 0, scan = 0;
unsigned long nr_pages;
unsigned long max_nr_skipped = 0;
LIST_HEAD(folios_skipped);
while (scan < nr_to_scan && !list_empty(src)) {
struct list_head *move_to = src;
struct folio *folio;
folio = lru_to_folio(src);
prefetchw_prev_lru_folio(folio, src, flags);
nr_pages = folio_nr_pages(folio);
total_scan += nr_pages;
/* Using max_nr_skipped to prevent hard LOCKUP*/
if (max_nr_skipped < SWAP_CLUSTER_MAX_SKIPPED &&
(folio_zonenum(folio) > sc->reclaim_idx)) {
nr_skipped[folio_zonenum(folio)] += nr_pages;
move_to = &folios_skipped;
max_nr_skipped++;
goto move;
}
/*
* Do not count skipped folios because that makes the function
* return with no isolated folios if the LRU mostly contains
* ineligible folios. This causes the VM to not reclaim any
* folios, triggering a premature OOM.
* Account all pages in a folio.
*/
scan += nr_pages;
if (!folio_test_lru(folio))
goto move;
if (!sc->may_unmap && folio_mapped(folio))
goto move;
/*
* Be careful not to clear the lru flag until after we're
* sure the folio is not being freed elsewhere -- the
* folio release code relies on it.
*/
if (unlikely(!folio_try_get(folio)))
goto move;
if (!folio_test_clear_lru(folio)) {
/* Another thread is already isolating this folio */
folio_put(folio);
goto move;
}
nr_taken += nr_pages;
nr_zone_taken[folio_zonenum(folio)] += nr_pages;
move_to = dst;
move:
list_move(&folio->lru, move_to);
}
/*
* Splice any skipped folios to the start of the LRU list. Note that
* this disrupts the LRU order when reclaiming for lower zones but
* we cannot splice to the tail. If we did then the SWAP_CLUSTER_MAX
* scanning would soon rescan the same folios to skip and waste lots
* of cpu cycles.
*/
if (!list_empty(&folios_skipped)) {
int zid;
list_splice(&folios_skipped, src);
for (zid = 0; zid < MAX_NR_ZONES; zid++) {
if (!nr_skipped[zid])
continue;
__count_zid_vm_events(PGSCAN_SKIP, zid, nr_skipped[zid]);
skipped += nr_skipped[zid];
}
}
*nr_scanned = total_scan;
trace_mm_vmscan_lru_isolate(sc->reclaim_idx, sc->order, nr_to_scan,
total_scan, skipped, nr_taken, lru);
update_lru_sizes(lruvec, lru, nr_zone_taken);
return nr_taken;
}
v6.18 move_folios_to_lru
// 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.18, mm/vmscan.c:1923-1989.
* Complete function definition; surrounding kernel context is required.
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/vmscan.c#L1923
* License text: GPL-2.0.txt (in this directory).
*/
static unsigned int move_folios_to_lru(struct lruvec *lruvec,
struct list_head *list)
{
int nr_pages, nr_moved = 0;
struct folio_batch free_folios;
folio_batch_init(&free_folios);
while (!list_empty(list)) {
struct folio *folio = lru_to_folio(list);
VM_BUG_ON_FOLIO(folio_test_lru(folio), folio);
list_del(&folio->lru);
if (unlikely(!folio_evictable(folio))) {
spin_unlock_irq(&lruvec->lru_lock);
folio_putback_lru(folio);
spin_lock_irq(&lruvec->lru_lock);
continue;
}
/*
* The folio_set_lru needs to be kept here for list integrity.
* Otherwise:
* #0 move_folios_to_lru #1 release_pages
* if (!folio_put_testzero())
* if (folio_put_testzero())
* !lru //skip lru_lock
* folio_set_lru()
* list_add(&folio->lru,)
* list_add(&folio->lru,)
*/
folio_set_lru(folio);
if (unlikely(folio_put_testzero(folio))) {
__folio_clear_lru_flags(folio);
folio_unqueue_deferred_split(folio);
if (folio_batch_add(&free_folios, folio) == 0) {
spin_unlock_irq(&lruvec->lru_lock);
mem_cgroup_uncharge_folios(&free_folios);
free_unref_folios(&free_folios);
spin_lock_irq(&lruvec->lru_lock);
}
continue;
}
/*
* All pages were isolated from the same lruvec (and isolation
* inhibits memcg migration).
*/
VM_BUG_ON_FOLIO(!folio_matches_lruvec(folio, lruvec), folio);
lruvec_add_folio(lruvec, folio);
nr_pages = folio_nr_pages(folio);
nr_moved += nr_pages;
if (folio_test_active(folio))
workingset_age_nonresident(lruvec, nr_pages);
}
if (free_folios.nr) {
spin_unlock_irq(&lruvec->lru_lock);
mem_cgroup_uncharge_folios(&free_folios);
free_unref_folios(&free_folios);
spin_lock_irq(&lruvec->lru_lock);
}
return nr_moved;
}
참고 자료
- Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 11, Shrinking the Active List / Isolating LRU Folios.
- Linux v6.18 mm/vmscan.c:
shrink_active_list(),isolate_lru_folios(),move_folios_to_lru(). - Linux v6.18 include/linux/page_ref.h: reference 조작의 기본 의미.