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의 두 전체 helper입니다.

wbc_end_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 pgoff_t wbc_end(struct writeback_control *wbc)
{
	if (wbc->range_cyclic)
		return -1;
	return wbc->range_end >> PAGE_SHIFT;
}

wbc_to_tag_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 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하고 다음 대상을 찾습니다.

writeback_get_folio_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 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 완료로 읽지 않습니다.

writeback_iter_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.
 */

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 갱신 조건에 참여합니다.

write_cache_pages_v6_0.c

// 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에도 기록합니다.

generic_writepages_v6_0.c

// 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;
}

__writepage_v6_0.c

// 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().