# Inactive folio가 allocator로 돌아가기까지

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 개수로 해석하지 않아야 합니다.

![격리된 folio가 clean·unmapped인지, 최근 사용되었는지, writeback이나 외부 참조가 있는지에 따라 결과가 달라지는 개념도입니다. 실제 모든 branch를 복제한 flowchart는 아닙니다.](assets/folio_checks.png)

성공 경로의 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](17-folio-reclaim-modern.md)에서 이어집니다.

### v6.0 shrink_inactive_list

[공식 source](https://github.com/torvalds/linux/blob/v6.0/mm/vmscan.c#L2401) · [전체 code](kernel/v6_0_shrink_inactive_list.c) · [GPL-2.0](kernel/GPL-2.0.txt)

<!-- include-code:kernel/v6_0_shrink_inactive_list.c -->

### v6.0 too_many_isolated

[공식 source](https://github.com/torvalds/linux/blob/v6.0/mm/vmscan.c#L2280) · [전체 code](kernel/v6_0_too_many_isolated.c) · [GPL-2.0](kernel/GPL-2.0.txt)

<!-- include-code:kernel/v6_0_too_many_isolated.c -->

### v6.0 shrink_page_list

[공식 source](https://github.com/torvalds/linux/blob/v6.0/mm/vmscan.c#L1589) · [전체 code](kernel/v6_0_shrink_page_list.c) · [GPL-2.0](kernel/GPL-2.0.txt)

<!-- include-code:kernel/v6_0_shrink_page_list.c -->

### v6.18 shrink_inactive_list

[공식 source](https://github.com/torvalds/linux/blob/v6.18/mm/vmscan.c#L2005) · [전체 code](kernel/v6_18_shrink_inactive_list.c) · [GPL-2.0](kernel/GPL-2.0.txt)

<!-- include-code:kernel/v6_18_shrink_inactive_list.c -->

### v6.18 too_many_isolated

[공식 source](https://github.com/torvalds/linux/blob/v6.18/mm/vmscan.c#L1881) · [전체 code](kernel/v6_18_too_many_isolated.c) · [GPL-2.0](kernel/GPL-2.0.txt)

<!-- include-code:kernel/v6_18_too_many_isolated.c -->

## 참고 자료

- Lorenzo Stoakes, *The Linux Memory Manager*, Early Access 2025, Chapter 11, Shrinking the Inactive List.
- [Linux v6.18 mm/vmscan.c](https://github.com/torvalds/linux/blob/v6.18/mm/vmscan.c): `too_many_isolated()`, `shrink_inactive_list()`, `shrink_folio_list()` 등 해당 version의 folio reclaim 경로.
- [Linux v6.12 mm/vmscan.c](https://github.com/torvalds/linux/blob/v6.12/mm/vmscan.c): `shrink_folio_list()`와 older `shrink_page_list()` 명칭의 비교 기준.
