Flusher의 oldest-first는 목록 방향과 superblock 묶음을 함께 봅니다

list의 head에 추가했는데 함수 이름에는 tail이 들어갑니다. 가장 오래된 inode부터 처리한다면서 다른 filesystem의 더 새로운 inode가 먼저 나올 수도 있습니다. writeback 목록을 읽다가 생기는 이 두 의문은 consumer의 방향과 superblock별 그룹화를 구별하면 풀립니다.

여기서 다루는 원소는 BIO나 folio가 아니라 inode의 i_io_list입니다. Linux v6.18의 wb->list_lock 아래에서 이동하며, inode의 상태를 바꿀 때에는 해당 i_lock도 사용합니다. 아래 code는 v6.18 fs/fs-writeback.c의 전체 함수이며 GPL-2.0-only kernel code입니다.

b_more_io를 먼저 옮겨도 나중에 넣은 항목이 앞에 보입니다

queue_io()는 b_more_io를 b_io의 head 쪽으로 splice하고 원래 목록을 비웁니다. 이어서 b_dirty에서 cutoff 이전 inode를 골라 b_io의 head 쪽에 붙입니다. consumer가 tail부터 꺼내므로, 처음에 옮겨 둔 추가 처리 대상이 뒤에 남아 먼저 선택됩니다. 여기서 먼저라는 말은 모든 device의 I/O 완료 순서를 보장한다는 뜻은 아닙니다.

queue_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 queue_io(struct bdi_writeback *wb, struct wb_writeback_work *work,
		     unsigned long dirtied_before)
{
	int moved;
	unsigned long time_expire_jif = dirtied_before;

	assert_spin_locked(&wb->list_lock);
	list_splice_init(&wb->b_more_io, &wb->b_io);
	moved = move_expired_inodes(&wb->b_dirty, &wb->b_io, dirtied_before);
	if (!work->for_sync)
		time_expire_jif = jiffies - dirtytime_expire_interval * HZ;
	moved += move_expired_inodes(&wb->b_dirty_time, &wb->b_io,
				     time_expire_jif);
	if (moved)
		wb_io_lists_populated(wb);
	trace_writeback_queue_io(wb, work, dirtied_before, moved);
}

실제 caller들은 b_io가 비었을 때 새 queueing을 합니다. 기존 dispatch batch를 매번 해체해 새 dirty inode와 섞는 방식이 아닙니다. timestamp-only 목록인 b_dirty_time도 처리하지만 기준은 별도입니다. for_sync이면 전달받은 cutoff를 사용하고, 아니면 현재 jiffies에서 dirtytime_expire_interval * HZ를 뺀 기준으로 만료된 inode를 고릅니다.

가장 오래된 inode부터 고른 뒤 같은 filesystem을 묶습니다

move_expired_inodes()는 delaying queue의 tail에서 inode를 얻습니다. 그 inode가 cutoff 뒤에 dirty가 되었다면 종료합니다. 목록이 시간순으로 유지되므로 그 앞에는 더 새로운 inode가 있다는 판단입니다. 선택한 inode를 임시 목록의 head로 옮기면서 I_SYNC_QUEUED를 설정합니다.

move_expired_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 int move_expired_inodes(struct list_head *delaying_queue,
			       struct list_head *dispatch_queue,
			       unsigned long dirtied_before)
{
	LIST_HEAD(tmp);
	struct list_head *pos, *node;
	struct super_block *sb = NULL;
	struct inode *inode;
	int do_sb_sort = 0;
	int moved = 0;

	while (!list_empty(delaying_queue)) {
		inode = wb_inode(delaying_queue->prev);
		if (inode_dirtied_after(inode, dirtied_before))
			break;
		spin_lock(&inode->i_lock);
		list_move(&inode->i_io_list, &tmp);
		moved++;
		inode->i_state |= I_SYNC_QUEUED;
		spin_unlock(&inode->i_lock);
		if (sb_is_blkdev_sb(inode->i_sb))
			continue;
		if (sb && sb != inode->i_sb)
			do_sb_sort = 1;
		sb = inode->i_sb;
	}

	/* just one sb in list, splice to dispatch_queue and we're done */
	if (!do_sb_sort) {
		list_splice(&tmp, dispatch_queue);
		goto out;
	}

	/*
	 * Although inode's i_io_list is moved from 'tmp' to 'dispatch_queue',
	 * we don't take inode->i_lock here because it is just a pointless overhead.
	 * Inode is already marked as I_SYNC_QUEUED so writeback list handling is
	 * fully under our control.
	 */
	while (!list_empty(&tmp)) {
		sb = wb_inode(tmp.prev)->i_sb;
		list_for_each_prev_safe(pos, node, &tmp) {
			inode = wb_inode(pos);
			if (inode->i_sb == sb)
				list_move(&inode->i_io_list, dispatch_queue);
		}
	}
out:
	return moved;
}

tail부터 꺼내 head에 넣는 두 방향이 결합되므로, 임시 목록에서도 같은 inode들의 상대 순서는 보존됩니다. superblock이 하나뿐이면 그 목록을 그대로 dispatch queue로 splice합니다. 여러 superblock을 발견한 경우에는 임시 목록의 tail inode와 같은 superblock의 원소들을 역방향으로 찾아 dispatch queue의 head에 묶어 넣습니다. 이 두 번째 이동에서는 이미 I_SYNC_QUEUED로 writeback 목록 관리가 이 경로의 통제 아래 있으므로 매번 i_lock을 다시 얻지 않습니다. 바깥 wb->list_lock까지 없어지는 뜻은 아닙니다.

함수의 반환형은 int이며 이동한 inode 수를 돌려줍니다. caller가 그 값의 0 여부만 사용한다고 해서 함수 자체가 bool을 반환한다고 설명하지 않습니다. block-device 전용 superblock은 일반 filesystem 사이의 그룹화 필요성을 판단하는 검사에서 특별 취급됩니다.

그룹화는 전역 시간순과 다릅니다

아래 예시에서 X와 Y는 서로 다른 superblock입니다. X4가 가장 최근이고 Y1이 가장 오래되었으며 모두 cutoff 이전이라고 가정합니다. 기존 b_more_io에는 R이 있고 b_io는 비어 있습니다.

두 superblock의 inode를 묶은 뒤 tail부터 처리하는 순서
두 superblock의 inode를 묶은 뒤 tail부터 처리하는 순서

그림은 네 inode의 그룹화와 R의 우선 재선택을 독립적인 예로 나타냅니다. dirty-time 목록, concurrent redirty, freeing inode, lock 실패와 I/O 오류는 생략했습니다.

Y1이 가장 오래되므로 Y 묶음이 먼저 처리할 쪽에 자리 잡고, 그 안에서는 Y1 다음 Y3입니다. 그다음 X2, X4로 이어집니다. 따라서 전역 시간순 Y1, X2, Y3, X4와는 다릅니다. 같은 superblock 안의 나이 순서를 보존하면서 unmount 보호를 확보한 filesystem 묶음마다 처리한다는 정책입니다. R은 그 앞에서 재선택되며 다음 periodic timer까지 무조건 미루지 않습니다.

cutoff 비교는 jiffies wrap도 고려합니다

inode_dirtied_after()의 핵심은 time_after(inode->dirtied_when, t)입니다. 일반 정수의 대소 비교로 바꾸어서는 안 됩니다. 32-bit 경로에는 오래 갱신되지 않은 시각이 wrap 때문에 미래처럼 보이는 경우를 완화하려고 현재 jiffies와의 비교도 들어 있습니다.

inode_dirtied_after_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 bool inode_dirtied_after(struct inode *inode, unsigned long t)
{
	bool ret = time_after(inode->dirtied_when, t);
#ifndef CONFIG_64BIT
	/*
	 * For inodes being constantly redirtied, dirtied_when can get stuck.
	 * It _appears_ to be in the future, but is actually in distant past.
	 * This test is necessary to prevent such wrapped-around relative times
	 * from permanently stopping the whole bdi writeback.
	 */
	ret = ret && time_before_eq(inode->dirtied_when, jiffies);
#endif
	return ret;
}

이 cutoff는 inode의 dirty 시각을 대상으로 합니다. folio마다 모든 store의 시각을 측정하는 snapshot이 아닙니다. 같은 inode가 저장 도중 다시 dirty가 되었을 때 어떤 목록으로 돌려보낼지는 inode 재등록 조건에서 별도로 결정합니다.

목록 상태와 bandwidth 합도 함께 바뀝니다

wb_io_lists_populated()는 이미 dirty-I/O 상태이면 false를 돌려줍니다. 처음 그 상태가 되면 WB_has_dirty_io를 세우고 BDI의 active bandwidth 합에 이 wb의 평균 bandwidth를 더합니다. queue helper는 반환값을 무시하지만, 첫 dirty를 만든 경로는 이 전환을 wakeup 판단에도 사용합니다.

wb_io_lists_populated_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
 * Copyright (C) 2002, Linus Torvalds.
 * 10Apr2002 Andrew Morton -- Split out of fs/inode.c
 * Kernel excerpt; not a standalone program.
 * The book's exact source tag is not asserted.
 */

static bool wb_io_lists_populated(struct bdi_writeback *wb)
{
	if (wb_has_dirty_io(wb)) {
		return false;
	} else {
		set_bit(WB_has_dirty_io, &wb->state);
		WARN_ON_ONCE(!wb->avg_write_bandwidth);
		atomic_long_add(wb->avg_write_bandwidth,
				&wb->bdi->tot_write_bandwidth);
		return true;
	}
}

반대로 wb_io_lists_depopulated()는 b_dirty, b_io, b_more_io가 모두 비었을 때 bit와 합을 정리합니다. timestamp-only 또는 cgroup의 attached 목록까지 모두 비었는지를 검사하는 함수는 아닙니다. state의 bit 하나가 모든 folio의 dirty 상태를 대신하지 않는 이유입니다.

wb_io_lists_depopulated_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 wb_io_lists_depopulated(struct bdi_writeback *wb)
{
	if (wb_has_dirty_io(wb) && list_empty(&wb->b_dirty) &&
	    list_empty(&wb->b_io) && list_empty(&wb->b_more_io)) {
		clear_bit(WB_has_dirty_io, &wb->state);
		WARN_ON_ONCE(atomic_long_sub_return(wb->avg_write_bandwidth,
					&wb->bdi->tot_write_bandwidth) < 0);
	}
}

목록이 비었다는 관측, worker가 당장 수행할 work의 유무, 이미 제출한 folio의 I/O 완료를 끝까지 구별해야 합니다. 그 구분을 유지하면 linked list의 물리적 head·tail과 정책상 다음 차례라는 표현도 혼동하지 않게 됩니다.

참고 자료

  • Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 10, inode queueing과 Flusher Thread Operation.
  • Linux v6.18 fs/fs-writeback.c: queue_io(), move_expired_inodes(), cutoff와 목록 통계 helper의 전체 정의.
  • Linux v6.18 GPL-2.0: code license text. 주변 kernel 선언을 전제로 하며 독립 실행 예제가 아닙니다.