# v6.18 folio reclaim: swap 준비부터 demotion과 최종 free까지

reclaim을 읽을 때 함수 이름을 최신 이름으로 바꾸는 것만으로는 충분하지 않습니다. v6.18의 `shrink_folio_list()`는 large folio와 memory tiering, swap table, 비동기 writeback을 함께 다룹니다. 고전 classic LRU의 기본 질문은 남아 있지만, 같은 성공·실패를 전달하는 API와 정리 책임이 달라진 부분을 분리해서 살펴보겠습니다.

## 먼저 이번 pass에서 처리할 수 있는 후보를 가릅니다

입력은 이미 LRU에서 isolate한 folio list입니다. 함수는 folio trylock에 실패하면 보존하고, lock을 얻으면 base page 수를 scan에 더한 뒤 evictable·unmap 가능 여부를 검사합니다. poisoned base page는 일반 reclaim과 다른 정리 경로를 탑니다. active 상태가 남아 있지 않아야 한다는 assertion도 이 함수가 최초 후보 선정자가 아니라 isolation 뒤 처리자라는 사실을 보여 줍니다.

dirty·writeback 통계는 `folio_check_dirty_writeback()`으로 얻습니다. 이미 writeback 중인 folio를 다시 만났다고 두 번째 I/O를 시작하지는 않습니다. kswapd가 reclaim 표시와 `PGDAT_WRITEBACK`을 함께 본 경우 immediate 통계를 올리고 activation 경로로 갑니다. 일반 dirty throttling이 작동하거나, reclaim 표시가 없거나, filesystem 진입이 금지되거나, mapping이 reclaim 중 writeback 대기로 deadlock을 일으킬 수 있으면 reclaim 표시를 남기고 다음 후보를 봅니다.

이 조건들이 모두 맞지 않는 legacy memcg 경로에서는 lock을 풀고 writeback을 기다린 뒤 같은 folio를 입력 list에 다시 넣어 재검사합니다. 따라서 생략된 조건을 모두 단순한 `else`로 읽으면 대기해도 되는 context와 대기하면 안 되는 context를 구분할 수 없습니다. lock을 내려놓는 시점과 다시 검사한다는 사실이 함께 중요합니다.

## Reference 검사 뒤에는 다른 node로 옮길 수도 있습니다

보통 `folio_check_references()`의 KEEP·ACTIVATE·RECLAIM·RECLAIM_CLEAN 결과를 따릅니다. `ignore_references`를 요청한 특수 caller는 기본 RECLAIM 값으로 진행하지만, 이것이 mlock·pin·dirty·cache reference 검사까지 생략한다는 뜻은 아닙니다.

demotion이 허용되면 swap-out이나 file eviction 전에 folio를 낮은 tier의 node로 옮길 후보 list에 넣을 수 있습니다. large folio는 해당 migration 지원 여부도 충족해야 합니다. 실제 이동은 주 loop 뒤 `demote_folio_list()`에서 수행하며, 성공한 base page 수는 `nr_demoted`와 `nr_reclaimed`에 들어갑니다. 내용이 다른 RAM node로 옮겨졌을 뿐 시스템 전체 RAM 소비가 그만큼 줄었다는 뜻은 아닙니다.

demotion에 실패한 folio는 ordinary reclaim으로 재시도할 수 있습니다. 다만 proactive reclaim에서는 이 fallback을 하지 않으므로 요청한 작업의 성격까지 함께 보아야 합니다. 매 pass에서 같은 folio를 계속 demotion 후보로 넣지 않도록 재시도 때 demotion pass도 끕니다.

## Swap 공간을 예약하는 API는 boolean이 아닙니다

anonymous·swap-backed folio가 아직 swap cache에 없다면 `__GFP_IO`와 DMA pin 가능성을 확인합니다. v6.18의 `folio_alloc_swap()`은 0을 성공으로 반환하므로, `if (folio_alloc_swap(...))` 안은 실패 처리입니다. 고전 `if (!add_to_swap(...))`와 조건을 비교할 때 특히 주의해야 합니다.

large folio는 split할 수 있어야 하며, deferred split 대상이면서 partially mapped인 경우에는 먼저 split하여 unmapped subpage를 I/O 없이 회수할 여지를 만듭니다. 연속 swap entry를 확보하지 못한 경우에도 base-page fallback을 시도할 수 있습니다. split된 subpage는 다시 loop를 통과하므로 현재 scan accounting을 중복하지 않도록 보정합니다.

swap cache를 마련한 뒤에는 folio를 dirty로 표시합니다. MADV_FREE와의 경쟁으로 PTE의 dirty가 이미 지워졌지만 swap-backed flag가 아직 남은 순간도 있기 때문입니다. 뒤의 unmap이 dirty를 전파할 것이라고만 믿으면 실제 내용이 swap에 기록되지 않은 채 복구 대상으로 남을 수 있습니다. slot 확보·swap cache 연결은 storage write 완료와 다릅니다.

## Unmap 성공 뒤에도 pin을 다시 확인합니다

`try_to_unmap()`에는 batched TLB flush를 위한 flag를 주고, PMD-mappable folio에는 huge PMD split flag를 더합니다. large folio에는 `TTU_SYNC`도 사용합니다. 동시 PTE 갱신이 PTE를 일시적으로 비우는 순간 때문에 앞부분을 놓치지 않도록 PTL 획득 범위를 맞추려는 조건입니다.

아직 mapped이면 unmap 실패를 세고 보존합니다. lazy-free folio가 이 사이 다시 swap-backed가 되었다면 lazy-free 실패도 별도로 셉니다. unmap에 성공했어도 DMA pin 가능성을 재검사합니다. pinning 주체가 내용을 바꿀 수 있는 folio나 그 filesystem metadata를 무리하게 회수해서는 안 되기 때문입니다. 이 검사는 마지막 cache reference freeze와 별개입니다.

## PAGE_SUCCESS 뒤에는 lock과 상태를 다시 얻습니다

v6.18 `pageout()`은 ordinary filesystem writeback을 직접 시작하지 않습니다. mapping이 shmem도 아니고 folio가 anon도 아니면 PAGE_ACTIVATE를 반환합니다. swap·shmem은 `writeout()`에서 각각 `swap_writeout()`·`shmem_writeout()`으로 나뉩니다. large shmem writeout 중 split된 folio가 입력 list로 돌아올 수 있어 반환 후 scan accounting도 다시 맞춥니다.

PAGE_KEEP·PAGE_ACTIVATE·PAGE_CLEAN 경로와 backend가 unlock한 PAGE_SUCCESS 경로의 ownership을 구별해야 합니다. PAGE_SUCCESS여도 writeback·dirty가 남으면 보존하고, 모두 없어 보이면 trylock한 뒤 두 상태를 다시 확인합니다. mapping도 다시 읽습니다. 제출 성공을 동기 storage 완료나 folio free와 같은 뜻으로 사용할 수 없습니다.

lazy-free는 isolation reference 하나를 0으로 freeze할 수 있는지 검사합니다. cache folio는 `__remove_mapping()`에서 예상 reference 수를 freeze하고 dirty를 재검사하여 제거합니다. v6.18 swap cache는 cluster lock과 swap table을 사용하고, file cache는 inode lock·XArray lock을 사용합니다. 이 둘의 lock을 같은 것으로 읽으면 안 됩니다.

## Free batch는 중간에도 비울 수 있습니다

v6.18은 free할 folio를 `struct folio_batch`에 모읍니다. batch가 차면 memcg uncharge와 필요한 TLB flush 뒤 `free_unref_folios()`로 비우며, deferred split queue에서도 먼저 제거합니다. 끝에 남은 batch도 같은 순서로 정리합니다. 보존한 folio는 반환 list로 옮겨 caller가 LRU에 복귀시킬 수 있도록 합니다.

이 전체 흐름에서 scan량, pageout 제출량, demotion량, reclaimed accounting, 실제 allocator 반환량은 관련 있지만 동일한 값이 아닙니다. 반환 counter 하나만으로 어느 storage I/O가 끝났는지, 원하는 allocation order가 확보됐는지까지 판단할 수는 없습니다.

## 전체 code로 분기와 정리를 연결합니다

아래는 Linux v6.18의 전체 함수입니다. kernel 내부 자료구조와 configuration에 의존하는 구현이며 standalone 프로그램은 아닙니다. 고전 `shrink_page_list()`의 전체 구현은 [inactive reclaim 글](09-inactive-reclaim.md)에서 함께 읽을 수 있습니다.

[Linux v6.18 원본](https://github.com/torvalds/linux/blob/v6.18/mm/vmscan.c#L1099) · [전체 code](kernel/v6_18_shrink_folio_list.c) · [GPL-2.0](kernel/GPL-2.0.txt)

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

## 참고 자료

- [Linux v6.18 mm/vmscan.c](https://github.com/torvalds/linux/blob/v6.18/mm/vmscan.c): `shrink_folio_list()`, `pageout()`, `__remove_mapping()`.
- [Linux v6.18 mm/swap_state.c](https://github.com/torvalds/linux/blob/v6.18/mm/swap_state.c), [mm/swapfile.c](https://github.com/torvalds/linux/blob/v6.18/mm/swapfile.c): swap cache와 slot lifetime.
- [Linux v6.18 Swap Table](https://github.com/torvalds/linux/blob/v6.18/Documentation/mm/swap-table.rst).
