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하는 일을 피하기 위해서입니다.
// 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을 유지하지 않습니다.
// 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 수는 아닙니다.
// 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로 이동합니다.
// 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);
}
// 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);
}
// 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입니다.
// 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 해제와 교환해서 사용할 수는 없습니다.
// 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()전체 경로.