Dirty folio를 찾으려면 inode도 dirty여야 합니다

write()가 성공했는데 device에는 아직 이전 내용이 남아 있을 수 있습니다. buffered write가 먼저 바꾸는 것은 page cache이기 때문입니다. kernel은 나중에 그 변경을 찾아 저장해야 하며, 모든 cached byte를 매번 비교하는 방법은 사용할 수 없습니다.

그래서 folio의 dirty bit, mapping의 XArray mark, inode의 dirty state, writeback 목록이 함께 움직입니다. 같은 일을 중복 기록하는 것처럼 보이지만 각각 답하는 질문이 다릅니다. 이 연결을 알면 dirty bit 하나만 지우는 것으로 writeback이 끝나지 않는 이유도 분명해집니다.

먼저 clean을 언제나 disk와 byte 단위로 일치하는 상태라고 정의하지는 않겠습니다. writeback을 시작하며 dirty를 지웠지만 I/O가 아직 끝나지 않은 구간이 있고, writeback 중 재수정되면 dirty와 writeback이 동시에 설정될 수도 있습니다. uptodate도 cached 내용을 사용할 수 있다는 별도의 상태입니다. 현재 dirty 추적의 출발점과 저장 완료의 판정은 다릅니다.

누적량과 경과 시간은 다른 일을 합니다

writeback은 명시적인 sync 요청뿐 아니라 background 처리와 dirty throttling으로도 시작됩니다. dirty를 만드는 task가 자기 data를 그 자리에서 모두 device에 쓰는 단일 경로가 아닙니다. worker가 I/O를 진행하도록 하고 생산 task의 속도를 줄이는 feedback이 함께 있습니다. 매 store마다 모든 전역 조건을 다시 계산하는 것도 아닙니다.

dirty_background_bytes와 dirty_background_ratio는 background 처리를, dirty_bytes와 dirty_ratio는 writer 제어의 한도를 정하는 두 쌍입니다. 각 쌍에서 bytes와 ratio가 대안이며 네 값 중 하나만 유효하다는 뜻은 아닙니다. ratio의 분모도 설치된 RAM 전체나 사용자 도구의 available 수치를 그대로 가져온 값이 아닙니다. Dirty throttling의 진입 조건은 이 구분을 따로 설명합니다.

dirty_writeback_centisecs는 periodic writeback의 확인 간격이고 dirty_expire_centisecs는 오래된 dirty data를 고르는 시간 기준입니다. 둘 다 1/100초 단위지만 같은 timer가 아닙니다. 만료 시간은 그 순간까지 반드시 영구 저장되었다는 deadline이 아니며, periodic 간격을 0으로 하는 것은 그 주기적 동작을 끄는 것이지 명시적 sync와 모든 background writeback을 없애는 뜻이 아닙니다. v6.18 VM 설정 문서

bit와 mark와 목록은 서로 다른 검색을 돕습니다

folio의 dirty bit는 그 folio에 저장해야 할 변경이 있다는 상태입니다. mapping->i_pages의 PAGECACHE_TAG_DIRTY mark는 큰 파일의 sparse cache에서 dirty folio만 찾게 해 줍니다. clean folio 수가 많아도 writeback 검색이 모든 entry를 순회할 필요는 없습니다.

하지만 flusher가 처음부터 모든 inode의 XArray를 살펴보면 여전히 비쌉니다. 따라서 inode에는 I_DIRTY_PAGES를 기록하고, 해당 inode를 bdi_writeback의 dirty 목록에 연결합니다. flusher는 먼저 inode를 찾고 그 mapping에서 folio를 찾습니다. 이 목록의 원소는 folio가 아니라 inode라는 점이 중요합니다.

Folio와 inode의 dirty 추적
Folio와 inode의 dirty 추적

그림은 일반 파일의 data dirty 경로를 나타냅니다. filesystem별 private dirty state, lazytime 목록과 cgroup writeback 이동은 생략했습니다.

backing_dev_info는 writeback을 뒷받침하는 backing device의 정보입니다. bdi_writeback은 그 위에서 dirty inode와 진행량, bandwidth 등을 관리합니다. cgroup writeback이 없을 때에는 하나의 BDI에 기본 writeback 객체를 생각할 수 있지만, 지원하는 설정에서는 여러 writeback 객체가 같은 BDI의 자원을 나눌 수 있습니다. inode_to_wb()가 반환하는 객체를 device당 영구 고정된 전용 thread와 동일시해서는 안 됩니다.

최초 dirty 전환에서 accounting을 더합니다

upstream v6.18의 filemap_dirty_folio()는 먼저 folio_test_set_dirty()를 실행합니다. 이미 dirty였다면 false를 돌려주고 최초 전환 accounting을 반복하지 않습니다. 처음 dirty가 되었을 때 __folio_mark_dirty()가 XArray mark와 통계를 갱신하고, host inode가 있으면 __mark_inode_dirty(..., I_DIRTY_PAGES)를 이어서 호출합니다. v6.18 mm/page-writeback.c

이 동작은 dirty byte 수를 매번 더한다는 뜻이 아닙니다. 한 byte를 바꿔도 folio의 상태가 전환되며, 통계는 folio_nr_pages()에 해당하는 base page 수를 단위로 갱신할 수 있습니다. 같은 dirty folio의 다른 byte를 계속 수정한다고 최초 dirty 수가 무한히 증가하지 않습니다.

folio_account_dirtied()는 writeback 가능한 mapping에서 NR_FILE_DIRTY, WB_RECLAIMABLE, WB_DIRTIED 등 목적이 다른 통계를 갱신합니다. 앞의 값은 현재 남은 dirty량과 관계되고, 뒤의 값은 누적 dirtying을 통해 rate를 계산하는 데 쓰입니다. task의 nr_dirtied와 CPU별 bdp_ratelimits도 이 경로에서 증가합니다. 어느 값이든 단순히 사용자에게 반환한 write byte 수와 같다고 볼 수 없습니다.

다음 두 함수는 v6.18 mm/page-writeback.c의 전체 정의입니다. folio_account_dirtied()는 mapping이 writeback 가능할 때 inode를 wb에 연결하고 lruvec·zone·node·wb·task·CPU 통계를 각각 갱신합니다. cgroup의 foreign dirty 추적도 포함되어 있습니다. counter의 현재량과 누적량을 같게 만들면 이후 rate 계산까지 달라집니다.

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

bool filemap_dirty_folio(struct address_space *mapping, struct folio *folio)
{
	if (folio_test_set_dirty(folio))
		return false;

	__folio_mark_dirty(folio, mapping, !folio_test_private(folio));

	if (mapping->host) {
		/* !PageAnon && !swapper_space */
		__mark_inode_dirty(mapping->host, I_DIRTY_PAGES);
	}
	return true;
}

folio_account_dirtied_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 void folio_account_dirtied(struct folio *folio,
		struct address_space *mapping)
{
	struct inode *inode = mapping->host;

	trace_writeback_dirty_folio(folio, mapping);

	if (mapping_can_writeback(mapping)) {
		struct bdi_writeback *wb;
		long nr = folio_nr_pages(folio);

		inode_attach_wb(inode, folio);
		wb = inode_to_wb(inode);

		__lruvec_stat_mod_folio(folio, NR_FILE_DIRTY, nr);
		__zone_stat_mod_folio(folio, NR_ZONE_WRITE_PENDING, nr);
		__node_stat_mod_folio(folio, NR_DIRTIED, nr);
		wb_stat_mod(wb, WB_RECLAIMABLE, nr);
		wb_stat_mod(wb, WB_DIRTIED, nr);
		task_io_account_write(nr * PAGE_SIZE);
		current->nr_dirtied += nr;
		__this_cpu_add(bdp_ratelimits, nr);

		mem_cgroup_track_foreign_dirty(folio, wb);
	}
}

truncate와 경쟁하지 않는 호출 조건이 필요합니다

__folio_mark_dirty()는 XArray lock을 잡고 folio가 여전히 mapping에 붙어 있는지 확인한 뒤 mark를 설정합니다. 이미 truncate된 객체에 오래된 mapping을 따라 mark를 넣어서는 안 되기 때문입니다.

그렇다고 함수 내부 XArray lock만 있으면 어떤 pointer든 안전하게 넘길 수 있다는 뜻은 아닙니다. 호출자는 진행 중인 truncate와의 경쟁을 막아야 합니다. 보통 folio lock을 사용하지만, mapped page의 page-table lock으로 제거를 막는 경로도 있습니다. buffer에서 dirty를 올리는 경우에는 buffer 참조가 private buffer 제거를 막는 계약에 참여합니다.

filemap_dirty_folio()는 개별 private block의 dirty 상태까지 알아서 갱신하지 않습니다. buffer 기반 filesystem이 folio 전체를 dirty로 만드는 경우에는 block_dirty_folio()처럼 buffer 쪽을 함께 처리하는 callback이 필요합니다. 반대로 buffer 하나를 고친 bottom-up 경로에서 모든 buffer를 dirty로 만드는 것은 과도한 처리가 됩니다.

아래는 __folio_mark_dirty()의 전체입니다. 이름과 달리 이 함수 혼자 folio의 최초 dirty bit 전환부터 inode 목록 연결까지 모두 하는 것은 아닙니다. caller가 수행한 bit 전환에 이어 mark와 accounting을 처리합니다. XArray lock 아래의 검사는 folio->mapping의 존재이며, 임의의 다른 mapping을 넘겨도 equality를 검사해 고쳐 주는 API는 아닙니다. caller가 해당 mapping과 folio의 관계를 지켜야 합니다. shmem·swapcache는 일반 file dirty mark를 사용하는 경로가 아니라는 경고도 현재 코드에 있습니다.

__folio_mark_dirty_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 __folio_mark_dirty(struct folio *folio, struct address_space *mapping,
			     int warn)
{
	unsigned long flags;

	/*
	 * Shmem writeback relies on swap, and swap writeback is LRU based,
	 * not using the dirty mark.
	 */
	VM_WARN_ON_ONCE(folio_test_swapcache(folio) || shmem_mapping(mapping));

	xa_lock_irqsave(&mapping->i_pages, flags);
	if (folio->mapping) {	/* Race with truncate? */
		WARN_ON_ONCE(warn && !folio_test_uptodate(folio));
		folio_account_dirtied(folio, mapping);
		__xa_set_mark(&mapping->i_pages, folio->index,
			      PAGECACHE_TAG_DIRTY);
	}
	xa_unlock_irqrestore(&mapping->i_pages, flags);
}

inode metadata의 dirty는 data와 별개입니다

I_DIRTY_PAGES는 cached data, I_DIRTY_SYNC와 I_DIRTY_DATASYNC는 inode metadata의 저장 필요성을 나타냅니다. I_DIRTY_TIME은 lazytime으로 지연된 timestamp 갱신을 별도로 표현합니다. 파일을 읽어 atime이 바뀐 상황과 data를 고쳐 size까지 바뀐 상황을 같은 state 하나로 처리하지 않는 이유입니다.

atime, mtime, ctime도 구별해야 합니다. ctime은 creation time이 아니라 inode status 변경 시각입니다. relatime과 lazytime은 timestamp 갱신·저장 정책이 다르며, 이 선택을 data writeback을 하지 않는다는 뜻으로 읽어서는 안 됩니다.

relatime은 이전 atime이 mtime이나 ctime보다 이르거나 같은 경우 등을 기준으로 access timestamp 갱신을 줄입니다. 하루 이상 오래된 atime을 갱신하는 조건도 있으므로 mtime·ctime 비교만이 유일한 경우는 아닙니다. lazytime은 memory의 timestamp 갱신을 없애는 대신 inode의 disk 반영을 늦추는 정책입니다. 다른 metadata 변경, 명시적 sync, inode eviction이나 경과 시간 조건에서 저장할 수 있습니다. 두 정책을 같은 종류의 dirty data 생략으로 생각해서는 안 됩니다. mount(8)의 timestamp 정책

v6.18 __mark_inode_dirty()는 metadata dirty인 경우 filesystem의 dirty_inode callback에 통지할 수 있습니다. data만 dirty가 되는 경우와 같은 callback 비용을 무조건 반복하지 않습니다. lockless 빠른 검사 앞의 memory barrier는 반대편 writeback의 state 정리와 짝을 이루며, 이어지는 i_lock 아래에서 state를 다시 검사합니다. v6.18 fs/fs-writeback.c

metadata dirty에 timestamp 변경이 겹치면 I_DIRTY_TIME을 callback에 함께 전달한 뒤 별도 lazytime 표시를 정리할 수 있습니다. 반면 이 API는 data dirty와 time-only dirty를 한 번의 호출에서 함께 설정하는 용도로 사용하지 않습니다. 또 was_dirty는 i_state & I_DIRTY이지 모든 state bit의 OR가 아닙니다. time-only 상태를 data dirty와 같은 목록 정책으로 취급하면 안 됩니다.

아래는 lazytime과 metadata 경로도 자르지 않은 v6.18 __mark_inode_dirty() 전체입니다. 본문의 data dirty 설명을 함수 일부로 재구성한 C가 아니라 실제 전체 정의입니다.

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

void __mark_inode_dirty(struct inode *inode, int flags)
{
	struct super_block *sb = inode->i_sb;
	int dirtytime = 0;
	struct bdi_writeback *wb = NULL;

	trace_writeback_mark_inode_dirty(inode, flags);

	if (flags & I_DIRTY_INODE) {
		/*
		 * Inode timestamp update will piggback on this dirtying.
		 * We tell ->dirty_inode callback that timestamps need to
		 * be updated by setting I_DIRTY_TIME in flags.
		 */
		if (inode->i_state & I_DIRTY_TIME) {
			spin_lock(&inode->i_lock);
			if (inode->i_state & I_DIRTY_TIME) {
				inode->i_state &= ~I_DIRTY_TIME;
				flags |= I_DIRTY_TIME;
			}
			spin_unlock(&inode->i_lock);
		}

		/*
		 * Notify the filesystem about the inode being dirtied, so that
		 * (if needed) it can update on-disk fields and journal the
		 * inode.  This is only needed when the inode itself is being
		 * dirtied now.  I.e. it's only needed for I_DIRTY_INODE, not
		 * for just I_DIRTY_PAGES or I_DIRTY_TIME.
		 */
		trace_writeback_dirty_inode_start(inode, flags);
		if (sb->s_op->dirty_inode)
			sb->s_op->dirty_inode(inode,
				flags & (I_DIRTY_INODE | I_DIRTY_TIME));
		trace_writeback_dirty_inode(inode, flags);

		/* I_DIRTY_INODE supersedes I_DIRTY_TIME. */
		flags &= ~I_DIRTY_TIME;
	} else {
		/*
		 * Else it's either I_DIRTY_PAGES, I_DIRTY_TIME, or nothing.
		 * (We don't support setting both I_DIRTY_PAGES and I_DIRTY_TIME
		 * in one call to __mark_inode_dirty().)
		 */
		dirtytime = flags & I_DIRTY_TIME;
		WARN_ON_ONCE(dirtytime && flags != I_DIRTY_TIME);
	}

	/*
	 * Paired with smp_mb() in __writeback_single_inode() for the
	 * following lockless i_state test.  See there for details.
	 */
	smp_mb();

	if ((inode->i_state & flags) == flags)
		return;

	spin_lock(&inode->i_lock);
	if ((inode->i_state & flags) != flags) {
		const int was_dirty = inode->i_state & I_DIRTY;

		inode_attach_wb(inode, NULL);

		inode->i_state |= flags;

		/*
		 * Grab inode's wb early because it requires dropping i_lock and we
		 * need to make sure following checks happen atomically with dirty
		 * list handling so that we don't move inodes under flush worker's
		 * hands.
		 */
		if (!was_dirty) {
			wb = locked_inode_to_wb_and_lock_list(inode);
			spin_lock(&inode->i_lock);
		}

		/*
		 * If the inode is queued for writeback by flush worker, just
		 * update its dirty state. Once the flush worker is done with
		 * the inode it will place it on the appropriate superblock
		 * list, based upon its state.
		 */
		if (inode->i_state & I_SYNC_QUEUED)
			goto out_unlock;

		/*
		 * Only add valid (hashed) inodes to the superblock's
		 * dirty list.  Add blockdev inodes as well.
		 */
		if (!S_ISBLK(inode->i_mode)) {
			if (inode_unhashed(inode))
				goto out_unlock;
		}
		if (inode->i_state & I_FREEING)
			goto out_unlock;

		/*
		 * If the inode was already on b_dirty/b_io/b_more_io, don't
		 * reposition it (that would break b_dirty time-ordering).
		 */
		if (!was_dirty) {
			struct list_head *dirty_list;
			bool wakeup_bdi = false;

			inode->dirtied_when = jiffies;
			if (dirtytime)
				inode->dirtied_time_when = jiffies;

			if (inode->i_state & I_DIRTY)
				dirty_list = &wb->b_dirty;
			else
				dirty_list = &wb->b_dirty_time;

			wakeup_bdi = inode_io_list_move_locked(inode, wb,
							       dirty_list);

			/*
			 * If this is the first dirty inode for this bdi,
			 * we have to wake-up the corresponding bdi thread
			 * to make sure background write-back happens
			 * later.
			 */
			if (wakeup_bdi &&
			    (wb->bdi->capabilities & BDI_CAP_WRITEBACK))
				wb_wakeup_delayed(wb);

			spin_unlock(&wb->list_lock);
			spin_unlock(&inode->i_lock);
			trace_writeback_dirty_inode_enqueue(inode);

			return;
		}
	}
out_unlock:
	if (wb)
		spin_unlock(&wb->list_lock);
	spin_unlock(&inode->i_lock);
}

다시 수정해도 inode를 매번 맨 앞으로 옮기지는 않습니다

clean에서 dirty로 전환된 inode는 dirtied_when을 설정하고 dirty 목록에 들어갑니다. 이미 dirty 목록이나 I/O 목록에 있는 inode를 수정할 때마다 새 inode처럼 옮기면 시간 순서가 깨집니다. 아주 자주 쓰는 파일이 영원히 새것으로 취급되어 저장이 밀리는 문제도 생깁니다.

I_SYNC_QUEUED가 설정되어 이미 flusher가 맡은 inode라면 state를 갱신하되 목록 배치는 flusher의 후처리에 맡깁니다. 정상 inode cache에 들어 있지 않은 unhashed inode는 목록에 넣지 않는 조건이 있으며 block-device inode는 예외입니다. I_FREEING인 inode도 새 작업처럼 추가하지 않습니다.

첫 dirty inode를 추가하면서 writeback 목록이 비어 있던 상태에서 바뀌면 delayed work를 깨울 필요가 생깁니다. 이때도 BDI가 writeback을 지원하는지 확인합니다. 중요한 것은 dirty bit를 세웠다는 사실이 아니라, 나중에 이를 찾을 수 있는 검색 경로와 worker 실행 조건까지 연결되었다는 사실입니다.

wb association을 얻는 helper는 i_lock을 보유하고 들어가지만 반환할 때에는 그 lock을 놓고 wb->list_lock을 보유합니다. 두 lock을 거꾸로 중첩하지 않으려는 계약입니다. cgroup writeback 활성 경로는 잠깐 wb 참조를 얻어 i_lock을 놓고 list lock을 얻은 뒤 association을 재검사합니다. 바뀌었으면 양쪽을 정리하고 반복하며, 성공하면 inode 자체의 wb 참조가 수명을 유지합니다. caller는 이어 i_lock을 다시 얻습니다.

locked_inode_to_wb_and_lock_list_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
 * CONFIG_CGROUP_WRITEBACK enabled definition.
 * Kernel excerpt; not a standalone program.
 * The book's exact source tag is not asserted.
 */

static struct bdi_writeback *
locked_inode_to_wb_and_lock_list(struct inode *inode)
	__releases(&inode->i_lock)
	__acquires(&wb->list_lock)
{
	while (true) {
		struct bdi_writeback *wb = inode_to_wb(inode);

		/*
		 * inode_to_wb() association is protected by both
		 * @inode->i_lock and @wb->list_lock but list_lock nests
		 * outside i_lock.  Drop i_lock and verify that the
		 * association hasn't changed after acquiring list_lock.
		 */
		wb_get(wb);
		spin_unlock(&inode->i_lock);
		spin_lock(&wb->list_lock);

		/* i_wb may have changed inbetween, can't use inode_to_wb() */
		if (likely(wb == inode->i_wb)) {
			wb_put(wb);	/* @inode already has ref */
			return wb;
		}

		spin_unlock(&wb->list_lock);
		wb_put(wb);
		cpu_relax();
		spin_lock(&inode->i_lock);
	}
}

CONFIG_CGROUP_WRITEBACK이 꺼진 경로는 root wb가 바뀌지 않으므로 다음 전체 정의를 사용합니다. 조건부 정의 두 개를 한 호출에서 연달아 실행하는 것은 아닙니다.

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

static struct bdi_writeback *
locked_inode_to_wb_and_lock_list(struct inode *inode)
	__releases(&inode->i_lock)
	__acquires(&wb->list_lock)
{
	struct bdi_writeback *wb = inode_to_wb(inode);

	spin_unlock(&inode->i_lock);
	spin_lock(&wb->list_lock);
	return wb;
}

목록에 연결하는 node는 inode->i_io_list입니다. 아래 두 helper에서 b_dirty_time은 실제 dirty I/O로 세지 않는 예외와 WB_has_dirty_io의 최초 전환을 확인할 수 있습니다. 원본의 오래된 부분 코드와 달리 현재 __mark_inode_dirty()는 이 경로의 delayed wakeup을 list lock과 inode lock을 놓기 전에 수행합니다. 그 뒤 unlock하므로 lock 해제 위치까지 버전 간에 같다고 가정하지 않습니다.

inode_io_list_move_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
 * 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 inode_io_list_move_locked(struct inode *inode,
				      struct bdi_writeback *wb,
				      struct list_head *head)
{
	assert_spin_locked(&wb->list_lock);
	assert_spin_locked(&inode->i_lock);
	WARN_ON_ONCE(inode->i_state & I_FREEING);

	list_move(&inode->i_io_list, head);

	/* dirty_time doesn't count as dirty_io until expiration */
	if (head != &wb->b_dirty_time)
		return wb_io_lists_populated(wb);

	wb_io_lists_depopulated(wb);
	return false;
}

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

시간 기준인 dirtied_when은 jiffies로 기록합니다. 1 jiffy를 고정된 millisecond 값으로 가정하지 않고 kernel의 tick 설정과 time helper를 통해 해석해야 합니다. raw block device의 dirty host inode도 /dev node inode가 아니라 해당 device의 내부 data mapping inode라는 점은 이 시간 추적에도 적용됩니다.

참고 자료