# Node reclaim은 왜 같은 folio를 더 빨리 돌기보다 기다릴까요

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()` 원본](https://github.com/torvalds/linux/blob/v6.12/mm/vmscan.c#L5936) · [전체 code](kernel/v6_12_shrink_node.c) · [GPL-2.0](kernel/GPL-2.0.txt)

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

## 더 회수할지 compaction으로 돌아갈지

`in_reclaim_compaction()`은 compaction이 구성되어 있고 order가 0이 아니어야 합니다. 여기에 `order > PAGE_ALLOC_COSTLY_ORDER`이거나 `priority < DEF_PRIORITY - 2`가 필요합니다. 기본값에서는 order-4 이상이거나, nonzero order에서 priority 9 이하인 상황입니다. 호출 횟수를 세는 조건은 아닙니다.

[v6.12 mode 판정](https://github.com/torvalds/linux/blob/v6.12/mm/vmscan.c#L5788) · [전체 code](kernel/v6_12_in_reclaim_compaction.c)

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

`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 계속 여부 판정](https://github.com/torvalds/linux/blob/v6.12/mm/vmscan.c#L5805) · [전체 code](kernel/v6_12_should_continue_reclaim.c)

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

계속한다면 `again`에서 이번 pass 통계를 새로 모읍니다. 종료할 때 어느 pass에서든 실제 회수가 있었다면 `kswapd_failures`를 0으로 돌립니다. 성공한 direct reclaim도 잠들어 있던 kswapd의 실패 이력을 회복할 수 있습니다. 진전이 없고 cache trim이었다면 trim 실패를 기록하여 caller가 정책을 바꿀 근거를 남깁니다. 어느 branch도 다음 allocation의 성공을 미리 보장하지는 않습니다.

## 참고 자료

- [Linux v6.12 mm/vmscan.c](https://github.com/torvalds/linux/blob/v6.12/mm/vmscan.c): scan 준비, node reclaim, writeback feedback, compaction 연계입니다.
- [Linux v6.18 mm/vmscan.c](https://github.com/torvalds/linux/blob/v6.18/mm/vmscan.c): 이후 version의 비교 기준입니다.
- [Code provenance](kernel/provenance.json), [GPL-2.0 전문](kernel/GPL-2.0.txt).
