Writeback을 마친 inode는 어느 목록으로 돌아갈까요?

flusher가 inode 하나를 처리했다고 그 inode를 무조건 clean 목록으로 보내지는 않습니다. quota를 다 썼을 수도 있고, filesystem이 일부 folio의 제출을 거절했을 수도 있으며, I/O를 만드는 동안 metadata가 다시 dirty가 되었을 수도 있습니다.

이후의 배치는 단순 cleanup이 아닙니다. 어떤 inode에 곧바로 다음 기회를 줄지, 어떤 것은 잠시 미룰지, 어떤 것은 dirty 목록에서 빼도 되는지를 결정합니다. 이 정책은 inode의 수명과 lock 순서에도 맞물립니다.

superblock을 보호한 범위만 처리합니다

writeback_sb_inodes()는 특정 superblock을 처리합니다. caller가 unmount를 막는 보호를 확보했으므로 다른 superblock의 inode를 우연히 같이 처리하면 안 됩니다.

명시적으로 work->sb가 지정된 요청에서는 다른 superblock의 inode를 dirty 쪽으로 돌려보내고 계속 찾습니다. 여러 filesystem을 처리하는 일반 요청이라면 다른 superblock 묶음을 만났을 때 caller로 돌아가 현재 보호를 놓고 다음 superblock의 보호를 얻습니다. 앞서 queue 단계에서 같은 superblock끼리 모으는 이유가 여기서 드러납니다.

여러 superblock을 도는 상위 경로는 즉시 lock을 얻지 못할 때 해당 inode를 미루어 다른 진행을 허용할 수 있습니다. 이것은 모든 inode를 기다리지 않고 건너뛴다는 일반 규칙이 아니라 flusher의 해당 trylock 경로에 대한 선택입니다. v6.18 fs/fs-writeback.c

아래는 v6.18 __writeback_inodes_wb() 전체입니다. 과거 이름인 trylock_super() 대신 현재는 super_trylock_shared()를 호출합니다. 성공하면 unmount 방지용 shared s_umount를 가진 상태에서 inode 묶음을 처리하고 up_read()로 놓습니다. 실패하면 즉시 다시 검사하는 b_more_io로 보내지 않고 b_dirty 쪽으로 미룹니다. 같은 superblock lock을 busy retry하는 일을 피하기 위해서입니다.

__writeback_inodes_wb_v6_18.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 * Complete scope from Linux v6.18 fs/fs-writeback.c.
 * Source: https://github.com/torvalds/linux/blob/v6.18/fs/fs-writeback.c
 * Kernel excerpt; not a standalone program.
 * The book exact source tag is not asserted.
 */

/*
 * fs/fs-writeback.c
 *
 * Copyright (C) 2002, Linus Torvalds.
 *
 * Contains all the functions related to writing back and waiting
 * upon dirty inodes against superblocks, and writing back dirty
 * pages against inodes.  ie: data writeback.  Writeout of the
 * inode itself is not handled here.
 *
 * 10Apr2002	Andrew Morton
 *		Split out of fs/inode.c
 *		Additions for address_space-based writeback
 */

static long __writeback_inodes_wb(struct bdi_writeback *wb,
				  struct wb_writeback_work *work)
{
	unsigned long start_time = jiffies;
	long wrote = 0;

	while (!list_empty(&wb->b_io)) {
		struct inode *inode = wb_inode(wb->b_io.prev);
		struct super_block *sb = inode->i_sb;

		if (!super_trylock_shared(sb)) {
			/*
			 * super_trylock_shared() may fail consistently due to
			 * s_umount being grabbed by someone else. Don't use
			 * requeue_io() to avoid busy retrying the inode/sb.
			 */
			redirty_tail(inode, wb);
			continue;
		}
		wrote += writeback_sb_inodes(sb, wb, work);
		up_read(&sb->s_umount);

		/* refer to the same tests at the end of writeback_sb_inodes */
		if (wrote) {
			if (time_is_before_jiffies(start_time + HZ / 10UL))
				break;
			if (work->nr_pages <= 0)
				break;
		}
	}
	/* Leave any unwritten inodes on b_io */
	return wrote;
}

이 함수의 wrote에는 page뿐 아니라 clean으로 정리한 inode의 진전도 합산됩니다. 한 page라도 저장되었다고 판단하는 엄격한 단위가 아닙니다. HZ / 10 검사도 진전 후 상위 loop로 돌아갈 기회를 만드는 조건이지, 한 번의 I/O나 함수 실행이 반드시 0.1초 안에 끝나는 hard deadline은 아닙니다.

I_SYNC를 기다린 뒤 pointer를 재사용하지 않습니다

inode의 i_lock 아래에서 I_NEW, I_FREEING, I_WILL_FREE를 확인하고 현재 작업에 적절하지 않으면 미룹니다. 이미 I_SYNC가 설정되어 다른 inode writeback이 진행 중인 경우도 있습니다.

WB_SYNC_NONE이면 b_more_io로 옮겨 다른 inode를 먼저 처리할 수 있습니다. integrity 작업이라면 기다려야 합니다. 이때 사용하는 inode_sleep_on_writeback()은 i_lock을 놓으며, caller가 별도 inode 참조를 가지고 있다는 보장이 없습니다. 기다리는 동안 inode가 사라질 수 있어 반환 후 기존 pointer로 그대로 이어가지 않고 목록부터 다시 시작합니다.

이것은 참조를 유지하는 inode_wait_for_writeback()의 계약과 다릅니다. helper 이름이 비슷하다는 이유로 lock·ref 조건을 교환해서 사용할 수는 없습니다. 정상 처리에서는 I_SYNC를 먼저 세워 eviction이 해당 inode를 해제하지 못하도록 하고 긴 I/O 경로로 내려갑니다.

다음은 inode_sleep_on_writeback() 전체입니다. waiter를 준비한 뒤 i_lock 아래에서 bit를 확인하고 lock을 놓습니다. 실제 sleep이 필요 없는 경우도 있지만 caller에게 inode 참조를 새로 주지는 않습니다. 깨어났다는 사실만으로 pointer를 다시 dereference하거나 folio I/O가 성공했다고 판단하지 않습니다.

inode_sleep_on_writeback_v6_18.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 * Complete scope from Linux v6.18 fs/fs-writeback.c.
 * Source: https://github.com/torvalds/linux/blob/v6.18/fs/fs-writeback.c
 * Kernel excerpt; not a standalone program.
 * The book exact source tag is not asserted.
 */

/*
 * fs/fs-writeback.c
 *
 * Copyright (C) 2002, Linus Torvalds.
 *
 * Contains all the functions related to writing back and waiting
 * upon dirty inodes against superblocks, and writing back dirty
 * pages against inodes.  ie: data writeback.  Writeout of the
 * inode itself is not handled here.
 *
 * 10Apr2002	Andrew Morton
 *		Split out of fs/inode.c
 *		Additions for address_space-based writeback
 */

static void inode_sleep_on_writeback(struct inode *inode)
	__releases(inode->i_lock)
{
	struct wait_bit_queue_entry wqe;
	struct wait_queue_head *wq_head;
	bool sleep;

	assert_spin_locked(&inode->i_lock);

	wq_head = inode_bit_waitqueue(&wqe, inode, __I_SYNC);
	prepare_to_wait_event(wq_head, &wqe.wq_entry, TASK_UNINTERRUPTIBLE);
	/* Checking I_SYNC with inode->i_lock guarantees memory ordering. */
	sleep = !!(inode->i_state & I_SYNC);
	spin_unlock(&inode->i_lock);
	if (sleep)
		schedule();
	finish_wait(wq_head, &wqe.wq_entry);
}

긴 I/O 중에는 목록 spinlock을 놓습니다

함수 진입 시에는 wb->list_lock을 가지고 있지만 실제 inode I/O를 수행하는 동안 이 lock을 계속 들고 있지 않습니다. per-inode control을 준비하고 chunk 크기를 정한 뒤 __writeback_single_inode()로 내려갑니다.

writeback_control은 work의 sync·background·cyclic 조건을 이어받고 inode 전체 byte 범위를 지정합니다. nr_to_write는 이 차례의 chunk이며, pages_skipped는 0으로 시작합니다. 끝나면 차감한 page 수와 건너뛴 양을 구별해 진전을 집계합니다. 이 수는 device에서 성공적으로 영구 저장한 byte를 직접 세는 counter가 아닙니다.

cgroup writeback을 사용하는 경우 처리 도중 inode의 소속 writeback 객체가 바뀔 수 있습니다. 후처리에서는 다시 inode의 wb를 얻고 그 목록 lock을 잡아야 합니다. 처음의 wb pointer에 영구히 고정되어 있다고 가정하면 다른 목록을 잘못 건드릴 수 있습니다.

아래는 v6.18 writeback_sb_inodes() 전체입니다. 초기 검사·대기·실제 호출·후처리를 하나의 scope로 볼 수 있습니다. 함수의 진입과 반환에는 caller의 wb->list_lock 보유가 연결되지만, 내부의 긴 I/O와 reschedule 중까지 그 spinlock을 유지하지 않습니다.

writeback_sb_inodes_v6_18.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 * Complete scope from Linux v6.18 fs/fs-writeback.c.
 * Source: https://github.com/torvalds/linux/blob/v6.18/fs/fs-writeback.c
 * Kernel excerpt; not a standalone program.
 * The book exact source tag is not asserted.
 */

/*
 * fs/fs-writeback.c
 *
 * Copyright (C) 2002, Linus Torvalds.
 *
 * Contains all the functions related to writing back and waiting
 * upon dirty inodes against superblocks, and writing back dirty
 * pages against inodes.  ie: data writeback.  Writeout of the
 * inode itself is not handled here.
 *
 * 10Apr2002	Andrew Morton
 *		Split out of fs/inode.c
 *		Additions for address_space-based writeback
 */

static long writeback_sb_inodes(struct super_block *sb,
				struct bdi_writeback *wb,
				struct wb_writeback_work *work)
{
	struct writeback_control wbc = {
		.sync_mode		= work->sync_mode,
		.tagged_writepages	= work->tagged_writepages,
		.for_kupdate		= work->for_kupdate,
		.for_background		= work->for_background,
		.for_sync		= work->for_sync,
		.range_cyclic		= work->range_cyclic,
		.range_start		= 0,
		.range_end		= LLONG_MAX,
	};
	unsigned long start_time = jiffies;
	long write_chunk;
	long total_wrote = 0;  /* count both pages and inodes */
	unsigned long dirtied_before = jiffies;

	if (work->for_kupdate)
		dirtied_before = jiffies -
			msecs_to_jiffies(dirty_expire_interval * 10);

	while (!list_empty(&wb->b_io)) {
		struct inode *inode = wb_inode(wb->b_io.prev);
		struct bdi_writeback *tmp_wb;
		long wrote;

		if (inode->i_sb != sb) {
			if (work->sb) {
				/*
				 * We only want to write back data for this
				 * superblock, move all inodes not belonging
				 * to it back onto the dirty list.
				 */
				redirty_tail(inode, wb);
				continue;
			}

			/*
			 * The inode belongs to a different superblock.
			 * Bounce back to the caller to unpin this and
			 * pin the next superblock.
			 */
			break;
		}

		/*
		 * Don't bother with new inodes or inodes being freed, first
		 * kind does not need periodic writeout yet, and for the latter
		 * kind writeout is handled by the freer.
		 */
		spin_lock(&inode->i_lock);
		if (inode->i_state & (I_NEW | I_FREEING | I_WILL_FREE)) {
			redirty_tail_locked(inode, wb);
			spin_unlock(&inode->i_lock);
			continue;
		}
		if ((inode->i_state & I_SYNC) && wbc.sync_mode != WB_SYNC_ALL) {
			/*
			 * If this inode is locked for writeback and we are not
			 * doing writeback-for-data-integrity, move it to
			 * b_more_io so that writeback can proceed with the
			 * other inodes on s_io.
			 *
			 * We'll have another go at writing back this inode
			 * when we completed a full scan of b_io.
			 */
			requeue_io(inode, wb);
			spin_unlock(&inode->i_lock);
			trace_writeback_sb_inodes_requeue(inode);
			continue;
		}
		spin_unlock(&wb->list_lock);

		/*
		 * We already requeued the inode if it had I_SYNC set and we
		 * are doing WB_SYNC_NONE writeback. So this catches only the
		 * WB_SYNC_ALL case.
		 */
		if (inode->i_state & I_SYNC) {
			/* Wait for I_SYNC. This function drops i_lock... */
			inode_sleep_on_writeback(inode);
			/* Inode may be gone, start again */
			spin_lock(&wb->list_lock);
			continue;
		}
		inode->i_state |= I_SYNC;
		wbc_attach_and_unlock_inode(&wbc, inode);

		write_chunk = writeback_chunk_size(wb, work);
		wbc.nr_to_write = write_chunk;
		wbc.pages_skipped = 0;

		/*
		 * We use I_SYNC to pin the inode in memory. While it is set
		 * evict_inode() will wait so the inode cannot be freed.
		 */
		__writeback_single_inode(inode, &wbc);

		wbc_detach_inode(&wbc);
		work->nr_pages -= write_chunk - wbc.nr_to_write;
		wrote = write_chunk - wbc.nr_to_write - wbc.pages_skipped;
		wrote = wrote < 0 ? 0 : wrote;
		total_wrote += wrote;

		if (need_resched()) {
			/*
			 * We're trying to balance between building up a nice
			 * long list of IOs to improve our merge rate, and
			 * getting those IOs out quickly for anyone throttling
			 * in balance_dirty_pages().  cond_resched() doesn't
			 * unplug, so get our IOs out the door before we
			 * give up the CPU.
			 */
			blk_flush_plug(current->plug, false);
			cond_resched();
		}

		/*
		 * Requeue @inode if still dirty.  Be careful as @inode may
		 * have been switched to another wb in the meantime.
		 */
		tmp_wb = inode_to_wb_and_lock_list(inode);
		spin_lock(&inode->i_lock);
		if (!(inode->i_state & I_DIRTY_ALL))
			total_wrote++;
		requeue_inode(inode, tmp_wb, &wbc, dirtied_before);
		inode_sync_complete(inode);
		spin_unlock(&inode->i_lock);

		if (unlikely(tmp_wb != wb)) {
			spin_unlock(&tmp_wb->list_lock);
			spin_lock(&wb->list_lock);
		}

		/*
		 * bail out to wb_writeback() often enough to check
		 * background threshold and other termination conditions.
		 */
		if (total_wrote) {
			if (time_is_before_jiffies(start_time + HZ / 10UL))
				break;
			if (work->nr_pages <= 0)
				break;
		}
	}
	return total_wrote;
}

write_chunk - wbc.nr_to_write는 work quota에서 차감합니다. 진전량에는 그 차이에서 pages_skipped를 빼고 0 아래를 잘라 사용합니다. inode가 더는 I_DIRTY_ALL이 아니면 별도로 1을 더하므로 반환값은 pages와 inodes를 함께 세는 진행 지표입니다. 이 caller는 __writeback_single_inode()의 반환 errno를 곧바로 자기 반환값으로 전달하지 않습니다. I/O error 기록과 나중 sync에서의 보고 경로를 함께 보아야 합니다.

chunk를 정하는 전체 helper는 다음과 같습니다. WB_SYNC_ALL 또는 tagged 작업은 LONG_MAX를 사용합니다. 일반 작업은 평균 bandwidth의 절반, dirty limit의 일부, work 잔여량을 고려하고 MIN_WRITEBACK_PAGES 단위로 조정합니다. 조정 때문에 결과가 단순한 byte quota나 모든 inode에 동일한 고정 page 수는 아닙니다.

writeback_chunk_size_v6_18.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 * Complete scope from Linux v6.18 fs/fs-writeback.c.
 * Source: https://github.com/torvalds/linux/blob/v6.18/fs/fs-writeback.c
 * Kernel excerpt; not a standalone program.
 * The book exact source tag is not asserted.
 */

/*
 * fs/fs-writeback.c
 *
 * Copyright (C) 2002, Linus Torvalds.
 *
 * Contains all the functions related to writing back and waiting
 * upon dirty inodes against superblocks, and writing back dirty
 * pages against inodes.  ie: data writeback.  Writeout of the
 * inode itself is not handled here.
 *
 * 10Apr2002	Andrew Morton
 *		Split out of fs/inode.c
 *		Additions for address_space-based writeback
 */

static long writeback_chunk_size(struct bdi_writeback *wb,
				 struct wb_writeback_work *work)
{
	long pages;

	/*
	 * WB_SYNC_ALL mode does livelock avoidance by syncing dirty
	 * inodes/pages in one big loop. Setting wbc.nr_to_write=LONG_MAX
	 * here avoids calling into writeback_inodes_wb() more than once.
	 *
	 * The intended call sequence for WB_SYNC_ALL writeback is:
	 *
	 *      wb_writeback()
	 *          writeback_sb_inodes()       <== called only once
	 *              write_cache_pages()     <== called once for each inode
	 *                   (quickly) tag currently dirty pages
	 *                   (maybe slowly) sync all tagged pages
	 */
	if (work->sync_mode == WB_SYNC_ALL || work->tagged_writepages)
		pages = LONG_MAX;
	else {
		pages = min(wb->avg_write_bandwidth / 2,
			    global_wb_domain.dirty_limit / DIRTY_SCOPE);
		pages = min(pages, work->nr_pages);
		pages = round_down(pages + MIN_WRITEBACK_PAGES,
				   MIN_WRITEBACK_PAGES);
	}

	return pages;
}

재등록은 남은 작업의 이유를 구별합니다

requeue_inode()는 먼저 freeing 상태를 확인합니다. integrity 또는 tagged 작업 뒤에도 inode가 dirty이면 새 시각을 기록하여 같은 sync 작업이 끊임없이 다시 잡지 않게 합니다.

pages_skipped가 있으면 이번에는 진행하기 어려웠다는 뜻일 수 있습니다. I_DIRTY_ALL이 남으면 dirty 목록으로 미루고, clean인 예외에는 inode_cgwb_move_to_attached()를 호출한 뒤 이 경로를 끝냅니다. Cgroup wb에서는 b_attached로 옮기지만, root wb이거나 cgroup writeback이 비활성인 경로에서는 writeback 목록에서 제거합니다. Skipped page가 있다는 사실만으로 inode 자체가 반드시 dirty라고 가정하지 않습니다.

XArray의 DIRTY mark가 남은 경우 b_more_io로 보내는 조건은 두 가지입니다. nr_to_write <= 0으로 chunk를 소진했으며, 동시에 !inode_dirtied_after(inode, dirtied_before)로 현재 cutoff보다 뒤에 dirty된 inode가 아니어야 합니다. Quota만 소진했다고 곧바로 이 목록으로 돌아가는 것은 아닙니다.

앞에서 sync 또는 tagged 작업 뒤 dirtied_when을 갱신했다면 이 cutoff 검사에서 제외될 수 있습니다. 그런 inode는 chunk를 소진했더라도 b_dirty 쪽으로 미뤄질 수 있습니다. Quota 미소진과 cutoff 이후 redirty를 모두 고려해야 같은 sync가 inode를 끝없이 다시 처리하는 일을 막을 수 있습니다.

XArray DIRTY mark는 없는데 inode 자체가 dirty일 수도 있습니다. delayed allocation이나 I/O 관련 metadata 갱신이 그 예입니다. 이 경우에도 metadata를 처리할 기회가 필요합니다. timestamp만 남았다면 별도 b_dirty_time으로 갈 수 있고, 정말 clean일 때에만 일반 writeback 목록에서 제거합니다.

redirty_tail_locked()의 tail은 처리 순서상 뒤로 미룬다는 의미로 읽어야 합니다. 실제 linked list에서는 head로 이동하는 코드가 보일 수 있습니다. 뒤에서부터 소비하기 때문입니다. 이때 시간 정렬이 깨지지 않도록 dirtied_when을 조정하는 조건도 함께 있습니다.

두 redirty helper와 requeue_io()의 전체는 다음과 같습니다. 현재 redirty_tail_locked()는 I_SYNC_QUEUED를 먼저 지우며, I_FREEING이면 dirty 목록으로 옮기는 대신 연결을 제거하고 dirty-I/O 통계를 정리합니다. freeing 상태를 검사하는 모든 경로가 같은 후처리를 한다고 일반화하지 않습니다. 반면 requeue_io()는 추가 처리를 위해 b_more_io로 이동합니다.

redirty_tail_v6_18.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 * Complete scope from Linux v6.18 fs/fs-writeback.c.
 * Source: https://github.com/torvalds/linux/blob/v6.18/fs/fs-writeback.c
 * Kernel excerpt; not a standalone program.
 * The book exact source tag is not asserted.
 */

/*
 * fs/fs-writeback.c
 *
 * Copyright (C) 2002, Linus Torvalds.
 *
 * Contains all the functions related to writing back and waiting
 * upon dirty inodes against superblocks, and writing back dirty
 * pages against inodes.  ie: data writeback.  Writeout of the
 * inode itself is not handled here.
 *
 * 10Apr2002	Andrew Morton
 *		Split out of fs/inode.c
 *		Additions for address_space-based writeback
 */

static void redirty_tail(struct inode *inode, struct bdi_writeback *wb)
{
	spin_lock(&inode->i_lock);
	redirty_tail_locked(inode, wb);
	spin_unlock(&inode->i_lock);
}

redirty_tail_locked_v6_18.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 * Complete scope from Linux v6.18 fs/fs-writeback.c.
 * Source: https://github.com/torvalds/linux/blob/v6.18/fs/fs-writeback.c
 * Kernel excerpt; not a standalone program.
 * The book exact source tag is not asserted.
 */

/*
 * fs/fs-writeback.c
 *
 * Copyright (C) 2002, Linus Torvalds.
 *
 * Contains all the functions related to writing back and waiting
 * upon dirty inodes against superblocks, and writing back dirty
 * pages against inodes.  ie: data writeback.  Writeout of the
 * inode itself is not handled here.
 *
 * 10Apr2002	Andrew Morton
 *		Split out of fs/inode.c
 *		Additions for address_space-based writeback
 */

static void redirty_tail_locked(struct inode *inode, struct bdi_writeback *wb)
{
	assert_spin_locked(&inode->i_lock);

	inode->i_state &= ~I_SYNC_QUEUED;
	/*
	 * When the inode is being freed just don't bother with dirty list
	 * tracking. Flush worker will ignore this inode anyway and it will
	 * trigger assertions in inode_io_list_move_locked().
	 */
	if (inode->i_state & I_FREEING) {
		list_del_init(&inode->i_io_list);
		wb_io_lists_depopulated(wb);
		return;
	}
	if (!list_empty(&wb->b_dirty)) {
		struct inode *tail;

		tail = wb_inode(wb->b_dirty.next);
		if (time_before(inode->dirtied_when, tail->dirtied_when))
			inode->dirtied_when = jiffies;
	}
	inode_io_list_move_locked(inode, wb, &wb->b_dirty);
}

requeue_io_v6_18.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 * Complete scope from Linux v6.18 fs/fs-writeback.c.
 * Source: https://github.com/torvalds/linux/blob/v6.18/fs/fs-writeback.c
 * Kernel excerpt; not a standalone program.
 * The book exact source tag is not asserted.
 */

/*
 * fs/fs-writeback.c
 *
 * Copyright (C) 2002, Linus Torvalds.
 *
 * Contains all the functions related to writing back and waiting
 * upon dirty inodes against superblocks, and writing back dirty
 * pages against inodes.  ie: data writeback.  Writeout of the
 * inode itself is not handled here.
 *
 * 10Apr2002	Andrew Morton
 *		Split out of fs/inode.c
 *		Additions for address_space-based writeback
 */

static void requeue_io(struct inode *inode, struct bdi_writeback *wb)
{
	inode_io_list_move_locked(inode, wb, &wb->b_more_io);
}

다음은 Linux v6.18의 requeue_inode() 함수 전체입니다. 함수 내부의 분기와 주석을 생략하지 않았으며, Linux kernel의 다른 선언과 helper를 사용하는 code이므로 이 파일만 따로 compile하는 예제는 아닙니다. 원본 파일의 라이선스는 GPL-2.0-only입니다.

함수 전체 파일 · 원본 source · 라이선스

requeue_inode_v6_18.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 * Linux v6.18, fs/fs-writeback.c: complete requeue_inode() function.
 * Copyright (C) 2002, Linus Torvalds.
 * Original file attribution: 10Apr2002 Andrew Morton, split from fs/inode.c.
 */

static void requeue_inode(struct inode *inode, struct bdi_writeback *wb,
			  struct writeback_control *wbc,
			  unsigned long dirtied_before)
{
	if (inode->i_state & I_FREEING)
		return;

	/*
	 * Sync livelock prevention. Each inode is tagged and synced in one
	 * shot. If still dirty, it will be redirty_tail()'ed below.  Update
	 * the dirty time to prevent enqueue and sync it again.
	 */
	if ((inode->i_state & I_DIRTY) &&
	    (wbc->sync_mode == WB_SYNC_ALL || wbc->tagged_writepages))
		inode->dirtied_when = jiffies;

	if (wbc->pages_skipped) {
		/*
		 * Writeback is not making progress due to locked buffers.
		 * Skip this inode for now. Although having skipped pages
		 * is odd for clean inodes, it can happen for some
		 * filesystems so handle that gracefully.
		 */
		if (inode->i_state & I_DIRTY_ALL)
			redirty_tail_locked(inode, wb);
		else
			inode_cgwb_move_to_attached(inode, wb);
		return;
	}

	if (mapping_tagged(inode->i_mapping, PAGECACHE_TAG_DIRTY)) {
		/*
		 * We didn't write back all the pages.  nfs_writepages()
		 * sometimes bales out without doing anything.
		 */
		if (wbc->nr_to_write <= 0 &&
		    !inode_dirtied_after(inode, dirtied_before)) {
			/* Slice used up. Queue for next turn. */
			requeue_io(inode, wb);
		} else {
			/*
			 * Writeback blocked by something other than
			 * congestion. Delay the inode for some time to
			 * avoid spinning on the CPU (100% iowait)
			 * retrying writeback of the dirty page/inode
			 * that cannot be performed immediately.
			 */
			redirty_tail_locked(inode, wb);
		}
	} else if (inode->i_state & I_DIRTY) {
		/*
		 * Filesystems can dirty the inode during writeback operations,
		 * such as delayed allocation during submission or metadata
		 * updates after data IO completion.
		 */
		redirty_tail_locked(inode, wb);
	} else if (inode->i_state & I_DIRTY_TIME) {
		inode->dirtied_when = jiffies;
		inode_io_list_move_locked(inode, wb, &wb->b_dirty_time);
		inode->i_state &= ~I_SYNC_QUEUED;
	} else {
		/* The inode is clean. Remove from writeback lists. */
		inode_cgwb_move_to_attached(inode, wb);
	}
}

clean 예외에서 사용하는 helper도 config별 전체를 확인할 수 있습니다. 첫 번째는 cgroup writeback 활성 구현이며 root wb에는 b_attached 이동을 하지 않습니다. 두 번째는 비활성 구현입니다. 두 경우 모두 필요한 목록 lock과 inode lock을 가진 상태여야 합니다. I_SYNC_QUEUED를 지웠다고 inode의 data I/O 완료까지 선언하는 것은 아닙니다.

inode_cgwb_move_to_attached_v6_18.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 * Complete scope from Linux v6.18 fs/fs-writeback.c.
 * Source: https://github.com/torvalds/linux/blob/v6.18/fs/fs-writeback.c
 * Kernel excerpt; not a standalone program.
 * The book exact source tag is not asserted.
 */

/*
 * fs/fs-writeback.c
 *
 * Copyright (C) 2002, Linus Torvalds.
 *
 * Contains all the functions related to writing back and waiting
 * upon dirty inodes against superblocks, and writing back dirty
 * pages against inodes.  ie: data writeback.  Writeout of the
 * inode itself is not handled here.
 *
 * 10Apr2002	Andrew Morton
 *		Split out of fs/inode.c
 *		Additions for address_space-based writeback
 */

static void inode_cgwb_move_to_attached(struct inode *inode,
					struct bdi_writeback *wb)
{
	assert_spin_locked(&wb->list_lock);
	assert_spin_locked(&inode->i_lock);
	WARN_ON_ONCE(inode->i_state & I_FREEING);

	inode->i_state &= ~I_SYNC_QUEUED;
	if (wb != &wb->bdi->wb)
		list_move(&inode->i_io_list, &wb->b_attached);
	else
		list_del_init(&inode->i_io_list);
	wb_io_lists_depopulated(wb);
}

inode_cgwb_move_to_attached_nocg_v6_18.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 * Complete scope from Linux v6.18 fs/fs-writeback.c.
 * Source: https://github.com/torvalds/linux/blob/v6.18/fs/fs-writeback.c
 * Kernel excerpt; not a standalone program.
 * The book exact source tag is not asserted.
 */

/*
 * fs/fs-writeback.c
 *
 * Copyright (C) 2002, Linus Torvalds.
 *
 * Contains all the functions related to writing back and waiting
 * upon dirty inodes against superblocks, and writing back dirty
 * pages against inodes.  ie: data writeback.  Writeout of the
 * inode itself is not handled here.
 *
 * 10Apr2002	Andrew Morton
 *		Split out of fs/inode.c
 *		Additions for address_space-based writeback
 */

static void inode_cgwb_move_to_attached(struct inode *inode,
					struct bdi_writeback *wb)
{
	assert_spin_locked(&wb->list_lock);
	assert_spin_locked(&inode->i_lock);
	WARN_ON_ONCE(inode->i_state & I_FREEING);

	inode->i_state &= ~I_SYNC_QUEUED;
	list_del_init(&inode->i_io_list);
	wb_io_lists_depopulated(wb);
}

dirty state를 지운 뒤 mark를 다시 보는 이유가 있습니다

__writeback_single_inode()는 do_writepages()를 호출합니다. WB_SYNC_ALL이면서 전체 sync의 별도 대기 경로가 아닌 경우에는 data 완료를 기다린 뒤 metadata 처리로 넘어갑니다. I/O 완료에서 metadata를 바꾸는 filesystem도 고려해야 합니다.

이후 i_lock 아래에서 inode dirty state를 수집하고 지웁니다. 하지만 여기서 종료하면 동시에 dirty로 바꾸는 task의 빠른 검사가 변경을 놓칠 수 있습니다. 따라서 smp_mb() 뒤 XArray의 DIRTY mark를 다시 확인하고 남아 있으면 I_DIRTY_PAGES를 복구합니다. 반대편 __mark_inode_dirty()의 barrier와 짝을 이뤄, 어느 쪽이든 새 변경을 보도록 하는 구조입니다.

data dirty만 있었을 때에는 inode metadata write callback을 불필요하게 호출하지 않습니다. 그 밖의 metadata dirty가 남았다면 write_inode()를 수행하고 먼저 발생한 error를 보존합니다. data scan의 종료와 metadata의 종료가 별도 단계라는 점을 다시 확인할 수 있습니다.

다음은 현재 __writeback_single_inode() 전체입니다. I_SYNC의 설정과 해제는 caller 책임이며 이 함수가 inode를 임의로 일반 목록에서 제거하지는 않습니다. dirty-time 만료나 WB_SYNC_ALL이면 mark_inode_dirty_sync()로 filesystem에 metadata 변경을 알린 뒤 dirty state를 수집합니다. dirty state를 지우는 위치가 writepages와 timestamp 처리 뒤인 이유입니다.

__writeback_single_inode_v6_18.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 * Complete scope from Linux v6.18 fs/fs-writeback.c.
 * Source: https://github.com/torvalds/linux/blob/v6.18/fs/fs-writeback.c
 * Kernel excerpt; not a standalone program.
 * The book exact source tag is not asserted.
 */

/*
 * fs/fs-writeback.c
 *
 * Copyright (C) 2002, Linus Torvalds.
 *
 * Contains all the functions related to writing back and waiting
 * upon dirty inodes against superblocks, and writing back dirty
 * pages against inodes.  ie: data writeback.  Writeout of the
 * inode itself is not handled here.
 *
 * 10Apr2002	Andrew Morton
 *		Split out of fs/inode.c
 *		Additions for address_space-based writeback
 */

static int
__writeback_single_inode(struct inode *inode, struct writeback_control *wbc)
{
	struct address_space *mapping = inode->i_mapping;
	long nr_to_write = wbc->nr_to_write;
	unsigned dirty;
	int ret;

	WARN_ON(!(inode->i_state & I_SYNC));

	trace_writeback_single_inode_start(inode, wbc, nr_to_write);

	ret = do_writepages(mapping, wbc);

	/*
	 * Make sure to wait on the data before writing out the metadata.
	 * This is important for filesystems that modify metadata on data
	 * I/O completion. We don't do it for sync(2) writeback because it has a
	 * separate, external IO completion path and ->sync_fs for guaranteeing
	 * inode metadata is written back correctly.
	 */
	if (wbc->sync_mode == WB_SYNC_ALL && !wbc->for_sync) {
		int err = filemap_fdatawait(mapping);
		if (ret == 0)
			ret = err;
	}

	/*
	 * If the inode has dirty timestamps and we need to write them, call
	 * mark_inode_dirty_sync() to notify the filesystem about it and to
	 * change I_DIRTY_TIME into I_DIRTY_SYNC.
	 */
	if ((inode->i_state & I_DIRTY_TIME) &&
	    (wbc->sync_mode == WB_SYNC_ALL ||
	     time_after(jiffies, inode->dirtied_time_when +
			dirtytime_expire_interval * HZ))) {
		trace_writeback_lazytime(inode);
		mark_inode_dirty_sync(inode);
	}

	/*
	 * Get and clear the dirty flags from i_state.  This needs to be done
	 * after calling writepages because some filesystems may redirty the
	 * inode during writepages due to delalloc.  It also needs to be done
	 * after handling timestamp expiration, as that may dirty the inode too.
	 */
	spin_lock(&inode->i_lock);
	dirty = inode->i_state & I_DIRTY;
	inode->i_state &= ~dirty;

	/*
	 * Paired with smp_mb() in __mark_inode_dirty().  This allows
	 * __mark_inode_dirty() to test i_state without grabbing i_lock -
	 * either they see the I_DIRTY bits cleared or we see the dirtied
	 * inode.
	 *
	 * I_DIRTY_PAGES is always cleared together above even if @mapping
	 * still has dirty pages.  The flag is reinstated after smp_mb() if
	 * necessary.  This guarantees that either __mark_inode_dirty()
	 * sees clear I_DIRTY_PAGES or we see PAGECACHE_TAG_DIRTY.
	 */
	smp_mb();

	if (mapping_tagged(mapping, PAGECACHE_TAG_DIRTY))
		inode->i_state |= I_DIRTY_PAGES;
	else if (unlikely(inode->i_state & I_PINNING_NETFS_WB)) {
		if (!(inode->i_state & I_DIRTY_PAGES)) {
			inode->i_state &= ~I_PINNING_NETFS_WB;
			wbc->unpinned_netfs_wb = true;
			dirty |= I_PINNING_NETFS_WB; /* Cause write_inode */
		}
	}

	spin_unlock(&inode->i_lock);

	/* Don't write the inode if only I_DIRTY_PAGES was set */
	if (dirty & ~I_DIRTY_PAGES) {
		int err = write_inode(inode, wbc);
		if (ret == 0)
			ret = err;
	}
	wbc->unpinned_netfs_wb = false;
	trace_writeback_single_inode(inode, wbc, nr_to_write);
	return ret;
}

mapping의 DIRTY mark가 없다면 netfs pin 해제 관련 상태도 처리할 수 있습니다. I_PINNING_NETFS_WB를 내리고 wbc.unpinned_netfs_wb를 callback에 전달한 뒤 마지막에 다시 false로 정리합니다. 따라서 dirty & ~I_DIRTY_PAGES는 단순한 data page 유무 검사보다 넓습니다. metadata callback의 error가 있어도 이미 앞선 error가 있으면 그것을 보존합니다. dirty flag를 지운 사실 자체는 저장 성공 증명이 아닙니다.

목록에서 빠져도 device I/O가 남을 수 있습니다

후처리는 requeue_inode()로 다음 위치를 정하고 inode_sync_complete()로 I_SYNC를 지워 waiter를 깨웁니다. I_SYNC는 이 inode의 writeback 처리를 직렬화하는 상태이지 각 folio의 모든 device I/O를 표현하는 bit가 아닙니다.

목록이 비면 WB_has_dirty_io와 active bandwidth 관련 통계를 정리합니다. 그러나 data-integrity caller는 이미 제출된 folio writeback을 별도로 기다려야 할 수 있습니다. flusher는 약 1/10초의 진행 구간 뒤 상위 loop로 돌아가 threshold나 새 요청도 다시 확인합니다. 이 시간은 모든 I/O를 그 안에 끝낸다는 latency 보장이 아니라 긴 독점을 피하기 위한 판단 주기입니다.

아래 inode_sync_complete() 전체는 i_lock 아래에서 I_SYNC를 지우고, clean·unused inode의 LRU 처리를 한 뒤 bit waiter를 깨웁니다. 이 wakeup을 모든 folio의 writeback flag 해제와 교환해서 사용할 수는 없습니다.

inode_sync_complete_v6_18.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 * Complete scope from Linux v6.18 fs/fs-writeback.c.
 * Source: https://github.com/torvalds/linux/blob/v6.18/fs/fs-writeback.c
 * Kernel excerpt; not a standalone program.
 * The book exact source tag is not asserted.
 */

/*
 * fs/fs-writeback.c
 *
 * Copyright (C) 2002, Linus Torvalds.
 *
 * Contains all the functions related to writing back and waiting
 * upon dirty inodes against superblocks, and writing back dirty
 * pages against inodes.  ie: data writeback.  Writeout of the
 * inode itself is not handled here.
 *
 * 10Apr2002	Andrew Morton
 *		Split out of fs/inode.c
 *		Additions for address_space-based writeback
 */

static void inode_sync_complete(struct inode *inode)
{
	assert_spin_locked(&inode->i_lock);

	inode->i_state &= ~I_SYNC;
	/* If inode is clean an unused, put it into LRU now... */
	inode_add_lru(inode);
	/* Called with inode->i_lock which ensures memory ordering. */
	inode_wake_up_bit(inode, __I_SYNC);
}

참고 자료

  • Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 10, inode writeback 및 requeue 설명.
  • Linux v6.18 fs/fs-writeback.c: writeback_sb_inodes(), inode wait, requeue_inode(), __writeback_single_inode() 전체 경로.