dirty data를 주기적으로 저장하던 중 application이 명시적인 sync를 요청하면 무엇을 먼저 해야 할까요? 오래된 모든 파일을 끝없이 처리하다 보면 sync가 뒤로 밀립니다. 반대로 새 요청마다 기존 진행을 버리면 dirty data가 쌓일 수 있습니다.
flusher는 pending work와 inode I/O 목록을 별도로 관리합니다. 전자는 요청의 우선순위를 표현하고, 후자는 지금 제출할 inode와 다음 차례로 미룬 inode를 표현합니다. upstream v6.18의 wb_workfn()에서 이 두 층이 만납니다.
일반 worker와 rescuer의 작업량은 다릅니다
평소 writeback은 workqueue의 일반 kworker에서 수행됩니다. worker description에 BDI 이름을 넣어 어떤 device를 flush하는지 식별할 수 있습니다. kworker 이름의 숫자를 곧바로 특정 CPU 번호라고 해석해서는 안 됩니다. v6.18의 format_worker_id()는 unbound 이름에 pool ID와 worker ID를 넣습니다. 예를 들어 u32:0의 0은 현재 CPU 번호가 아니라 worker ID입니다. 배치는 별도로 결정됩니다. v6.18 worker 이름 생성
현재 이름 생성 함수 전체는 다음과 같습니다. rescuer 이름도 v6.18에서는 kworker/R- prefix를 사용하므로, 과거 시스템의 [writeback]과 /proc/.../stack 출력 주소를 현재 시스템에서 그대로 재현되는 결과로 제시하지 않습니다. kernel config와 실행 시점에 따라 stack과 worker가 달라지며 이 글에서는 그 관측을 실행하지 않았습니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete scope from Linux v6.18 kernel/workqueue.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/kernel/workqueue.c
* Kernel excerpt; not a standalone program.
* The book exact source tag is not asserted.
*/
/*
* kernel/workqueue.c - generic async execution with shared worker pool
*
* Copyright (C) 2002 Ingo Molnar
*
* Derived from the taskqueue/keventd code by:
* David Woodhouse <dwmw2@infradead.org>
* Andrew Morton
* Kai Petzke <wpp@marie.physik.tu-berlin.de>
* Theodore Ts'o <tytso@mit.edu>
*
* Made to use alloc_percpu by Christoph Lameter.
*
* Copyright (C) 2010 SUSE Linux Products GmbH
* Copyright (C) 2010 Tejun Heo <tj@kernel.org>
*
* This is the generic async execution mechanism. Work items as are
* executed in process context. The worker pool is shared and
* automatically managed. There are two worker pools for each CPU (one for
* normal work items and the other for high priority ones) and some extra
* pools for workqueues which are not bound to any specific CPU - the
* number of these backing pools is dynamic.
*
* Please read Documentation/core-api/workqueue.rst for details.
*/
static int format_worker_id(char *buf, size_t size, struct worker *worker,
struct worker_pool *pool)
{
if (worker->rescue_wq)
return scnprintf(buf, size, "kworker/R-%s",
worker->rescue_wq->name);
if (pool) {
if (pool->cpu >= 0)
return scnprintf(buf, size, "kworker/%d:%d%s",
pool->cpu, worker->id,
pool->attrs->nice < 0 ? "H" : "");
else
return scnprintf(buf, size, "kworker/u%d:%d",
pool->id, worker->id);
} else {
return scnprintf(buf, size, "kworker/dying");
}
}
procfs에서 이름과 stack을 관찰할 때에도 둘을 함께 읽는 편이 좋습니다. 다음은 어떤 write나 memory pressure도 만들지 않고 후보 process의 comm과 stack만 읽는 독립적인 Python 3 프로그램입니다. 모든 kworker/를 후보로 보여 주므로 이름이 보인다는 사실만으로 writeback이 실행 중이라고 판정하지 않습니다. stack에 writeback 관련 함수가 나타나는지와 관측 시각을 따로 확인해야 합니다. 이 예제는 여기서 Linux runtime 실행을 하지 않았으며 출력 예시를 실측으로 제시하지 않습니다.
"""Read process names and candidate kernel stacks without triggering I/O."""
from pathlib import Path
def main():
proc = Path('/proc')
if not (proc / 'self').exists():
raise SystemExit('Linux procfs is required')
candidates = 0
for path in sorted(proc.iterdir(), key=lambda p: p.name):
if not path.name.isdecimal():
continue
try:
name = (path / 'comm').read_text().strip()
except OSError:
continue
if name != 'writeback' and not name.startswith('kworker/'):
continue
candidates += 1
print(f'pid={path.name} comm={name}')
try:
stack = (path / 'stack').read_text()
except OSError as error:
print(f' stack unavailable: errno={error.errno}')
continue
print(stack.rstrip() or ' stack empty at observation time')
print(f'candidate_names={candidates}; names alone do not prove activity')
if __name__ == '__main__':
main()
Linux procfs가 필요하며 일반 권한에서는 stack을 읽지 못할 수 있습니다. 프로그램은 접근 거절과 process 종료 경쟁을 errno로 표시하고 다음 후보로 진행합니다. kernel config, ptrace 접근 검사와 보안 정책에 따라 권한을 높여도 stack을 못 볼 수 있습니다. comm은 길이가 제한되므로 rescuer의 전체 이름도 잘릴 수 있습니다. 또한 여러 파일을 읽는 과정은 원자적인 snapshot이 아닙니다. 후보가 없거나 stack이 비어 있다는 결과만으로 workqueue나 writeback이 비활성이라고 결론내릴 수 없습니다. proc_pid_stack, proc_pid_comm
자원 부족으로 rescuer를 사용하는 경우에는 제한된 emergency worker를 한 writeback 객체가 독점하지 않도록 별도 경로를 탑니다. 등록된 writeback 객체에 대해 1024 base page 정도의 작업을 시도하는 경로가 있습니다. 반면 shutdown 중에는 rescuer라도 pending work를 비워야 하므로 일반 drain 경로를 사용합니다.
작업을 마친 뒤 pending work가 남아 있으면 다시 즉시 깨웁니다. 그렇지 않고 dirty I/O와 주기 설정이 남아 있으면 delayed wakeup을 예약합니다. 모든 dirty 작업이 사라졌는데도 영구 timer처럼 무조건 돌지는 않습니다. v6.18 wb_workfn
아래는 v6.18 wb_workfn()과 rescuer 쪽에서 사용하는 writeback_inodes_wb() 전체입니다. 1024는 목표량이지 매번 정확히 1024 page가 성공적으로 저장된다는 보장은 아닙니다. 기존 b_io가 비어 있을 때에만 새 inode를 queue하고, 이 경우에는 현재 jiffies까지를 대상으로 삼습니다. periodic 만료 시간을 뺀 대상만 다루는 호출과 다릅니다.
// 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 wb_workfn(struct work_struct *work)
{
struct bdi_writeback *wb = container_of(to_delayed_work(work),
struct bdi_writeback, dwork);
long pages_written;
set_worker_desc("flush-%s", bdi_dev_name(wb->bdi));
if (likely(!current_is_workqueue_rescuer() ||
!test_bit(WB_registered, &wb->state))) {
/*
* The normal path. Keep writing back @wb until its
* work_list is empty. Note that this path is also taken
* if @wb is shutting down even when we're running off the
* rescuer as work_list needs to be drained.
*/
do {
pages_written = wb_do_writeback(wb);
trace_writeback_pages_written(pages_written);
} while (!list_empty(&wb->work_list));
} else {
/*
* bdi_wq can't get enough workers and we're running off
* the emergency worker. Don't hog it. Hopefully, 1024 is
* enough for efficient IO.
*/
pages_written = writeback_inodes_wb(wb, 1024,
WB_REASON_FORKER_THREAD);
trace_writeback_pages_written(pages_written);
}
if (!list_empty(&wb->work_list))
wb_wakeup(wb);
else if (wb_has_dirty_io(wb) && dirty_writeback_interval)
wb_wakeup_delayed(wb);
}
// 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, long nr_pages,
enum wb_reason reason)
{
struct wb_writeback_work work = {
.nr_pages = nr_pages,
.sync_mode = WB_SYNC_NONE,
.range_cyclic = 1,
.reason = reason,
};
struct blk_plug plug;
blk_start_plug(&plug);
spin_lock(&wb->list_lock);
if (list_empty(&wb->b_io))
queue_io(wb, &work, jiffies);
__writeback_inodes_wb(wb, &work);
spin_unlock(&wb->list_lock);
blk_finish_plug(&plug);
return nr_pages - work.nr_pages;
}
구현코드가 아닌 호출관계로 두 실행 경로를 정리하면 다음과 같습니다. queue_io()는 대상을 고르는 단계이며 I/O 완료를 기다리는 함수가 아닙니다.
wb_workfn
normal or shutdown -> wb_do_writeback
queued work -> wb_writeback
start_all, periodic, background checks -> wb_writeback
queue_io -> select inodes when b_io is empty
work->sb -> writeback_sb_inodes
no work->sb -> __writeback_inodes_wb -> writeback_sb_inodes
registered rescuer -> writeback_inodes_wb
queue_io -> select inodes when b_io is empty
__writeback_inodes_wb -> writeback_sb_inodes
writeback_sb_inodes -> __writeback_single_inode -> do_writepages
명시적 work를 먼저 꺼냅니다
wb_do_writeback()은 WB_writeback_running을 설정하고 pending work_list에서 항목을 하나씩 꺼냅니다. producer는 tail에 넣고 consumer는 head에서 꺼내므로 이 목록은 FIFO입니다. 꺼내는 짧은 구간은 work_lock으로 보호하고 실제 긴 writeback은 그 spinlock 아래에서 수행하지 않습니다.
각 work는 wb_writeback()으로 처리하고 completion·해제를 정리합니다. 그 뒤 start-all 요청, 주기적인 old-data flush, dirty background threshold에 따른 flush를 확인합니다. background 작업이 실행되는 동안 새 명시적 work가 들어오면 그 작업이 양보할 수 있어야 합니다.
WB_start_all은 모든 dirty 작업을 시작해 달라는 요청을 병합하는 상태입니다. 같은 종류의 wakeup이 반복될 때 중복 work 객체를 끝없이 만드는 대신 flag와 reason을 보관할 수 있습니다. 이것이 실제 I/O가 모두 끝났다는 flag는 아닙니다.
다음은 dispatcher와 work를 꺼내는 helper의 전체입니다. WB_writeback_running은 이 dispatcher가 수행 중인 상태이며 개별 folio의 writeback bit나 WB_start_all 요청 bit와 같지 않습니다. work를 꺼낼 때 list_del_init()으로 pending 연결을 끊은 다음 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 long wb_do_writeback(struct bdi_writeback *wb)
{
struct wb_writeback_work *work;
long wrote = 0;
set_bit(WB_writeback_running, &wb->state);
while ((work = get_next_work_item(wb)) != NULL) {
trace_writeback_exec(wb, work);
wrote += wb_writeback(wb, work);
finish_writeback_work(work);
}
/*
* Check for a flush-everything request
*/
wrote += wb_check_start_all(wb);
/*
* Check for periodic writeback, kupdated() style
*/
wrote += wb_check_old_data_flush(wb);
wrote += wb_check_background_flush(wb);
clear_bit(WB_writeback_running, &wb->state);
return wrote;
}
// 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 struct wb_writeback_work *get_next_work_item(struct bdi_writeback *wb)
{
struct wb_writeback_work *work = NULL;
spin_lock_irq(&wb->work_lock);
if (!list_empty(&wb->work_list)) {
work = list_entry(wb->work_list.next,
struct wb_writeback_work, list);
list_del_init(&work->list);
}
spin_unlock_irq(&wb->work_lock);
return work;
}
요청 병합은 wb_start_writeback()에서 bit를 test-and-set하고 reason을 남기는 것으로 시작합니다. 같은 bit를 여기서 즉시 clear하는 것은 아닙니다. 처리 쪽 wb_check_start_all()이 dirty량을 얻어 cyclic WB_SYNC_NONE work를 실행하고 마지막에 bit를 clear합니다. dirty가 없다면 work를 만들지 않고 종료할 수 있습니다. v6.18 두 전체 함수
// 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_start_writeback(struct bdi_writeback *wb, enum wb_reason reason)
{
if (!wb_has_dirty_io(wb))
return;
/*
* All callers of this function want to start writeback of all
* dirty pages. Places like vmscan can call this at a very
* high frequency, causing pointless allocations of tons of
* work items and keeping the flusher threads busy retrieving
* that work. Ensure that we only allow one of them pending and
* inflight at the time.
*/
if (test_bit(WB_start_all, &wb->state) ||
test_and_set_bit(WB_start_all, &wb->state))
return;
wb->start_all_reason = reason;
wb_wakeup(wb);
}
// 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_check_start_all(struct bdi_writeback *wb)
{
long nr_pages;
if (!test_bit(WB_start_all, &wb->state))
return 0;
nr_pages = get_nr_dirty_pages();
if (nr_pages) {
struct wb_writeback_work work = {
.nr_pages = wb_split_bdi_pages(wb, nr_pages),
.sync_mode = WB_SYNC_NONE,
.range_cyclic = 1,
.reason = wb->start_all_reason,
};
nr_pages = wb_writeback(wb, &work);
}
clear_bit(WB_start_all, &wb->state);
return nr_pages;
}
모든 BDI를 깨우는 경로는 RCU 아래에서 bdi_list를 순회하고 각 BDI의 wb 목록을 확인합니다. 진행을 기대하기 전에 caller가 모아 둔 block plug의 I/O도 내보냅니다. 이 경로는 기다려서 모든 I/O의 성공을 수집하는 API가 아닙니다.
wakeup_flusher_threads_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 wakeup_flusher_threads(enum wb_reason reason)
{
struct backing_dev_info *bdi;
/*
* If we are expecting writeback progress we must submit plugged IO.
*/
blk_flush_plug(current->plug, true);
rcu_read_lock();
list_for_each_entry_rcu(bdi, &bdi_list, bdi_list)
__wakeup_flusher_threads_bdi(bdi, reason);
rcu_read_unlock();
}
__wakeup_flusher_threads_bdi_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 __wakeup_flusher_threads_bdi(struct backing_dev_info *bdi,
enum wb_reason reason)
{
struct bdi_writeback *wb;
if (!bdi_has_dirty_io(bdi))
return;
list_for_each_entry_rcu(wb, &bdi->wb_list, bdi_node)
wb_start_writeback(wb, reason);
}
dirty balancing이 사용하는 wb_start_background_writeback()도 별도 work를 무조건 할당하는 함수가 아닙니다. worker를 깨우고 실제 background 검사는 dispatcher에 맡깁니다.
wb_start_background_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
*/
void wb_start_background_writeback(struct bdi_writeback *wb)
{
/*
* We just wake up the flusher thread. It will perform background
* writeback as soon as there is no other work to do.
*/
trace_writeback_wake_background(wb);
wb_wakeup(wb);
}
sync()뿐 아니라 reclaim도 flusher를 깨울 수 있습니다. 예를 들어 v6.18 shrink_inactive_list()는 스캔한 대상이 아직 I/O에 queue되지 않은 dirty folio들인 경우 WB_REASON_VMSCAN으로 wakeup을 요청합니다. reclaim 호출마다 모든 dirty data를 동기 저장하는 것은 아닙니다. 해당 v6.18 reclaim 조건
주기 설정을 바꾸는 경로도 별도입니다. 다음은 CONFIG_SYSCTL의 v6.18 handler 전체입니다. 정상적인 write였고 interval이 이전과 다른 nonzero 값일 때에만 flusher wakeup을 요청합니다. read나 실패한 write, 0으로 바꾼 경우까지 모두 깨운다고 일반화하지 않습니다. 아래 code를 확인한 것이며 실제 sysctl 변경은 수행하지 않았습니다. v6.18 page-writeback.c
dirty_writeback_centisecs_handler_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 exact source tag is not asserted.
*/
static int dirty_writeback_centisecs_handler(const struct ctl_table *table, int write,
void *buffer, size_t *length, loff_t *ppos)
{
unsigned int old_interval = dirty_writeback_interval;
int ret;
ret = proc_dointvec(table, write, buffer, length, ppos);
/*
* Writing 0 to dirty_writeback_interval will disable periodic writeback
* and a different non-zero value will wakeup the writeback threads.
* wb_wakeup_delayed() would be more appropriate, but it's a pain to
* iterate over all bdis and wbs.
* The reason we do this is to make the change take effect immediately.
*/
if (!ret && write && dirty_writeback_interval &&
dirty_writeback_interval != old_interval)
wakeup_flusher_threads(WB_REASON_PERIODIC);
return ret;
}
주기와 만료는 서로 다른 시간입니다
dirty_writeback_interval은 언제 old-data 검사를 다시 할지 정하는 주기입니다. dirty_expire_interval은 어떤 inode를 오래된 대상으로 볼지 정합니다. 둘 다 centisecond 기반 tunable과 연결되지만 동일한 값이나 변수는 아닙니다.
wb_check_old_data_flush()는 주기가 0이면 해당 periodic 작업을 하지 않고, last_old_flush 이후 정해진 주기가 지났는지 확인합니다. 실제 old-data 작업이 inode를 고를 때에는 현재 시각에서 expire 기간을 뺀 기준을 사용합니다. 주기를 0으로 했다고 명시적 sync나 dirty pressure에 의한 writeback까지 비활성화되는 것은 아닙니다.
for_kupdate와 for_background도 구별됩니다. 전자는 만료된 대상을 주기적으로 처리하고, 후자는 dirty량을 background threshold 아래로 낮추는 목적입니다. 모든 background 실행이 만료 시각을 기다리는 것은 아닙니다.
아래의 두 전체 함수를 비교하면 주기 검사에는 dirty_writeback_interval, inode 만료 계산에는 dirty_expire_interval이 쓰인다는 차이가 드러납니다. last_old_flush를 갱신한 뒤 dirty량이 0이면 I/O 없이 반환할 수도 있습니다. background 쪽은 wb_over_bg_thresh()가 true인 경우 for_background work를 만들며 LONG_MAX 목표를 주더라도 이후 threshold 해소나 다른 work 때문에 종료할 수 있습니다.
wb_check_old_data_flush_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_check_old_data_flush(struct bdi_writeback *wb)
{
unsigned long expired;
long nr_pages;
/*
* When set to zero, disable periodic writeback
*/
if (!dirty_writeback_interval)
return 0;
expired = wb->last_old_flush +
msecs_to_jiffies(dirty_writeback_interval * 10);
if (time_before(jiffies, expired))
return 0;
wb->last_old_flush = jiffies;
nr_pages = get_nr_dirty_pages();
if (nr_pages) {
struct wb_writeback_work work = {
.nr_pages = nr_pages,
.sync_mode = WB_SYNC_NONE,
.for_kupdate = 1,
.range_cyclic = 1,
.reason = WB_REASON_PERIODIC,
};
return wb_writeback(wb, &work);
}
return 0;
}
wb_check_background_flush_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_check_background_flush(struct bdi_writeback *wb)
{
if (wb_over_bg_thresh(wb)) {
struct wb_writeback_work work = {
.nr_pages = LONG_MAX,
.sync_mode = WB_SYNC_NONE,
.for_background = 1,
.range_cyclic = 1,
.reason = WB_REASON_BACKGROUND,
};
return wb_writeback(wb, &work);
}
return 0;
}
b_dirty와 b_io와 b_more_io를 구별합니다
처음 dirty가 된 inode는 b_dirty에 들어갑니다. 지금 제출할 대상은 b_io, 이전 차례에서 더 처리해야 할 대상은 b_more_io에 있을 수 있습니다. 이 목록은 BIO 목록이 아니라 inode의 i_io_list 연결입니다.

그림은 일반 data dirty 경로의 목록 이동을 나타냅니다. timestamp 전용 목록, cgroup 이동과 filesystem별 I/O 실패는 생략했습니다.
queue_io()는 먼저 b_more_io를 b_io로 splice하고, move_expired_inodes()로 기준 시각에 맞는 dirty inode를 추가합니다. 목록의 물리적 head와 실제 처리 순서에 주의해야 합니다. consumer는 b_io의 뒤에서 꺼내므로 먼저 등록된 미완료 작업이 다시 기회를 얻도록 배치됩니다.
b_dirty는 새 inode가 앞에 있는 시간 순서를 유지합니다. 뒤에서부터 기준보다 오래된 inode를 찾고, I_SYNC_QUEUED를 설정하며 임시 목록으로 옮깁니다. 여러 superblock이 섞여 있다면 같은 superblock끼리 모읍니다. 이후 한 superblock을 unmount로부터 보호한 상태에서 그 묶음을 처리할 수 있기 때문입니다.
이 그룹화 뒤에도 모든 inode가 전역의 엄격한 oldest-first 순서를 유지하는 것은 아닙니다. 같은 superblock 안의 순서를 유지하며 묶음을 처리합니다. 실제 list 방향과 전체 queue_io()·move_expired_inodes()는 inode 목록을 묶는 과정에서 독립 예시로 확인할 수 있습니다.
고정한 시각은 새 inode를 무한히 쫓지 않게 합니다
일반적인 명시적 work에서는 wb_writeback() 시작 때 잡은 dirtied_before를 기준으로 사용합니다. 시작 이후 새로 dirty가 된 inode를 매번 같은 작업에 포함하면 생산이 소비보다 빠를 때 종료하지 못할 수 있습니다.

그림은 한 work의 대상 선택을 설명합니다. background·kupdate 작업은 loop에서 시각 기준을 갱신할 수 있다는 예외와 jiffies wrap 처리는 생략했습니다.
background 작업은 현재 threshold를 낮추려는 목적이므로 기준을 현재 시각으로 갱신할 수 있습니다. kupdate도 expire 기준을 다시 계산할 수 있습니다. v6.18은 b_io가 비어 새 queue_io()를 수행할 때 그 기준을 갱신합니다. 매 loop마다 무조건 바꾸는 것은 아닙니다. 이 두 경로는 명시적 work가 들어오면 양보하는 것으로 livelock 위험을 조절합니다.
wb_writeback()은 목표량 소진, 새 고우선 work, background threshold 해소 등을 종료 조건으로 확인합니다. 진전이 있으면 계속 처리할 수 있지만, 진전 없이 b_more_io만 남으면 inode의 I_SYNC 종료를 기다려 busy-loop를 피합니다. 목록이 비지 않았다는 이유만으로 CPU를 계속 쓰며 재시도하는 설계가 아닙니다.
다음은 v6.18 함수 전체입니다. queued는 이 호출에서 새 queueing을 한 적이 있는지 나타냅니다. 이미 남아 있던 b_io만 처리한 뒤 아직 한 번도 queueing하지 않았다면, 그 처리에서 진전이 없더라도 즉시 전체 완료로 판단하지 않고 다음 loop를 허용합니다. progress || !queued 분기를 빼고 읽으면 왜 한 번 더 진행하는지 놓치기 쉽습니다.
// 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_writeback(struct bdi_writeback *wb,
struct wb_writeback_work *work)
{
long nr_pages = work->nr_pages;
unsigned long dirtied_before = jiffies;
struct inode *inode;
long progress;
struct blk_plug plug;
bool queued = false;
blk_start_plug(&plug);
for (;;) {
/*
* Stop writeback when nr_pages has been consumed
*/
if (work->nr_pages <= 0)
break;
/*
* Background writeout and kupdate-style writeback may
* run forever. Stop them if there is other work to do
* so that e.g. sync can proceed. They'll be restarted
* after the other works are all done.
*/
if ((work->for_background || work->for_kupdate) &&
!list_empty(&wb->work_list))
break;
/*
* For background writeout, stop when we are below the
* background dirty threshold
*/
if (work->for_background && !wb_over_bg_thresh(wb))
break;
spin_lock(&wb->list_lock);
trace_writeback_start(wb, work);
if (list_empty(&wb->b_io)) {
/*
* Kupdate and background works are special and we want
* to include all inodes that need writing. Livelock
* avoidance is handled by these works yielding to any
* other work so we are safe.
*/
if (work->for_kupdate) {
dirtied_before = jiffies -
msecs_to_jiffies(dirty_expire_interval *
10);
} else if (work->for_background)
dirtied_before = jiffies;
queue_io(wb, work, dirtied_before);
queued = true;
}
if (work->sb)
progress = writeback_sb_inodes(work->sb, wb, work);
else
progress = __writeback_inodes_wb(wb, work);
trace_writeback_written(wb, work);
/*
* Did we write something? Try for more
*
* Dirty inodes are moved to b_io for writeback in batches.
* The completion of the current batch does not necessarily
* mean the overall work is done. So we keep looping as long
* as made some progress on cleaning pages or inodes.
*/
if (progress || !queued) {
spin_unlock(&wb->list_lock);
continue;
}
/*
* No more inodes for IO, bail
*/
if (list_empty(&wb->b_more_io)) {
spin_unlock(&wb->list_lock);
break;
}
/*
* Nothing written. Wait for some inode to
* become available for writeback. Otherwise
* we'll just busyloop.
*/
trace_writeback_wait(wb, work);
inode = wb_inode(wb->b_more_io.prev);
spin_lock(&inode->i_lock);
spin_unlock(&wb->list_lock);
/* This function drops i_lock... */
inode_sleep_on_writeback(inode);
}
blk_finish_plug(&plug);
return nr_pages - work->nr_pages;
}
목록 spinlock은 threshold 검사를 포함한 함수 전체에서 계속 들고 있지 않습니다. inode 선택·목록 이동을 위해 얻었다가 진행 후 놓고, 대기할 때에는 inode의 i_lock을 얻은 뒤 목록 lock을 놓습니다. inode_sleep_on_writeback() 자체가 i_lock을 놓으므로 그 반환 뒤에 다시 unlock하지 않습니다. 반환값 nr_pages - work->nr_pages도 목표량 accounting이지 성공적인 durable byte 수가 아닙니다.
역사적 구현을 비교할 때에는 v6.0 wb_writeback() 전체를 따로 봅니다. 이 버전은 list_lock의 배치와 cutoff 갱신 위치가 다르고 위 queued 상태가 없습니다. 책 code의 정확한 tag라고 단정하는 대신, 어느 버전의 실제 분기를 설명하는지 명시합니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete scope from Linux v6.0 fs/fs-writeback.c.
* Source: https://github.com/torvalds/linux/blob/v6.0/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_writeback(struct bdi_writeback *wb,
struct wb_writeback_work *work)
{
long nr_pages = work->nr_pages;
unsigned long dirtied_before = jiffies;
struct inode *inode;
long progress;
struct blk_plug plug;
blk_start_plug(&plug);
spin_lock(&wb->list_lock);
for (;;) {
/*
* Stop writeback when nr_pages has been consumed
*/
if (work->nr_pages <= 0)
break;
/*
* Background writeout and kupdate-style writeback may
* run forever. Stop them if there is other work to do
* so that e.g. sync can proceed. They'll be restarted
* after the other works are all done.
*/
if ((work->for_background || work->for_kupdate) &&
!list_empty(&wb->work_list))
break;
/*
* For background writeout, stop when we are below the
* background dirty threshold
*/
if (work->for_background && !wb_over_bg_thresh(wb))
break;
/*
* Kupdate and background works are special and we want to
* include all inodes that need writing. Livelock avoidance is
* handled by these works yielding to any other work so we are
* safe.
*/
if (work->for_kupdate) {
dirtied_before = jiffies -
msecs_to_jiffies(dirty_expire_interval * 10);
} else if (work->for_background)
dirtied_before = jiffies;
trace_writeback_start(wb, work);
if (list_empty(&wb->b_io))
queue_io(wb, work, dirtied_before);
if (work->sb)
progress = writeback_sb_inodes(work->sb, wb, work);
else
progress = __writeback_inodes_wb(wb, work);
trace_writeback_written(wb, work);
/*
* Did we write something? Try for more
*
* Dirty inodes are moved to b_io for writeback in batches.
* The completion of the current batch does not necessarily
* mean the overall work is done. So we keep looping as long
* as made some progress on cleaning pages or inodes.
*/
if (progress)
continue;
/*
* No more inodes for IO, bail
*/
if (list_empty(&wb->b_more_io))
break;
/*
* Nothing written. Wait for some inode to
* become available for writeback. Otherwise
* we'll just busyloop.
*/
trace_writeback_wait(wb, work);
inode = wb_inode(wb->b_more_io.prev);
spin_lock(&inode->i_lock);
spin_unlock(&wb->list_lock);
/* This function drops i_lock... */
inode_sleep_on_writeback(inode);
spin_lock(&wb->list_lock);
}
spin_unlock(&wb->list_lock);
blk_finish_plug(&plug);
return nr_pages - work->nr_pages;
}
이 글에 포함한 전체 kernel 함수들은 각 source의 GPL-2.0 license를 따르며 kernel 문맥을 전제로 합니다. 위 code를 조합해 단독 compile하는 예제나 현재 장치의 실측 결과는 아닙니다.
참고 자료
- Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 10, Flusher Thread Operation의 worker, dispatch, queueing 설명.
- Linux v6.18 fs/fs-writeback.c:
wb_workfn(),wb_do_writeback(),queue_io(),move_expired_inodes(),wb_writeback(). - Linux v6.18 VM tunable documentation: dirty writeback 주기와 만료 조건.
- Linux v6.18 Workqueue documentation: worker pool, unbound 배치와 rescuer.