inactive list에 있다는 것은 회수 후보라는 뜻이지 버려도 된다는 판정이 아닙니다. page table의 사용 흔적, mlock, swap backing, writeback, cache reference를 차례로 확인해야 합니다. shrink_inactive_list()와 고전적으로 shrink_page_list()라 부르던 경로가 이 후보를 실제 free로 바꿉니다. v6.12·v6.18에서는 중심 helper 이름이 shrink_folio_list()이며, 뒤에서 writeback의 version 차이도 구분하겠습니다.
너무 많은 task가 후보를 가져가면 먼저 조절합니다
여러 direct reclaimer가 한꺼번에 folio를 isolate하고 I/O를 기다리면 shared inactive list가 지나치게 작아질 수 있습니다. too_many_isolated()는 anon/file family별 inactive와 isolated base page 양을 비교합니다. v6.18의 일반적인 I/O·filesystem 접근 허용 context에서는 inactive를 3 bit shift하여 isolated가 대략 inactive의 1/8을 넘는지 검사합니다. NOIO·NOFS context에서는 inactive 전체와 비교하여 다른 reclaimer와의 circular dependency를 피할 여지를 줍니다. kswapd이거나 해당 writeback-throttling protocol을 적용하지 않는 context는 별도로 제외합니다.
격리가 과도하면 VMSCAN_THROTTLE_ISOLATED로 한 번 기다립니다. 깨어나도 여전히 과도하면 같은 호출에서 반복 대기하지 않고 0을 반환합니다. 대기 뒤 fatal signal이 생겼다면 SWAP_CLUSTER_MAX를 반환하는데, 이 특별한 양수는 이번에 그만큼 free했다는 측정값이 아닙니다. 종료할 task가 더 회수하려고 머무르지 않도록 상위 reclaim의 제어 흐름을 진행시키는 값입니다.
pending add batch를 drain하고 lruvec lock 아래 후보를 isolate한 뒤, 비싼 검사는 lock 밖에서 수행합니다. 회수되지 않은 후보는 현재 상태에 맞는 LRU로 putback합니다. 실제로 pageout한 양은 비용 accounting에, dirty·writeback 관찰은 scan_control 통계에 반영하며 필요하면 flusher를 깨웁니다.
Folio lock을 얻지 못하면 다른 후보를 봅니다
shrink_page_list()는 local input list에서 folio를 꺼내 folio_trylock()을 시도합니다. 다른 후보가 있는데 여기서 매번 blocking하면 reclaim의 진행이 한 folio에 묶이기 때문입니다. lock을 얻지 못한 folio는 보존하며, lock을 얻었어도 evictable인지 다시 확인합니다.
mlock이나 mapping의 unevictable 상태는 앞의 isolation 이후에도 바뀔 수 있습니다. user mapping이 남았는데 이번 reclaim에서 unmap이 허용되지 않았다면 free할 수 없습니다. 이런 이유로 후보를 돌려보내는 것은 회수 실패를 숨기는 일이 아니라 안전한 ownership 유지입니다.
Writeback 중인 folio를 다시 쓰지는 않습니다
dirty·writeback 상태는 기본 folio flag뿐 아니라 mapping의 전용 검사 hook이 있으면 그것도 고려합니다. ordinary anon과 lazy-free를 file writeback 통계에 기계적으로 포함해서는 안 됩니다. anon swap-out은 일반 file flusher와 처리 책임이 다릅니다.
이미 writeback 중이면 대부분 PG_reclaim을 남겨 완료 후 다시 빨리 검사하도록 유도하고 이번 pass에서는 보존합니다. kswapd가 writeback 중인 같은 후보를 재차 만나고 node 전체도 writeback에 막힌 상태라면 immediate 통계를 올려 상위 층이 throttle하도록 합니다. 같은 folio를 CPU로 더 빨리 반복 검사해도 disk가 빨라지지는 않습니다.
writeback 완료 후의 rotation 역시 즉시 free는 아닙니다. v6.18 진입 helper는 unlocked·clean·evictable·LRU 상태를 확인하며, active라는 이유만으로 제외하지는 않습니다. batch callback은 unevictable을 재검사하고 active를 clear하여 inactive tail로 이동할 수 있습니다. 이후 reclaim에서는 reference와 mapping을 다시 확인합니다.
사용 흔적이 있으면 backing 준비보다 먼저 보호합니다
보통 reclaim은 folio_check_references()의 결과를 따릅니다. KEEP이면 보존하고, ACTIVATE이면 active 보호를 요청합니다. RECLAIM_CLEAN은 clean한 경우에만 회수를 계속하며 dirty data를 여기서 쓰지 않도록 합니다. RECLAIM은 다음 조건을 살펴보라는 뜻이지 즉시 free하라는 뜻이 아닙니다.
special caller가 reference 무시를 요청하는 경로는 ordinary pressure reclaim과 구분해야 합니다. v6.18에서는 FOLIOREF_*라는 이름을 사용하며 MGLRU branch도 있으므로 과거의 enum 이름만 치환해서 모든 version을 설명할 수는 없습니다.
Anonymous 내용에는 복구 경로가 필요합니다
ordinary anonymous이고 swap-backed 성격인데 아직 swap cache에 없다면 backing을 마련해야 합니다. v6.0·v6.12에서는 add_to_swap()을 사용하고, v6.18에서는 folio_alloc_swap()의 반환값 0이 성공입니다. 이름만 바뀐 같은 boolean API로 읽으면 성공과 실패를 뒤집게 됩니다. I/O가 금지된 context나 GUP pin 때문에 진행할 수 없는 경우에는 무리하게 시도하지 않습니다. backing 확보 실패는 다른 reclaimable folio의 진행을 막지 않도록 보존·activation 경로로 처리합니다.
그 뒤 try_to_unmap()이 reverse mapping을 따라 user PTE를 제거합니다. ordinary anonymous swap-out에서는 PTE를 그냥 비우지 않고 swap entry로 바꾸어 이후 fault가 내용을 찾을 수 있도록 합니다. file folio는 file과 offset으로 복구할 수 있습니다. unmap 뒤에도 mapped라면 아직 free의 전제조건을 만족하지 못한 것입니다.
Dirty data는 caller context에 맞게 내보냅니다
v6.12의 dirty file writeback은 stack 깊이를 예측하기 어려운 direct reclaim에서 함부로 수행하지 않습니다. file LRU folio에서는 kswapd이고, 이전에 PG_reclaim이 설정되었으며, node에 PGDAT_DIRTY가 있는 조건을 요구합니다. 그 밖에는 reclaim 표시를 남기고 activation하여 flusher가 진행할 기회를 줍니다. 이미 dirty 후보가 한 차례 돌았는데 writeback이 따라오지 않는 상황과, 단순히 file을 방금 수정한 상황은 다릅니다.
v6.18의 pageout()은 더 좁습니다. 일반 filesystem folio의 writeback을 여기서 시작하지 않고 page-writeback 쪽에 맡기며, swap과 tmpfs/shmem 경로를 다룹니다. 따라서 v6.12의 mapping->a_ops->writepage 설명을 모든 최신 file-reclaim에 적용해서는 안 됩니다. dirty filesystem folio를 임의 순서의 단건 I/O로 내보내면 이미 진행 중인 연속 write를 악화시킬 수도 있습니다.
may_enter_fs()와 may_writepage도 계속 유효합니다. filesystem에 들어갈 수 없거나 clean-only 판정이면 folio를 보존합니다. I/O를 시작하기 전에는 필요한 batched TLB flush를 완료하여 이전 user mapping을 통한 수정과의 순서를 맞춰야 합니다.
pageout()의 PAGE_SUCCESS는 writeback을 제출했다는 결과일 수 있습니다. 동기 완료나 RAM 반환을 뜻하지 않습니다. callback 뒤에 folio가 unlock되었을 수도 있으므로 dirty·writeback을 다시 검사하고, 가능한 경우 lock을 재획득한 뒤 다시 검사합니다. 그 사이 재오염되거나 I/O가 진행 중이면 이번 pass에서 free하지 않습니다.
마지막 reference까지 확인해야 free할 수 있습니다
MADV_FREE의 lazy-free anon은 내용을 backing에 보존할 의무가 ordinary anon과 다릅니다. 그러나 여기서도 예상 refcount를 원자적으로 freeze할 수 있어야 합니다. concurrent user write나 다른 reference가 있으면 무조건 버려서는 안 됩니다.
page cache·swap cache에 들어 있는 folio는 __remove_mapping()이 cache lookup과의 경쟁을 막으며 제거합니다. 실패하면 되돌립니다. 이 제거에 성공한 항목은 reclaimed 수를 늘리고 free용 경로에 모읍니다. 나머지는 잠금을 적절히 풀어 ret list로 돌려 caller가 LRU를 복구합니다. 다만 함수의 전체 nr_reclaimed에는 NUMA demotion 성공량도 포함될 수 있으므로 반환값 전체를 시스템에서 사라진 physical page 수와 같다고 보아서는 안 됩니다.
반환 list와 free 경로를 끝까지 따라갑니다
고전 shrink_page_list()의 입력 page_list는 loop가 끝날 때 비어 있고, 보존할 folio는 ret_pages에 따로 모여 있습니다. keep_locked는 lock을 풀고 keep으로 내려가며, 이미 pageout backend가 unlock한 경로는 keep으로 직접 갑니다. 마지막에 이 반환 list를 입력 list에 splice해야 caller가 putback할 항목을 잃지 않습니다. 단순히 모든 goto keep을 'LRU tail 이동'으로 설명하면 아직 local list인 상태와 shared LRU 복귀를 섞게 됩니다.
activate_locked에서는 swap cache folio이면서 swap이 부족하거나 mlocked이면 불필요한 swap backing을 해제할 수 있는지 먼저 시도합니다. 실제 slot의 해제 여부는 swap reference와 writeback 등 하위 조건에 달려 있습니다. 그 뒤에도 mlocked가 아닌 folio에만 active를 설정합니다. unevictable 후보를 발견했다는 이유로 무조건 active list에 연결하는 동작은 아닙니다.
private buffer가 붙은 folio에는 filemap_release_folio()를 통한 별도 정리가 필요합니다. 이 helper 자체가 I/O를 수행하지 않아도 buffer 상태 때문에 folio가 실제로 clean하다는 사실이 드러날 수 있습니다. mapping이 없고 isolation reference 하나만 남은 예외에서는 unlock 뒤 마지막 put으로 free할 수 있고, speculative reference와 경합하면 그 reference가 최종 해제를 맡을 수 있습니다. cache 제거의 일반 경로 하나로 모든 해제를 설명할 수 없는 이유입니다.
large folio가 split되면 남은 subpage가 이후 loop를 따로 통과하므로 nr_scanned와 현재 nr_pages를 보정합니다. 고전 구현은 large folio를 별도 해제하고 base page는 free list에서 bulk 처리합니다. 끝에서는 memcg uncharge, batched TLB flush, free, 반환 list 복구, 남은 swap plug 제출까지 마칩니다. pageout 제출량이나 scan량을 이 정리 이전의 최종 free 개수로 해석하지 않아야 합니다.

성공 경로의 bulk free 전에 필요한 TLB 정리를 마치고 allocator에 넘긴 뒤에야 physical page를 다른 용도로 재사용할 수 있습니다. PTE 제거, cache 제거, refcount freeze, allocator 반환은 연결되어 있지만 서로 같은 시점은 아닙니다.
전체 code로 ownership과 종료 경로를 확인합니다
아래 v6.0 전체 함수는 isolation 제한부터 반환 list 복구와 swap plug 정리까지 포함합니다. v6.18의 driver·isolation 제한도 함께 두었습니다. 현대 중심 loop의 전체 구현과 달라진 조건은 v6.18 folio reclaim에서 이어집니다.
v6.0 shrink_inactive_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:2401-2490.
* Complete function definition; surrounding kernel context is required.
* Source: https://github.com/torvalds/linux/blob/v6.0/mm/vmscan.c#L2401
* License text: GPL-2.0.txt (in this directory).
*/
static unsigned long
shrink_inactive_list(unsigned long nr_to_scan, struct lruvec *lruvec,
struct scan_control *sc, enum lru_list lru)
{
LIST_HEAD(page_list);
unsigned long nr_scanned;
unsigned int nr_reclaimed = 0;
unsigned long nr_taken;
struct reclaim_stat stat;
bool file = is_file_lru(lru);
enum vm_event_item item;
struct pglist_data *pgdat = lruvec_pgdat(lruvec);
bool stalled = false;
while (unlikely(too_many_isolated(pgdat, file, sc))) {
if (stalled)
return 0;
/* wait a bit for the reclaimer. */
stalled = true;
reclaim_throttle(pgdat, VMSCAN_THROTTLE_ISOLATED);
/* We are about to die and free our memory. Return now. */
if (fatal_signal_pending(current))
return SWAP_CLUSTER_MAX;
}
lru_add_drain();
spin_lock_irq(&lruvec->lru_lock);
nr_taken = isolate_lru_pages(nr_to_scan, lruvec, &page_list,
&nr_scanned, sc, lru);
__mod_node_page_state(pgdat, NR_ISOLATED_ANON + file, nr_taken);
item = current_is_kswapd() ? PGSCAN_KSWAPD : PGSCAN_DIRECT;
if (!cgroup_reclaim(sc))
__count_vm_events(item, nr_scanned);
__count_memcg_events(lruvec_memcg(lruvec), item, nr_scanned);
__count_vm_events(PGSCAN_ANON + file, nr_scanned);
spin_unlock_irq(&lruvec->lru_lock);
if (nr_taken == 0)
return 0;
nr_reclaimed = shrink_page_list(&page_list, pgdat, sc, &stat, false);
spin_lock_irq(&lruvec->lru_lock);
move_pages_to_lru(lruvec, &page_list);
__mod_node_page_state(pgdat, NR_ISOLATED_ANON + file, -nr_taken);
item = current_is_kswapd() ? PGSTEAL_KSWAPD : PGSTEAL_DIRECT;
if (!cgroup_reclaim(sc))
__count_vm_events(item, nr_reclaimed);
__count_memcg_events(lruvec_memcg(lruvec), item, nr_reclaimed);
__count_vm_events(PGSTEAL_ANON + file, nr_reclaimed);
spin_unlock_irq(&lruvec->lru_lock);
lru_note_cost(lruvec, file, stat.nr_pageout);
mem_cgroup_uncharge_list(&page_list);
free_unref_page_list(&page_list);
/*
* If dirty pages are scanned that are not queued for IO, it
* implies that flushers are not doing their job. This can
* happen when memory pressure pushes dirty pages to the end of
* the LRU before the dirty limits are breached and the dirty
* data has expired. It can also happen when the proportion of
* dirty pages grows not through writes but through memory
* pressure reclaiming all the clean cache. And in some cases,
* the flushers simply cannot keep up with the allocation
* rate. Nudge the flusher threads in case they are asleep.
*/
if (stat.nr_unqueued_dirty == nr_taken)
wakeup_flusher_threads(WB_REASON_VMSCAN);
sc->nr.dirty += stat.nr_dirty;
sc->nr.congested += stat.nr_congested;
sc->nr.unqueued_dirty += stat.nr_unqueued_dirty;
sc->nr.writeback += stat.nr_writeback;
sc->nr.immediate += stat.nr_immediate;
sc->nr.taken += nr_taken;
if (file)
sc->nr.file_taken += nr_taken;
trace_mm_vmscan_lru_shrink_inactive(pgdat->node_id,
nr_scanned, nr_reclaimed, &stat, sc->priority, file);
return nr_reclaimed;
}
v6.0 too_many_isolated
// 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:2280-2315.
* Complete function definition; surrounding kernel context is required.
* Source: https://github.com/torvalds/linux/blob/v6.0/mm/vmscan.c#L2280
* License text: GPL-2.0.txt (in this directory).
*/
static int too_many_isolated(struct pglist_data *pgdat, int file,
struct scan_control *sc)
{
unsigned long inactive, isolated;
bool too_many;
if (current_is_kswapd())
return 0;
if (!writeback_throttling_sane(sc))
return 0;
if (file) {
inactive = node_page_state(pgdat, NR_INACTIVE_FILE);
isolated = node_page_state(pgdat, NR_ISOLATED_FILE);
} else {
inactive = node_page_state(pgdat, NR_INACTIVE_ANON);
isolated = node_page_state(pgdat, NR_ISOLATED_ANON);
}
/*
* GFP_NOIO/GFP_NOFS callers are allowed to isolate more pages, so they
* won't get blocked by normal direct-reclaimers, forming a circular
* deadlock.
*/
if ((sc->gfp_mask & (__GFP_IO | __GFP_FS)) == (__GFP_IO | __GFP_FS))
inactive >>= 3;
too_many = isolated > inactive;
/* Wake up tasks throttled due to too_many_isolated. */
if (!too_many)
wake_throttle_isolated(pgdat);
return too_many;
}
v6.0 shrink_page_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:1589-2048.
* Complete function definition; surrounding kernel context is required.
* Source: https://github.com/torvalds/linux/blob/v6.0/mm/vmscan.c#L1589
* License text: GPL-2.0.txt (in this directory).
*/
static unsigned int shrink_page_list(struct list_head *page_list,
struct pglist_data *pgdat,
struct scan_control *sc,
struct reclaim_stat *stat,
bool ignore_references)
{
LIST_HEAD(ret_pages);
LIST_HEAD(free_pages);
LIST_HEAD(demote_pages);
unsigned int nr_reclaimed = 0;
unsigned int pgactivate = 0;
bool do_demote_pass;
struct swap_iocb *plug = NULL;
memset(stat, 0, sizeof(*stat));
cond_resched();
do_demote_pass = can_demote(pgdat->node_id, sc);
retry:
while (!list_empty(page_list)) {
struct address_space *mapping;
struct folio *folio;
enum page_references references = PAGEREF_RECLAIM;
bool dirty, writeback;
unsigned int nr_pages;
cond_resched();
folio = lru_to_folio(page_list);
list_del(&folio->lru);
if (!folio_trylock(folio))
goto keep;
VM_BUG_ON_FOLIO(folio_test_active(folio), folio);
nr_pages = folio_nr_pages(folio);
/* Account the number of base pages */
sc->nr_scanned += nr_pages;
if (unlikely(!folio_evictable(folio)))
goto activate_locked;
if (!sc->may_unmap && folio_mapped(folio))
goto keep_locked;
/*
* The number of dirty pages determines if a node is marked
* reclaim_congested. kswapd will stall and start writing
* folios if the tail of the LRU is all dirty unqueued folios.
*/
folio_check_dirty_writeback(folio, &dirty, &writeback);
if (dirty || writeback)
stat->nr_dirty += nr_pages;
if (dirty && !writeback)
stat->nr_unqueued_dirty += nr_pages;
/*
* Treat this folio as congested if folios are cycling
* through the LRU so quickly that the folios marked
* for immediate reclaim are making it to the end of
* the LRU a second time.
*/
if (writeback && folio_test_reclaim(folio))
stat->nr_congested += nr_pages;
/*
* If a folio at the tail of the LRU is under writeback, there
* are three cases to consider.
*
* 1) If reclaim is encountering an excessive number
* of folios under writeback and this folio has both
* the writeback and reclaim flags set, then it
* indicates that folios are being queued for I/O but
* are being recycled through the LRU before the I/O
* can complete. Waiting on the folio itself risks an
* indefinite stall if it is impossible to writeback
* the folio due to I/O error or disconnected storage
* so instead note that the LRU is being scanned too
* quickly and the caller can stall after the folio
* list has been processed.
*
* 2) Global or new memcg reclaim encounters a folio that is
* not marked for immediate reclaim, or the caller does not
* have __GFP_FS (or __GFP_IO if it's simply going to swap,
* not to fs). In this case mark the folio for immediate
* reclaim and continue scanning.
*
* Require may_enter_fs() because we would wait on fs, which
* may not have submitted I/O yet. And the loop driver might
* enter reclaim, and deadlock if it waits on a folio for
* which it is needed to do the write (loop masks off
* __GFP_IO|__GFP_FS for this reason); but more thought
* would probably show more reasons.
*
* 3) Legacy memcg encounters a folio that already has the
* reclaim flag set. memcg does not have any dirty folio
* throttling so we could easily OOM just because too many
* folios are in writeback and there is nothing else to
* reclaim. Wait for the writeback to complete.
*
* In cases 1) and 2) we activate the folios to get them out of
* the way while we continue scanning for clean folios on the
* inactive list and refilling from the active list. The
* observation here is that waiting for disk writes is more
* expensive than potentially causing reloads down the line.
* Since they're marked for immediate reclaim, they won't put
* memory pressure on the cache working set any longer than it
* takes to write them to disk.
*/
if (folio_test_writeback(folio)) {
/* Case 1 above */
if (current_is_kswapd() &&
folio_test_reclaim(folio) &&
test_bit(PGDAT_WRITEBACK, &pgdat->flags)) {
stat->nr_immediate += nr_pages;
goto activate_locked;
/* Case 2 above */
} else if (writeback_throttling_sane(sc) ||
!folio_test_reclaim(folio) ||
!may_enter_fs(folio, sc->gfp_mask)) {
/*
* This is slightly racy -
* folio_end_writeback() might have
* just cleared the reclaim flag, then
* setting the reclaim flag here ends up
* interpreted as the readahead flag - but
* that does not matter enough to care.
* What we do want is for this folio to
* have the reclaim flag set next time
* memcg reclaim reaches the tests above,
* so it will then wait for writeback to
* avoid OOM; and it's also appropriate
* in global reclaim.
*/
folio_set_reclaim(folio);
stat->nr_writeback += nr_pages;
goto activate_locked;
/* Case 3 above */
} else {
folio_unlock(folio);
folio_wait_writeback(folio);
/* then go back and try same folio again */
list_add_tail(&folio->lru, page_list);
continue;
}
}
if (!ignore_references)
references = folio_check_references(folio, sc);
switch (references) {
case PAGEREF_ACTIVATE:
goto activate_locked;
case PAGEREF_KEEP:
stat->nr_ref_keep += nr_pages;
goto keep_locked;
case PAGEREF_RECLAIM:
case PAGEREF_RECLAIM_CLEAN:
; /* try to reclaim the folio below */
}
/*
* Before reclaiming the folio, try to relocate
* its contents to another node.
*/
if (do_demote_pass &&
(thp_migration_supported() || !folio_test_large(folio))) {
list_add(&folio->lru, &demote_pages);
folio_unlock(folio);
continue;
}
/*
* Anonymous process memory has backing store?
* Try to allocate it some swap space here.
* Lazyfree folio could be freed directly
*/
if (folio_test_anon(folio) && folio_test_swapbacked(folio)) {
if (!folio_test_swapcache(folio)) {
if (!(sc->gfp_mask & __GFP_IO))
goto keep_locked;
if (folio_maybe_dma_pinned(folio))
goto keep_locked;
if (folio_test_large(folio)) {
/* cannot split folio, skip it */
if (!can_split_folio(folio, NULL))
goto activate_locked;
/*
* Split folios without a PMD map right
* away. Chances are some or all of the
* tail pages can be freed without IO.
*/
if (!folio_entire_mapcount(folio) &&
split_folio_to_list(folio,
page_list))
goto activate_locked;
}
if (!add_to_swap(folio)) {
if (!folio_test_large(folio))
goto activate_locked_split;
/* Fallback to swap normal pages */
if (split_folio_to_list(folio,
page_list))
goto activate_locked;
#ifdef CONFIG_TRANSPARENT_HUGEPAGE
count_vm_event(THP_SWPOUT_FALLBACK);
#endif
if (!add_to_swap(folio))
goto activate_locked_split;
}
}
} else if (folio_test_swapbacked(folio) &&
folio_test_large(folio)) {
/* Split shmem folio */
if (split_folio_to_list(folio, page_list))
goto keep_locked;
}
/*
* If the folio was split above, the tail pages will make
* their own pass through this function and be accounted
* then.
*/
if ((nr_pages > 1) && !folio_test_large(folio)) {
sc->nr_scanned -= (nr_pages - 1);
nr_pages = 1;
}
/*
* The folio is mapped into the page tables of one or more
* processes. Try to unmap it here.
*/
if (folio_mapped(folio)) {
enum ttu_flags flags = TTU_BATCH_FLUSH;
bool was_swapbacked = folio_test_swapbacked(folio);
if (folio_test_pmd_mappable(folio))
flags |= TTU_SPLIT_HUGE_PMD;
try_to_unmap(folio, flags);
if (folio_mapped(folio)) {
stat->nr_unmap_fail += nr_pages;
if (!was_swapbacked &&
folio_test_swapbacked(folio))
stat->nr_lazyfree_fail += nr_pages;
goto activate_locked;
}
}
mapping = folio_mapping(folio);
if (folio_test_dirty(folio)) {
/*
* Only kswapd can writeback filesystem folios
* to avoid risk of stack overflow. But avoid
* injecting inefficient single-folio I/O into
* flusher writeback as much as possible: only
* write folios when we've encountered many
* dirty folios, and when we've already scanned
* the rest of the LRU for clean folios and see
* the same dirty folios again (with the reclaim
* flag set).
*/
if (folio_is_file_lru(folio) &&
(!current_is_kswapd() ||
!folio_test_reclaim(folio) ||
!test_bit(PGDAT_DIRTY, &pgdat->flags))) {
/*
* Immediately reclaim when written back.
* Similar in principle to deactivate_page()
* except we already have the folio isolated
* and know it's dirty
*/
node_stat_mod_folio(folio, NR_VMSCAN_IMMEDIATE,
nr_pages);
folio_set_reclaim(folio);
goto activate_locked;
}
if (references == PAGEREF_RECLAIM_CLEAN)
goto keep_locked;
if (!may_enter_fs(folio, sc->gfp_mask))
goto keep_locked;
if (!sc->may_writepage)
goto keep_locked;
/*
* Folio is dirty. Flush the TLB if a writable entry
* potentially exists to avoid CPU writes after I/O
* starts and then write it out here.
*/
try_to_unmap_flush_dirty();
switch (pageout(folio, mapping, &plug)) {
case PAGE_KEEP:
goto keep_locked;
case PAGE_ACTIVATE:
goto activate_locked;
case PAGE_SUCCESS:
stat->nr_pageout += nr_pages;
if (folio_test_writeback(folio))
goto keep;
if (folio_test_dirty(folio))
goto keep;
/*
* A synchronous write - probably a ramdisk. Go
* ahead and try to reclaim the folio.
*/
if (!folio_trylock(folio))
goto keep;
if (folio_test_dirty(folio) ||
folio_test_writeback(folio))
goto keep_locked;
mapping = folio_mapping(folio);
fallthrough;
case PAGE_CLEAN:
; /* try to free the folio below */
}
}
/*
* If the folio has buffers, try to free the buffer
* mappings associated with this folio. If we succeed
* we try to free the folio as well.
*
* We do this even if the folio is dirty.
* filemap_release_folio() does not perform I/O, but it
* is possible for a folio to have the dirty flag set,
* but it is actually clean (all its buffers are clean).
* This happens if the buffers were written out directly,
* with submit_bh(). ext3 will do this, as well as
* the blockdev mapping. filemap_release_folio() will
* discover that cleanness and will drop the buffers
* and mark the folio clean - it can be freed.
*
* Rarely, folios can have buffers and no ->mapping.
* These are the folios which were not successfully
* invalidated in truncate_cleanup_folio(). We try to
* drop those buffers here and if that worked, and the
* folio is no longer mapped into process address space
* (refcount == 1) it can be freed. Otherwise, leave
* the folio on the LRU so it is swappable.
*/
if (folio_has_private(folio)) {
if (!filemap_release_folio(folio, sc->gfp_mask))
goto activate_locked;
if (!mapping && folio_ref_count(folio) == 1) {
folio_unlock(folio);
if (folio_put_testzero(folio))
goto free_it;
else {
/*
* rare race with speculative reference.
* the speculative reference will free
* this folio shortly, so we may
* increment nr_reclaimed here (and
* leave it off the LRU).
*/
nr_reclaimed += nr_pages;
continue;
}
}
}
if (folio_test_anon(folio) && !folio_test_swapbacked(folio)) {
/* follow __remove_mapping for reference */
if (!folio_ref_freeze(folio, 1))
goto keep_locked;
/*
* The folio has only one reference left, which is
* from the isolation. After the caller puts the
* folio back on the lru and drops the reference, the
* folio will be freed anyway. It doesn't matter
* which lru it goes on. So we don't bother checking
* the dirty flag here.
*/
count_vm_events(PGLAZYFREED, nr_pages);
count_memcg_folio_events(folio, PGLAZYFREED, nr_pages);
} else if (!mapping || !__remove_mapping(mapping, folio, true,
sc->target_mem_cgroup))
goto keep_locked;
folio_unlock(folio);
free_it:
/*
* Folio may get swapped out as a whole, need to account
* all pages in it.
*/
nr_reclaimed += nr_pages;
/*
* Is there need to periodically free_page_list? It would
* appear not as the counts should be low
*/
if (unlikely(folio_test_large(folio)))
destroy_large_folio(folio);
else
list_add(&folio->lru, &free_pages);
continue;
activate_locked_split:
/*
* The tail pages that are failed to add into swap cache
* reach here. Fixup nr_scanned and nr_pages.
*/
if (nr_pages > 1) {
sc->nr_scanned -= (nr_pages - 1);
nr_pages = 1;
}
activate_locked:
/* Not a candidate for swapping, so reclaim swap space. */
if (folio_test_swapcache(folio) &&
(mem_cgroup_swap_full(&folio->page) ||
folio_test_mlocked(folio)))
try_to_free_swap(&folio->page);
VM_BUG_ON_FOLIO(folio_test_active(folio), folio);
if (!folio_test_mlocked(folio)) {
int type = folio_is_file_lru(folio);
folio_set_active(folio);
stat->nr_activate[type] += nr_pages;
count_memcg_folio_events(folio, PGACTIVATE, nr_pages);
}
keep_locked:
folio_unlock(folio);
keep:
list_add(&folio->lru, &ret_pages);
VM_BUG_ON_FOLIO(folio_test_lru(folio) ||
folio_test_unevictable(folio), folio);
}
/* 'page_list' is always empty here */
/* Migrate folios selected for demotion */
nr_reclaimed += demote_page_list(&demote_pages, pgdat);
/* Folios that could not be demoted are still in @demote_pages */
if (!list_empty(&demote_pages)) {
/* Folios which weren't demoted go back on @page_list for retry: */
list_splice_init(&demote_pages, page_list);
do_demote_pass = false;
goto retry;
}
pgactivate = stat->nr_activate[0] + stat->nr_activate[1];
mem_cgroup_uncharge_list(&free_pages);
try_to_unmap_flush();
free_unref_page_list(&free_pages);
list_splice(&ret_pages, page_list);
count_vm_events(PGACTIVATE, pgactivate);
if (plug)
swap_write_unplug(plug);
return nr_reclaimed;
}
v6.18 shrink_inactive_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:2005-2107.
* Complete function definition; surrounding kernel context is required.
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/vmscan.c#L2005
* License text: GPL-2.0.txt (in this directory).
*/
static unsigned long shrink_inactive_list(unsigned long nr_to_scan,
struct lruvec *lruvec, struct scan_control *sc,
enum lru_list lru)
{
LIST_HEAD(folio_list);
unsigned long nr_scanned;
unsigned int nr_reclaimed = 0;
unsigned long nr_taken;
struct reclaim_stat stat;
bool file = is_file_lru(lru);
enum vm_event_item item;
struct pglist_data *pgdat = lruvec_pgdat(lruvec);
bool stalled = false;
while (unlikely(too_many_isolated(pgdat, file, sc))) {
if (stalled)
return 0;
/* wait a bit for the reclaimer. */
stalled = true;
reclaim_throttle(pgdat, VMSCAN_THROTTLE_ISOLATED);
/* We are about to die and free our memory. Return now. */
if (fatal_signal_pending(current))
return SWAP_CLUSTER_MAX;
}
lru_add_drain();
spin_lock_irq(&lruvec->lru_lock);
nr_taken = isolate_lru_folios(nr_to_scan, lruvec, &folio_list,
&nr_scanned, sc, lru);
__mod_node_page_state(pgdat, NR_ISOLATED_ANON + file, nr_taken);
item = PGSCAN_KSWAPD + reclaimer_offset(sc);
if (!cgroup_reclaim(sc))
__count_vm_events(item, nr_scanned);
count_memcg_events(lruvec_memcg(lruvec), item, nr_scanned);
__count_vm_events(PGSCAN_ANON + file, nr_scanned);
spin_unlock_irq(&lruvec->lru_lock);
if (nr_taken == 0)
return 0;
nr_reclaimed = shrink_folio_list(&folio_list, pgdat, sc, &stat, false,
lruvec_memcg(lruvec));
spin_lock_irq(&lruvec->lru_lock);
move_folios_to_lru(lruvec, &folio_list);
__mod_lruvec_state(lruvec, PGDEMOTE_KSWAPD + reclaimer_offset(sc),
stat.nr_demoted);
__mod_node_page_state(pgdat, NR_ISOLATED_ANON + file, -nr_taken);
item = PGSTEAL_KSWAPD + reclaimer_offset(sc);
if (!cgroup_reclaim(sc))
__count_vm_events(item, nr_reclaimed);
count_memcg_events(lruvec_memcg(lruvec), item, nr_reclaimed);
__count_vm_events(PGSTEAL_ANON + file, nr_reclaimed);
lru_note_cost_unlock_irq(lruvec, file, stat.nr_pageout,
nr_scanned - nr_reclaimed);
/*
* If dirty folios are scanned that are not queued for IO, it
* implies that flushers are not doing their job. This can
* happen when memory pressure pushes dirty folios to the end of
* the LRU before the dirty limits are breached and the dirty
* data has expired. It can also happen when the proportion of
* dirty folios grows not through writes but through memory
* pressure reclaiming all the clean cache. And in some cases,
* the flushers simply cannot keep up with the allocation
* rate. Nudge the flusher threads in case they are asleep.
*/
if (stat.nr_unqueued_dirty == nr_taken) {
wakeup_flusher_threads(WB_REASON_VMSCAN);
/*
* For cgroupv1 dirty throttling is achieved by waking up
* the kernel flusher here and later waiting on folios
* which are in writeback to finish (see shrink_folio_list()).
*
* Flusher may not be able to issue writeback quickly
* enough for cgroupv1 writeback throttling to work
* on a large system.
*/
if (!writeback_throttling_sane(sc))
reclaim_throttle(pgdat, VMSCAN_THROTTLE_WRITEBACK);
}
sc->nr.dirty += stat.nr_dirty;
sc->nr.congested += stat.nr_congested;
sc->nr.unqueued_dirty += stat.nr_unqueued_dirty;
sc->nr.writeback += stat.nr_writeback;
sc->nr.immediate += stat.nr_immediate;
sc->nr.taken += nr_taken;
if (file)
sc->nr.file_taken += nr_taken;
trace_mm_vmscan_lru_shrink_inactive(pgdat->node_id,
nr_scanned, nr_reclaimed, &stat, sc->priority, file);
return nr_reclaimed;
}
v6.18 too_many_isolated
// 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:1881-1916.
* Complete function definition; surrounding kernel context is required.
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/vmscan.c#L1881
* License text: GPL-2.0.txt (in this directory).
*/
static bool too_many_isolated(struct pglist_data *pgdat, int file,
struct scan_control *sc)
{
unsigned long inactive, isolated;
bool too_many;
if (current_is_kswapd())
return false;
if (!writeback_throttling_sane(sc))
return false;
if (file) {
inactive = node_page_state(pgdat, NR_INACTIVE_FILE);
isolated = node_page_state(pgdat, NR_ISOLATED_FILE);
} else {
inactive = node_page_state(pgdat, NR_INACTIVE_ANON);
isolated = node_page_state(pgdat, NR_ISOLATED_ANON);
}
/*
* GFP_NOIO/GFP_NOFS callers are allowed to isolate more pages, so they
* won't get blocked by normal direct-reclaimers, forming a circular
* deadlock.
*/
if (gfp_has_io_fs(sc->gfp_mask))
inactive >>= 3;
too_many = isolated > inactive;
/* Wake up tasks throttled due to too_many_isolated. */
if (!too_many)
wake_throttle_isolated(pgdat);
return too_many;
}
참고 자료
- Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 11, Shrinking the Inactive List.
- Linux v6.18 mm/vmscan.c:
too_many_isolated(),shrink_inactive_list(),shrink_folio_list()등 해당 version의 folio reclaim 경로. - Linux v6.12 mm/vmscan.c:
shrink_folio_list()와 oldershrink_page_list()명칭의 비교 기준.