# Writeback iterator는 오류가 나도 다음 folio를 찾을 수 있습니다

writeback을 단순히 dirty page를 찾아 writepage를 호출하는 loop로 생각하면, 중간의 오류와 이미 진행 중인 I/O를 처리하기 어렵습니다. background 작업은 지금 가능한 양만 처리해도 되지만 fsync 같은 data-integrity 작업은 선택한 집합을 끝까지 살펴야 합니다. 끝내는 조건이 다르면 참조를 놓고 다음 검색 위치를 남기는 방식도 달라집니다.

Linux v6.18은 `writeback_iter()`와 `writeback_control` 안의 folio batch를 사용합니다. 예전 `write_cache_pages()`의 pagevec loop와 목표는 이어지지만 같은 함수 이름이나 callback 계약은 아닙니다. 두 전체 구현을 비교하면서 범위·tag·lock·오류가 어떤 순서로 연결되는지 보겠습니다.

## 먼저 검색 범위와 tag를 정합니다

byte 단위인 `range_end`는 inclusive입니다. iterator의 index는 page 단위이며 cyclic이면 `mapping->writeback_index`에서, 아니면 `range_start >> PAGE_SHIFT`에서 시작합니다. cyclic 끝은 unsigned 최대 index이고 일반 범위의 끝은 byte 끝을 page index로 바꾼 값입니다.

`WB_SYNC_ALL` 또는 `tagged_writepages`이면 DIRTY만 계속 쫓지 않고 TOWRITE 집합을 먼저 표시합니다. 그렇지 않으면 DIRTY mark를 검색합니다. 다음은 [v6.18 `mm/page-writeback.c`](https://github.com/torvalds/linux/blob/v6.18/mm/page-writeback.c)의 두 전체 helper입니다.

<!-- include-code:code/wbc_end_v6_18.c -->

<!-- include-code:code/wbc_to_tag_v6_18.c -->

`tag_pages_for_writeback()`은 XArray IRQ lock 아래에서 DIRTY인 entry에 TOWRITE mark를 붙입니다. 일정 수마다 cursor를 pause하고 lock을 놓은 뒤 reschedule하여 긴 범위가 lock을 독점하지 않게 합니다. 다시 lock을 잡고 순회를 이어가므로 byte 내용이나 모든 entry를 한 시점에 복제한 snapshot이 아닙니다.

<!-- include-code:code/tag_pages_for_writeback_v6_18.c -->

TOWRITE는 concurrent dirtying으로 검색이 끝없이 늘어나는 것을 피하려는 선택입니다. 이미 고른 대상을 다른 경로가 임의로 검색에서 빼서도 안 됩니다. DIRTY를 지우며 I/O로 넘기는 전환과 함께 TOWRITE를 정리하는 규칙을 지켜야 합니다.

## Lookup 참조만으로 원래 mapping을 믿지 않습니다

`writeback_get_folio()`는 남은 batch에서 다음 folio를 꺼내고, batch가 끝나면 이전 참조들을 release한 뒤 tag lookup으로 새 batch를 얻습니다. 더 없으면 NULL입니다. 실제 folio에는 lock을 얻은 뒤 preparation을 수행합니다. 준비할 수 없다면 unlock하고 다음 대상을 찾습니다.

<!-- include-code:code/writeback_get_folio_v6_18.c -->

`folio_prepare_writeback()`는 아직 같은 mapping에 속하는지와 dirty인지부터 확인합니다. truncate되어 빠져나간 folio라면 같은 index에 새로운 dirty folio가 있어도 예전 pointer를 그 새 대상으로 취급할 수 없습니다. ref가 있는 객체가 살아 있다는 사실과 cache membership이 같다는 사실은 다릅니다.

진행 중 writeback은 background에서는 건너뜁니다. integrity 처리에서는 기다린 뒤 새 dirty를 처리할 수 있어야 합니다. 이 대기도 I/O 성공 판정은 아니며 오류 보고는 별도입니다. 마지막 dirty clear가 false면 이번 I/O 대상으로 넘기지 않습니다.

<!-- include-code:code/folio_prepare_writeback_v6_18.c -->

## Iterator는 이전 folio의 결과도 받습니다

첫 호출에는 folio NULL을 전달합니다. 이후에는 직전 반환 folio와 그 처리의 error를 다시 전달합니다. iterator가 새 folio만 반환하는 stateless lookup이 아니라 이전 진행량을 마무리하는 API인 이유입니다. large folio는 base-page 수만큼 `nr_to_write`에서 뺍니다.

background는 error 또는 quota 소진에서 종료할 수 있습니다. `WB_SYNC_ALL`은 첫 error를 `saved_err`에 보관하되 선택한 대상의 처리를 계속합니다. filesystem이 각 folio에 대해 정리해야 할 상태가 남을 수 있기 때문입니다. error가 났다고 caller가 loop에서 곧바로 break하면 batch 참조와 iterator 종료 처리를 건너뛸 수 있습니다. 이 API는 NULL이 나올 때까지 계속 호출해야 합니다.

아래는 [v6.18 `writeback_iter()` 전체](https://github.com/torvalds/linux/blob/v6.18/mm/page-writeback.c#L2504-L2590)입니다. 양수 error는 정상 errno 계약이 아니므로 경고합니다. caller는 folio의 실제 제출·unlock 등 자기 filesystem의 책임을 수행해야 하며 iterator 호출 자체를 I/O 완료로 읽지 않습니다.

<!-- include-code:code/writeback_iter_v6_18.c -->

범위를 끝까지 보았다면 cyclic 다음 위치를 0으로 남기고 종료합니다. 같은 호출에서 즉시 파일 처음으로 돌아가지는 않습니다. caller가 I/O를 묶으려고 높은 index의 writeback 상태를 유지하는 동안 낮은 index를 다시 잠그면 lock·writeback 순서가 역전될 수 있기 때문입니다. 반면 quota나 error로 일찍 끝난 cyclic 경로는 마지막 처리 folio의 다음 index를 저장하고 batch 참조를 정리합니다.

## 예전 pagevec loop의 전체 코드와 비교합니다

다음은 [v6.0 `write_cache_pages()` 전체](https://github.com/torvalds/linux/blob/v6.0/mm/page-writeback.c)입니다. 책의 정확한 kernel tag를 이 tag라고 단정하지는 않습니다. pagevec 초기화, TOWRITE 선택, mapping 재검사와 dirty clear, callback 오류 처리, cyclic 후처리를 생략하지 않은 비교 구현입니다.

이 버전의 `pagevec`는 [v6.0 `pagevec.h`](https://github.com/torvalds/linux/blob/v6.0/include/linux/pagevec.h)에서 `PAGEVEC_SIZE` 15개의 page pointer를 담습니다. page pointer batch를 current large-folio batch와 counter까지 완전히 같다고 일반화하지 않습니다. loop 안의 lookup index는 다음 검색 위치로 전진하므로 마지막 실제 처리 위치는 `done_index`에 따로 남깁니다. 범위 전체 검색 여부인 `range_whole`도 이후 `mapping->writeback_index` 갱신 조건에 참여합니다.

<!-- include-code:code/write_cache_pages_v6_0.c -->

이 v6.0 함수의 cyclic 후처리는 `range_cyclic && !done`일 때 다음 시작 위치를 0으로 바꿉니다. 여기서 `!done`은 목표량을 모두 썼다는 뜻이 아니라 scan이 범위 끝까지 갔다는 경우와 연결됩니다. quota나 background error로 일찍 끝났다면 별도로 기록한 `done_index`에서 이어갑니다. `mapping->writeback_index`를 갱신하는 조건은 cyclic이거나, non-cyclic이지만 전체 byte 범위를 요구했고 `nr_to_write > 0`이 남은 경우입니다. 모든 non-cyclic write가 이 cursor를 갱신하는 것은 아닙니다. 이 control의 실제 필드명도 `nr_to_write`이지 `nr_pages`가 아닙니다.

예전 callback에 넘기는 `data`는 filesystem이 고른 부가 pointer입니다. generic 경로는 mapping을 넣습니다. `AOP_WRITEPAGE_ACTIVATE`이면 caller가 아직 locked인 page를 unlock하고 일반 errno와 다르게 처리합니다. 이 값이 device I/O를 성공적으로 마쳤다는 뜻은 아닙니다. 일반 background 오류는 다음 index로 넘겨 종료하고, integrity 경로는 첫 오류를 보관하면서 나머지 집합을 처리합니다.

예전 `generic_writepages()`와 callback wrapper도 전체를 함께 볼 수 있습니다. writepage callback이 없으면 처리 없음으로 돌아가고, 있으면 block plug 안에서 page loop를 수행합니다. `__writepage()`는 filesystem callback의 결과를 mapping error에도 기록합니다.

<!-- include-code:code/generic_writepages_v6_0.c -->

<!-- include-code:code/__writepage_v6_0.c -->

이 fallback을 v6.18 `do_writepages()`에도 그대로 있다고 설명하면 틀립니다. 현재 진입점은 writepages가 없는 특수 파일을 no-op으로 처리합니다. 버전을 나누어 읽어야 예전 `a_ops->writepage`를 현재 모든 filesystem에서 반드시 찾으려는 혼동을 피할 수 있습니다.

## 선택한 집합과 저장한 내용은 다릅니다

TOWRITE로 집합을 골랐더라도 folio는 lookup과 lock 사이에 제거될 수 있고, byte 내용도 이후 수정될 수 있습니다. folio lock 아래의 재검사, shared PTE의 clean·write-protect, dirty·writeback 상태 전환이 따로 필요한 이유입니다.

따라서 loop 종료와 I/O 완료도 구별해야 합니다. iterator가 NULL을 반환했다는 것은 선택과 후처리가 끝났다는 뜻입니다. sync caller는 이미 제출한 I/O와 filesystem metadata, 오류를 자기 계약에 맞게 마저 처리해야 합니다. 이 구분을 지키면 error 뒤에도 왜 다음 folio를 처리하는지, cyclic인데도 왜 즉시 처음으로 돌아가지 않는지가 같은 제어 흐름 안에서 설명됩니다.

## 참고 자료

- Lorenzo Stoakes, *The Linux Memory Manager*, Early Access 2025, Chapter 10의 `writeback_control`과 page-cache writeback loop 설명.
- [Linux v6.18 `mm/page-writeback.c`](https://github.com/torvalds/linux/blob/v6.18/mm/page-writeback.c): tag, folio preparation, iterator의 전체 정의와 호출 계약.
- [Linux v6.0 `mm/page-writeback.c`](https://github.com/torvalds/linux/blob/v6.0/mm/page-writeback.c): pagevec 기반 `write_cache_pages()`, `generic_writepages()`, `__writepage()`.
