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의 두 전체 helper입니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete scope from Linux v6.18 mm/page-writeback.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/page-writeback.c
* Copyright (C) 2002, Linus Torvalds.
* Copyright (C) 2007 Red Hat, Inc., Peter Zijlstra
* 10Apr2002 Andrew Morton -- Initial version.
* Kernel excerpt; not a standalone program.
* The book's exact source tag is not asserted.
*/
static pgoff_t wbc_end(struct writeback_control *wbc)
{
if (wbc->range_cyclic)
return -1;
return wbc->range_end >> PAGE_SHIFT;
}
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete scope from Linux v6.18 mm/page-writeback.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/page-writeback.c
* Copyright (C) 2002, Linus Torvalds.
* Copyright (C) 2007 Red Hat, Inc., Peter Zijlstra
* 10Apr2002 Andrew Morton -- Initial version.
* Kernel excerpt; not a standalone program.
* The book's exact source tag is not asserted.
*/
static xa_mark_t wbc_to_tag(struct writeback_control *wbc)
{
if (wbc->sync_mode == WB_SYNC_ALL || wbc->tagged_writepages)
return PAGECACHE_TAG_TOWRITE;
return PAGECACHE_TAG_DIRTY;
}
tag_pages_for_writeback()은 XArray IRQ lock 아래에서 DIRTY인 entry에 TOWRITE mark를 붙입니다. 일정 수마다 cursor를 pause하고 lock을 놓은 뒤 reschedule하여 긴 범위가 lock을 독점하지 않게 합니다. 다시 lock을 잡고 순회를 이어가므로 byte 내용이나 모든 entry를 한 시점에 복제한 snapshot이 아닙니다.
tag_pages_for_writeback_v6_18.c
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete scope from Linux v6.18 mm/page-writeback.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/page-writeback.c
* Copyright (C) 2002, Linus Torvalds.
* Copyright (C) 2007 Red Hat, Inc., Peter Zijlstra
* 10Apr2002 Andrew Morton -- Initial version.
* Kernel excerpt; not a standalone program.
* The book's exact source tag is not asserted.
*/
void tag_pages_for_writeback(struct address_space *mapping,
pgoff_t start, pgoff_t end)
{
XA_STATE(xas, &mapping->i_pages, start);
unsigned int tagged = 0;
void *page;
xas_lock_irq(&xas);
xas_for_each_marked(&xas, page, end, PAGECACHE_TAG_DIRTY) {
xas_set_mark(&xas, PAGECACHE_TAG_TOWRITE);
if (++tagged % XA_CHECK_SCHED)
continue;
xas_pause(&xas);
xas_unlock_irq(&xas);
cond_resched();
xas_lock_irq(&xas);
}
xas_unlock_irq(&xas);
}
TOWRITE는 concurrent dirtying으로 검색이 끝없이 늘어나는 것을 피하려는 선택입니다. 이미 고른 대상을 다른 경로가 임의로 검색에서 빼서도 안 됩니다. DIRTY를 지우며 I/O로 넘기는 전환과 함께 TOWRITE를 정리하는 규칙을 지켜야 합니다.
Lookup 참조만으로 원래 mapping을 믿지 않습니다
writeback_get_folio()는 남은 batch에서 다음 folio를 꺼내고, batch가 끝나면 이전 참조들을 release한 뒤 tag lookup으로 새 batch를 얻습니다. 더 없으면 NULL입니다. 실제 folio에는 lock을 얻은 뒤 preparation을 수행합니다. 준비할 수 없다면 unlock하고 다음 대상을 찾습니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete scope from Linux v6.18 mm/page-writeback.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/page-writeback.c
* Copyright (C) 2002, Linus Torvalds.
* Copyright (C) 2007 Red Hat, Inc., Peter Zijlstra
* 10Apr2002 Andrew Morton -- Initial version.
* Kernel excerpt; not a standalone program.
* The book's exact source tag is not asserted.
*/
static struct folio *writeback_get_folio(struct address_space *mapping,
struct writeback_control *wbc)
{
struct folio *folio;
retry:
folio = folio_batch_next(&wbc->fbatch);
if (!folio) {
folio_batch_release(&wbc->fbatch);
cond_resched();
filemap_get_folios_tag(mapping, &wbc->index, wbc_end(wbc),
wbc_to_tag(wbc), &wbc->fbatch);
folio = folio_batch_next(&wbc->fbatch);
if (!folio)
return NULL;
}
folio_lock(folio);
if (unlikely(!folio_prepare_writeback(mapping, wbc, folio))) {
folio_unlock(folio);
goto retry;
}
trace_wbc_writepage(wbc, inode_to_bdi(mapping->host));
return folio;
}
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 대상으로 넘기지 않습니다.
folio_prepare_writeback_v6_18.c
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete scope from Linux v6.18 mm/page-writeback.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/page-writeback.c
* Copyright (C) 2002, Linus Torvalds.
* Copyright (C) 2007 Red Hat, Inc., Peter Zijlstra
* 10Apr2002 Andrew Morton -- Initial version.
* Kernel excerpt; not a standalone program.
* The book's exact source tag is not asserted.
*/
static bool folio_prepare_writeback(struct address_space *mapping,
struct writeback_control *wbc, struct folio *folio)
{
/*
* Folio truncated or invalidated. We can freely skip it then,
* even for data integrity operations: the folio has disappeared
* concurrently, so there could be no real expectation of this
* data integrity operation even if there is now a new, dirty
* folio at the same pagecache index.
*/
if (unlikely(folio->mapping != mapping))
return false;
/*
* Did somebody else write it for us?
*/
if (!folio_test_dirty(folio))
return false;
if (folio_test_writeback(folio)) {
if (wbc->sync_mode == WB_SYNC_NONE)
return false;
folio_wait_writeback(folio);
}
BUG_ON(folio_test_writeback(folio));
if (!folio_clear_dirty_for_io(folio))
return false;
return true;
}
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() 전체입니다. 양수 error는 정상 errno 계약이 아니므로 경고합니다. caller는 folio의 실제 제출·unlock 등 자기 filesystem의 책임을 수행해야 하며 iterator 호출 자체를 I/O 완료로 읽지 않습니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete scope from Linux v6.18 mm/page-writeback.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/page-writeback.c
* Copyright (C) 2002, Linus Torvalds.
* Copyright (C) 2007 Red Hat, Inc., Peter Zijlstra
* 10Apr2002 Andrew Morton -- Initial version.
* Kernel excerpt; not a standalone program.
* The book's exact source tag is not asserted.
*/
struct folio *writeback_iter(struct address_space *mapping,
struct writeback_control *wbc, struct folio *folio, int *error)
{
if (!folio) {
folio_batch_init(&wbc->fbatch);
wbc->saved_err = *error = 0;
/*
* For range cyclic writeback we remember where we stopped so
* that we can continue where we stopped.
*
* For non-cyclic writeback we always start at the beginning of
* the passed in range.
*/
if (wbc->range_cyclic)
wbc->index = mapping->writeback_index;
else
wbc->index = wbc->range_start >> PAGE_SHIFT;
/*
* To avoid livelocks when other processes dirty new pages, we
* first tag pages which should be written back and only then
* start writing them.
*
* For data-integrity writeback we have to be careful so that we
* do not miss some pages (e.g., because some other process has
* cleared the TOWRITE tag we set). The rule we follow is that
* TOWRITE tag can be cleared only by the process clearing the
* DIRTY tag (and submitting the page for I/O).
*/
if (wbc->sync_mode == WB_SYNC_ALL || wbc->tagged_writepages)
tag_pages_for_writeback(mapping, wbc->index,
wbc_end(wbc));
} else {
wbc->nr_to_write -= folio_nr_pages(folio);
WARN_ON_ONCE(*error > 0);
/*
* For integrity writeback we have to keep going until we have
* written all the folios we tagged for writeback above, even if
* we run past wbc->nr_to_write or encounter errors.
* We stash away the first error we encounter in wbc->saved_err
* so that it can be retrieved when we're done. This is because
* the file system may still have state to clear for each folio.
*
* For background writeback we exit as soon as we run past
* wbc->nr_to_write or encounter the first error.
*/
if (wbc->sync_mode == WB_SYNC_ALL) {
if (*error && !wbc->saved_err)
wbc->saved_err = *error;
} else {
if (*error || wbc->nr_to_write <= 0)
goto done;
}
}
folio = writeback_get_folio(mapping, wbc);
if (!folio) {
/*
* To avoid deadlocks between range_cyclic writeback and callers
* that hold folios in writeback to aggregate I/O until
* the writeback iteration finishes, we do not loop back to the
* start of the file. Doing so causes a folio lock/folio
* writeback access order inversion - we should only ever lock
* multiple folios in ascending folio->index order, and looping
* back to the start of the file violates that rule and causes
* deadlocks.
*/
if (wbc->range_cyclic)
mapping->writeback_index = 0;
/*
* Return the first error we encountered (if there was any) to
* the caller.
*/
*error = wbc->saved_err;
}
return folio;
done:
if (wbc->range_cyclic)
mapping->writeback_index = folio_next_index(folio);
folio_batch_release(&wbc->fbatch);
return NULL;
}
범위를 끝까지 보았다면 cyclic 다음 위치를 0으로 남기고 종료합니다. 같은 호출에서 즉시 파일 처음으로 돌아가지는 않습니다. caller가 I/O를 묶으려고 높은 index의 writeback 상태를 유지하는 동안 낮은 index를 다시 잠그면 lock·writeback 순서가 역전될 수 있기 때문입니다. 반면 quota나 error로 일찍 끝난 cyclic 경로는 마지막 처리 folio의 다음 index를 저장하고 batch 참조를 정리합니다.
예전 pagevec loop의 전체 코드와 비교합니다
다음은 v6.0 write_cache_pages() 전체입니다. 책의 정확한 kernel tag를 이 tag라고 단정하지는 않습니다. pagevec 초기화, TOWRITE 선택, mapping 재검사와 dirty clear, callback 오류 처리, cyclic 후처리를 생략하지 않은 비교 구현입니다.
이 버전의 pagevec는 v6.0 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 갱신 조건에 참여합니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete scope from Linux v6.0 mm/page-writeback.c.
* Source: https://github.com/torvalds/linux/blob/v6.0/mm/page-writeback.c
* Copyright (C) 2002, Linus Torvalds.
* Copyright (C) 2007 Red Hat, Inc., Peter Zijlstra
* 10Apr2002 Andrew Morton -- Initial version.
* Kernel excerpt; not a standalone program.
* The book's exact source tag is not asserted.
*/
int write_cache_pages(struct address_space *mapping,
struct writeback_control *wbc, writepage_t writepage,
void *data)
{
int ret = 0;
int done = 0;
int error;
struct pagevec pvec;
int nr_pages;
pgoff_t index;
pgoff_t end; /* Inclusive */
pgoff_t done_index;
int range_whole = 0;
xa_mark_t tag;
pagevec_init(&pvec);
if (wbc->range_cyclic) {
index = mapping->writeback_index; /* prev offset */
end = -1;
} else {
index = wbc->range_start >> PAGE_SHIFT;
end = wbc->range_end >> PAGE_SHIFT;
if (wbc->range_start == 0 && wbc->range_end == LLONG_MAX)
range_whole = 1;
}
if (wbc->sync_mode == WB_SYNC_ALL || wbc->tagged_writepages) {
tag_pages_for_writeback(mapping, index, end);
tag = PAGECACHE_TAG_TOWRITE;
} else {
tag = PAGECACHE_TAG_DIRTY;
}
done_index = index;
while (!done && (index <= end)) {
int i;
nr_pages = pagevec_lookup_range_tag(&pvec, mapping, &index, end,
tag);
if (nr_pages == 0)
break;
for (i = 0; i < nr_pages; i++) {
struct page *page = pvec.pages[i];
done_index = page->index;
lock_page(page);
/*
* Page truncated or invalidated. We can freely skip it
* then, even for data integrity operations: the page
* has disappeared concurrently, so there could be no
* real expectation of this data integrity operation
* even if there is now a new, dirty page at the same
* pagecache address.
*/
if (unlikely(page->mapping != mapping)) {
continue_unlock:
unlock_page(page);
continue;
}
if (!PageDirty(page)) {
/* someone wrote it for us */
goto continue_unlock;
}
if (PageWriteback(page)) {
if (wbc->sync_mode != WB_SYNC_NONE)
wait_on_page_writeback(page);
else
goto continue_unlock;
}
BUG_ON(PageWriteback(page));
if (!clear_page_dirty_for_io(page))
goto continue_unlock;
trace_wbc_writepage(wbc, inode_to_bdi(mapping->host));
error = (*writepage)(page, wbc, data);
if (unlikely(error)) {
/*
* Handle errors according to the type of
* writeback. There's no need to continue for
* background writeback. Just push done_index
* past this page so media errors won't choke
* writeout for the entire file. For integrity
* writeback, we must process the entire dirty
* set regardless of errors because the fs may
* still have state to clear for each page. In
* that case we continue processing and return
* the first error.
*/
if (error == AOP_WRITEPAGE_ACTIVATE) {
unlock_page(page);
error = 0;
} else if (wbc->sync_mode != WB_SYNC_ALL) {
ret = error;
done_index = page->index + 1;
done = 1;
break;
}
if (!ret)
ret = error;
}
/*
* We stop writing back only if we are not doing
* integrity sync. In case of integrity sync we have to
* keep going until we have written all the pages
* we tagged for writeback prior to entering this loop.
*/
if (--wbc->nr_to_write <= 0 &&
wbc->sync_mode == WB_SYNC_NONE) {
done = 1;
break;
}
}
pagevec_release(&pvec);
cond_resched();
}
/*
* If we hit the last page and there is more work to be done: wrap
* back the index back to the start of the file for the next
* time we are called.
*/
if (wbc->range_cyclic && !done)
done_index = 0;
if (wbc->range_cyclic || (range_whole && wbc->nr_to_write > 0))
mapping->writeback_index = done_index;
return ret;
}
이 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에도 기록합니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete scope from Linux v6.0 mm/page-writeback.c.
* Source: https://github.com/torvalds/linux/blob/v6.0/mm/page-writeback.c
* Copyright (C) 2002, Linus Torvalds.
* Copyright (C) 2007 Red Hat, Inc., Peter Zijlstra
* 10Apr2002 Andrew Morton -- Initial version.
* Kernel excerpt; not a standalone program.
* The book's exact source tag is not asserted.
*/
int generic_writepages(struct address_space *mapping,
struct writeback_control *wbc)
{
struct blk_plug plug;
int ret;
/* deal with chardevs and other special file */
if (!mapping->a_ops->writepage)
return 0;
blk_start_plug(&plug);
ret = write_cache_pages(mapping, wbc, __writepage, mapping);
blk_finish_plug(&plug);
return ret;
}
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete scope from Linux v6.0 mm/page-writeback.c.
* Source: https://github.com/torvalds/linux/blob/v6.0/mm/page-writeback.c
* Copyright (C) 2002, Linus Torvalds.
* Copyright (C) 2007 Red Hat, Inc., Peter Zijlstra
* 10Apr2002 Andrew Morton -- Initial version.
* Kernel excerpt; not a standalone program.
* The book's exact source tag is not asserted.
*/
static int __writepage(struct page *page, struct writeback_control *wbc,
void *data)
{
struct address_space *mapping = data;
int ret = mapping->a_ops->writepage(page, wbc);
mapping_set_error(mapping, ret);
return ret;
}
이 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: tag, folio preparation, iterator의 전체 정의와 호출 계약. - Linux v6.0
mm/page-writeback.c: pagevec 기반write_cache_pages(),generic_writepages(),__writepage().