Superblock sync는 work 완료와 I/O 완료를 따로 기다립니다

같은 storage를 사용하는 두 filesystem을 생각해 보겠습니다. 한쪽에 syncfs()를 호출했다고 다른 filesystem의 모든 dirty inode까지 같은 요청으로 저장할 필요는 없습니다. 반대로 해당 filesystem의 inode가 dirty 목록에서 사라졌다는 이유만으로 sync를 끝내서도 안 됩니다. 이미 제출되어 device에서 진행 중인 writeback이 남을 수 있기 때문입니다.

Linux v6.18의 superblock writeback은 범위가 담긴 work를 wb들에 분배하고, worker의 요청 처리가 끝나는 시점과 실제 folio writeback이 끝나는 시점을 나누어 기다립니다. 이 글의 code는 공식 v6.18 fs/fs-writeback.c의 전체 함수입니다. kernel 선언이 필요한 GPL-2.0-only code이며 독립 실행 예제는 아닙니다.

WB_SYNC_NONE도 caller가 전혀 기다리지 않는 뜻은 아닙니다

writeback_inodes_sb()는 전역의 get_nr_dirty_pages()로 목표량을 구하고, writeback_inodes_sb_nr()를 거쳐 __writeback_inodes_sb_nr()로 내려갑니다. inode 개수를 읽는 helper가 아닙니다. 이 수는 특정 superblock의 정확한 dirty page 수를 잠가서 센 값도 아니며, 실제로 몇 page를 제출할지 보장하지 않습니다.

writeback_inodes_sb_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
 */

void writeback_inodes_sb(struct super_block *sb, enum wb_reason reason)
{
	writeback_inodes_sb_nr(sb, get_nr_dirty_pages(), reason);
}

writeback_inodes_sb_nr_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
 */

void writeback_inodes_sb_nr(struct super_block *sb,
			    unsigned long nr,
			    enum wb_reason reason)
{
	__writeback_inodes_sb_nr(sb, nr, reason, false);
}

내부 함수는 WB_SYNC_NONE, tagged_writepages = 1, 지정한 sb와 목표량으로 work를 만듭니다. dirty I/O가 없는 BDI와 noop BDI는 일찍 반환합니다. s_umount는 caller가 이미 보호해야 하며 WARN은 그 계약의 위반을 드러내는 검사입니다. 이 함수가 unmount와의 race를 복구하거나 여기서 lock을 새로 잡아 주는 것은 아닙니다.

__writeback_inodes_sb_nr_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 __writeback_inodes_sb_nr(struct super_block *sb, unsigned long nr,
				     enum wb_reason reason, bool skip_if_busy)
{
	struct backing_dev_info *bdi = sb->s_bdi;
	DEFINE_WB_COMPLETION(done, bdi);
	struct wb_writeback_work work = {
		.sb			= sb,
		.sync_mode		= WB_SYNC_NONE,
		.tagged_writepages	= 1,
		.done			= &done,
		.nr_pages		= nr,
		.reason			= reason,
	};

	if (!bdi_has_dirty_io(bdi) || bdi == &noop_backing_dev_info)
		return;
	WARN_ON(!rwsem_is_locked(&sb->s_umount));

	bdi_split_work_to_wbs(sb->s_bdi, &work, skip_if_busy);
	wb_wait_for_completion(&done);
}

끝의 wb_wait_for_completion()은 의도적입니다. WB_SYNC_NONE이어도 stack에 있는 completion을 worker가 나중에 건드리도록 남기고 반환할 수는 없습니다. 요청 처리의 완료는 기다리지만, 이 API는 제출한 모든 BIO의 완료까지 기다리겠다는 data-integrity 계약을 제공하지 않습니다. asynchronous라는 설명은 이 두 wait를 구별한 뒤 읽어야 합니다.

Sync는 이미 clean이 된 inode의 I/O도 놓치지 않습니다

sync_inodes_sb()의 work는 WB_SYNC_ALL, nr_pages = LONG_MAX, for_sync = 1, non-cyclic 전체 범위를 사용합니다. for_sync는 단순히 sync mode를 한 번 더 적은 flag가 아닙니다. inode별 I/O 대기의 일부를 바깥 superblock 경로에서 담당한다는 제어 흐름과 연결됩니다.

sync_inodes_sb_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
 */

void sync_inodes_sb(struct super_block *sb)
{
	struct backing_dev_info *bdi = sb->s_bdi;
	DEFINE_WB_COMPLETION(done, bdi);
	struct wb_writeback_work work = {
		.sb		= sb,
		.sync_mode	= WB_SYNC_ALL,
		.nr_pages	= LONG_MAX,
		.range_cyclic	= 0,
		.done		= &done,
		.reason		= WB_REASON_SYNC,
		.for_sync	= 1,
	};

	/*
	 * Can't skip on !bdi_has_dirty() because we should wait for !dirty
	 * inodes under writeback and I_DIRTY_TIME inodes ignored by
	 * bdi_has_dirty() need to be written out too.
	 */
	if (bdi == &noop_backing_dev_info)
		return;
	WARN_ON(!rwsem_is_locked(&sb->s_umount));

	/* protect against inode wb switch, see inode_switch_wbs_work_fn() */
	bdi_down_write_wb_switch_rwsem(bdi);
	bdi_split_work_to_wbs(bdi, &work, false);
	wb_wait_for_completion(&done);
	bdi_up_write_wb_switch_rwsem(bdi);

	wait_sb_inodes(sb);
}

이 함수는 !bdi_has_dirty_io()만 보고 반환하지 않습니다. dirty bit를 지우고 이미 I/O 중인 inode도 기다려야 하고, 일반 dirty-I/O 검사에서 빠지는 timestamp-only inode도 sync해야 하기 때문입니다. noop BDI인 경우만 여기서 제외합니다.

bdi_down_write_wb_switch_rwsem()부터 대응하는 unlock까지는 inode가 wb를 바꾸는 경로와의 조율입니다. 그 안에서 work를 분배하고 completion을 기다린 뒤, wait_sb_inodes()를 별도로 호출합니다. 이 순서 때문에 work가 끝났다는 사실을 곧바로 device가 모든 내용을 받았다는 사실로 바꾸면 안 됩니다.

Completion의 초기 count는 요청 발행 구간을 보호합니다

다음은 v6.18 backing-dev-defs.h의 completion 선언과 초기화 macro 전체입니다. 이 header의 라이선스 표기는 GPL-2.0입니다.

struct wb_completion {
	atomic_t		cnt;
	wait_queue_head_t	*waitq;
};

#define __WB_COMPLETION_INIT(_waitq)	\
	(struct wb_completion){ .cnt = ATOMIC_INIT(1), .waitq = (_waitq) }

#define WB_COMPLETION_INIT(bdi)		__WB_COMPLETION_INIT(&(bdi)->wb_waitq)

#define DEFINE_WB_COMPLETION(cmpl, bdi)	\
	struct wb_completion cmpl = WB_COMPLETION_INIT(bdi)

초기 count 1은 caller가 아직 work들을 발행 중이라는 표시입니다. 각 wb_queue_work()가 count를 늘리고, worker의 finish_writeback_work()가 줄입니다. 발행을 마친 caller는 wb_wait_for_completion()에서 초기 count를 하나 내려놓고 0을 기다립니다. 요청이 하나도 발행되지 않은 경우에도 이 규칙으로 정상 종료합니다. count와 wait queue의 역할은 worker에게 요청을 전달하는 과정에서 전체 helper로 확인할 수 있습니다.

여러 wb에 나눌 때 allocation 실패도 수명 문제입니다

bdi_split_work_to_wbs()는 RCU 아래에서 BDI의 wb 목록을 순회합니다. 일반 dirty I/O가 없더라도 WB_SYNC_ALL이고 dirty-time 목록이 남으면 대상이 됩니다. skip_if_busy가 true이면 이미 writeback 중인 wb를 건너뛸 수 있지만, 모든 sync가 그런 skip을 허용하는 것은 아닙니다.

bdi_split_work_to_wbs_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 bdi_split_work_to_wbs(struct backing_dev_info *bdi,
				  struct wb_writeback_work *base_work,
				  bool skip_if_busy)
{
	struct bdi_writeback *last_wb = NULL;
	struct bdi_writeback *wb = list_entry(&bdi->wb_list,
					      struct bdi_writeback, bdi_node);

	might_sleep();
restart:
	rcu_read_lock();
	list_for_each_entry_continue_rcu(wb, &bdi->wb_list, bdi_node) {
		DEFINE_WB_COMPLETION(fallback_work_done, bdi);
		struct wb_writeback_work fallback_work;
		struct wb_writeback_work *work;
		long nr_pages;

		if (last_wb) {
			wb_put(last_wb);
			last_wb = NULL;
		}

		/* SYNC_ALL writes out I_DIRTY_TIME too */
		if (!wb_has_dirty_io(wb) &&
		    (base_work->sync_mode == WB_SYNC_NONE ||
		     list_empty(&wb->b_dirty_time)))
			continue;
		if (skip_if_busy && writeback_in_progress(wb))
			continue;

		nr_pages = wb_split_bdi_pages(wb, base_work->nr_pages);

		work = kmalloc(sizeof(*work), GFP_ATOMIC);
		if (work) {
			*work = *base_work;
			work->nr_pages = nr_pages;
			work->auto_free = 1;
			wb_queue_work(wb, work);
			continue;
		}

		/*
		 * If wb_tryget fails, the wb has been shutdown, skip it.
		 *
		 * Pin @wb so that it stays on @bdi->wb_list.  This allows
		 * continuing iteration from @wb after dropping and
		 * regrabbing rcu read lock.
		 */
		if (!wb_tryget(wb))
			continue;

		/* alloc failed, execute synchronously using on-stack fallback */
		work = &fallback_work;
		*work = *base_work;
		work->nr_pages = nr_pages;
		work->auto_free = 0;
		work->done = &fallback_work_done;

		wb_queue_work(wb, work);
		last_wb = wb;

		rcu_read_unlock();
		wb_wait_for_completion(&fallback_work_done);
		goto restart;
	}
	rcu_read_unlock();

	if (last_wb)
		wb_put(last_wb);
}

work는 RCU read-side 구간에서 GFP_ATOMIC으로 할당합니다. 성공하면 base work를 복사하되 해당 wb의 목표량과 auto_free = 1을 설정해 queue합니다. 이 allocation이 실패했다고 요청을 버리지 않습니다. 먼저 wb_tryget()으로 shutdown되지 않은 wb의 참조를 얻고, stack fallback work를 queue한 뒤 RCU lock을 놓고 fallback completion을 기다립니다. 실패한 wb_tryget()이면 그 wb는 건너뜁니다.

fallback의 동기적이라는 말은 같은 caller가 filesystem writeback을 직접 실행한다는 뜻이 아닙니다. worker에 넘긴 stack 객체가 더는 사용되지 않을 때까지 caller가 기다린다는 뜻입니다. 이후 순회를 재개하려고 wb 참조를 유지하며, 다음 단계 또는 마지막에 그 참조를 놓습니다. RCU를 든 채 잠들거나 stack work를 남기고 반환하지 않는 두 조건이 함께 필요합니다.

목표량을 나누는 wb_split_bdi_pages() 전체도 함께 볼 수 있습니다.

wb_split_bdi_pages_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 wb_split_bdi_pages(struct bdi_writeback *wb, long nr_pages)
{
	unsigned long this_bw = wb->avg_write_bandwidth;
	unsigned long tot_bw = atomic_long_read(&wb->bdi->tot_write_bandwidth);

	if (nr_pages == LONG_MAX)
		return LONG_MAX;

	/*
	 * This may be called on clean wb's and proportional distribution
	 * may not make sense, just use the original @nr_pages in those
	 * cases.  In general, we wanna err on the side of writing more.
	 */
	if (!tot_bw || this_bw >= tot_bw)
		return nr_pages;
	else
		return DIV_ROUND_UP_ULL((u64)nr_pages * this_bw, tot_bw);
}

LONG_MAX는 그대로 넘깁니다. finite 목표량은 wb의 avg_write_bandwidth와 BDI의 active bandwidth 합의 비율로 올림 분배합니다. 합이 0이거나 이 wb의 bandwidth가 합 이상이면 원래 목표량을 그대로 사용합니다. 각 몫을 올림하므로 모든 몫의 합이 원래 값과 정확히 같다는 conservation 규칙은 없습니다. cgroup writeback을 쓰지 않으면 보통 embedded root wb 하나를 처리하지만, 그렇다고 이 호출이 무의미해지는 것은 아닙니다.

마지막 wait는 inode 참조를 얻고 lock을 놓은 뒤 수행합니다

wait_sb_inodes()는 s_sync_lock으로 다른 sync 순회를 조율하고, s_inode_wblist_lock 아래에서 현재 s_inodes_wb를 임시 목록으로 splice합니다. 뒤늦게 writeback을 시작하는 inode를 무한히 따라가지 않기 위한 경계입니다. 현재 필드명은 superblock의 s_inodes_wb이며 각 inode의 연결 node는 i_wb_list입니다.

wait_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 void wait_sb_inodes(struct super_block *sb)
{
	LIST_HEAD(sync_list);

	/*
	 * We need to be protected against the filesystem going from
	 * r/o to r/w or vice versa.
	 */
	WARN_ON(!rwsem_is_locked(&sb->s_umount));

	mutex_lock(&sb->s_sync_lock);

	/*
	 * Splice the writeback list onto a temporary list to avoid waiting on
	 * inodes that have started writeback after this point.
	 *
	 * Use rcu_read_lock() to keep the inodes around until we have a
	 * reference. s_inode_wblist_lock protects sb->s_inodes_wb as well as
	 * the local list because inodes can be dropped from either by writeback
	 * completion.
	 */
	rcu_read_lock();
	spin_lock_irq(&sb->s_inode_wblist_lock);
	list_splice_init(&sb->s_inodes_wb, &sync_list);

	/*
	 * Data integrity sync. Must wait for all pages under writeback, because
	 * there may have been pages dirtied before our sync call, but which had
	 * writeout started before we write it out.  In which case, the inode
	 * may not be on the dirty list, but we still have to wait for that
	 * writeout.
	 */
	while (!list_empty(&sync_list)) {
		struct inode *inode = list_first_entry(&sync_list, struct inode,
						       i_wb_list);
		struct address_space *mapping = inode->i_mapping;

		/*
		 * Move each inode back to the wb list before we drop the lock
		 * to preserve consistency between i_wb_list and the mapping
		 * writeback tag. Writeback completion is responsible to remove
		 * the inode from either list once the writeback tag is cleared.
		 */
		list_move_tail(&inode->i_wb_list, &sb->s_inodes_wb);

		/*
		 * The mapping can appear untagged while still on-list since we
		 * do not have the mapping lock. Skip it here, wb completion
		 * will remove it.
		 */
		if (!mapping_tagged(mapping, PAGECACHE_TAG_WRITEBACK))
			continue;

		spin_unlock_irq(&sb->s_inode_wblist_lock);

		spin_lock(&inode->i_lock);
		if (inode->i_state & (I_FREEING|I_WILL_FREE|I_NEW)) {
			spin_unlock(&inode->i_lock);

			spin_lock_irq(&sb->s_inode_wblist_lock);
			continue;
		}
		__iget(inode);
		spin_unlock(&inode->i_lock);
		rcu_read_unlock();

		/*
		 * We keep the error status of individual mapping so that
		 * applications can catch the writeback error using fsync(2).
		 * See filemap_fdatawait_keep_errors() for details.
		 */
		filemap_fdatawait_keep_errors(mapping);

		cond_resched();

		iput(inode);

		rcu_read_lock();
		spin_lock_irq(&sb->s_inode_wblist_lock);
	}
	spin_unlock_irq(&sb->s_inode_wblist_lock);
	rcu_read_unlock();
	mutex_unlock(&sb->s_sync_lock);
}

각 inode를 다시 원래 목록으로 옮겨 놓은 뒤 WRITEBACK mark를 확인합니다. mark와 목록이 순간적으로 어긋난 inode는 완료 경로가 제거하도록 두고 넘어갑니다. 실제 대기 대상이면 목록 spinlock을 놓고 i_lock 아래에서 freeing·new 상태를 제외한 후 __iget()으로 참조를 얻습니다. 그 뒤 RCU read lock까지 놓고 filemap_fdatawait_keep_errors()를 호출합니다.

iput() 뒤에는 예전 inode pointer를 계속 따라가지 않고 lock과 RCU를 다시 얻어 목록에서 다음 대상을 고릅니다. error를 보존하는 helper를 사용하는 이유도 중요합니다. 시스템 sync가 error 상태를 지워 버려 뒤의 application fsync()가 오류를 놓치게 해서는 안 됩니다. wait가 끝났다는 조건, 오류를 관찰하는 주체, metadata와 device flush를 보장하는 최종 callback은 별개의 책임입니다.

참고 자료