filesystem 전체를 동기화하려는 task와 주기적으로 오래된 data를 쓰는 작업은 결국 같은 writeback 기반을 사용할 수 있습니다. 그렇지만 요청한 범위, 대기 여부, 객체 수명은 다릅니다. background timer만 깨우는 것으로 file sync의 완료를 표현할 수는 없습니다.
Linux는 bdi_writeback별 delayed work와 명시적인 wb_writeback_work 목록을 함께 사용합니다. worker가 실행될 기회를 만드는 장치와 worker가 해야 할 요청을 저장하는 장치를 구별하는 것이 출발점입니다.
workqueue는 device마다 고정 thread를 만드는 API가 아닙니다
bdi_wq는 비동기 writeback 작업들이 공유하는 workqueue입니다. 초기화 시 WQ_UNBOUND와 WQ_MEM_RECLAIM 조건을 사용합니다. 전자는 특정 CPU에 묶인 작업 배치를 피하는 선택이고, 후자는 메모리 부족으로 일반 worker 확보가 어려울 때에도 필요한 진행을 보장하기 위한 rescuer 기반과 연결됩니다.
따라서 BDI 하나가 생기면 전용 kernel thread 하나가 항상 살아 있다고 이해해서는 안 됩니다. workqueue의 worker pool이 실행 context를 제공합니다. WQ_MEM_RECLAIM도 메모리 부족이나 I/O 실패가 절대로 생기지 않는다는 보장은 아닙니다. v6.18 mm/backing-dev.c
다음은 v6.18 default_bdi_init() 전체입니다. queue 할당 실패는 -ENOMEM으로 반환합니다. WQ_SYSFS는 workqueue 설정의 sysfs 노출과 관계된 flag이며, WQ_UNBOUND가 모든 CPU에서 무제한으로 동시에 실행한다는 뜻은 아닙니다. affinity와 worker pool의 제약은 여전히 적용됩니다. Workqueue API
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete scope from Linux v6.18 mm/backing-dev.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/backing-dev.c
* Kernel excerpt; not a standalone program.
* The book exact source tag is not asserted.
*/
static int __init default_bdi_init(void)
{
bdi_wq = alloc_workqueue("writeback", WQ_MEM_RECLAIM | WQ_UNBOUND |
WQ_SYSFS, 0);
if (!bdi_wq)
return -ENOMEM;
return 0;
}
각 bdi_writeback의 dwork는 wb_workfn()을 실행합니다. dirty 상태가 없으면 불필요하게 주기적 작업을 반복하지 않도록 dirty 목록 상태와 재예약 조건을 연결합니다. WB_registered는 shutdown 중인 객체에 새 delayed work를 넣지 않는 검사에도 사용됩니다.
wb_init() 전체를 보면 inode 목록과 work 목록을 각각 초기화하고, dwork에는 wb_workfn, bw_dwork에는 bandwidth 갱신 callback을 연결합니다. per-CPU 통계 할당이 실패하면 앞서 만든 completion 비율 통계도 파괴하고 error를 반환합니다. 초기화 중인 객체를 이미 등록되어 work를 받을 수 있는 객체와 같게 보아서는 안 됩니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete scope from Linux v6.18 mm/backing-dev.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/backing-dev.c
* Kernel excerpt; not a standalone program.
* The book exact source tag is not asserted.
*/
static int wb_init(struct bdi_writeback *wb, struct backing_dev_info *bdi,
gfp_t gfp)
{
int err;
memset(wb, 0, sizeof(*wb));
wb->bdi = bdi;
wb->last_old_flush = jiffies;
INIT_LIST_HEAD(&wb->b_dirty);
INIT_LIST_HEAD(&wb->b_io);
INIT_LIST_HEAD(&wb->b_more_io);
INIT_LIST_HEAD(&wb->b_dirty_time);
spin_lock_init(&wb->list_lock);
atomic_set(&wb->writeback_inodes, 0);
wb->bw_time_stamp = jiffies;
wb->balanced_dirty_ratelimit = INIT_BW;
wb->dirty_ratelimit = INIT_BW;
wb->write_bandwidth = INIT_BW;
wb->avg_write_bandwidth = INIT_BW;
spin_lock_init(&wb->work_lock);
INIT_LIST_HEAD(&wb->work_list);
INIT_DELAYED_WORK(&wb->dwork, wb_workfn);
INIT_DELAYED_WORK(&wb->bw_dwork, wb_update_bandwidth_workfn);
err = fprop_local_init_percpu(&wb->completions, gfp);
if (err)
return err;
err = percpu_counter_init_many(wb->stat, 0, gfp, NR_WB_STAT_ITEMS);
if (err)
fprop_local_destroy_percpu(&wb->completions);
return err;
}
wb의 inode 목록과 요청 목록은 다릅니다
아래는 v6.18 struct bdi_writeback 전체입니다. b_dirty, b_io, b_more_io, b_dirty_time은 inode의 목록이며 list_lock으로 보호합니다. work_list는 worker에게 전달할 요청의 목록이고 work_lock은 그 목록과 delayed-work 예약을 보호합니다. 두 목록의 원소와 lock을 바꿔 읽으면 수명과 lock ordering도 잘못 따라가게 됩니다.
// SPDX-License-Identifier: GPL-2.0
/*
* Complete scope from Linux v6.18 include/linux/backing-dev-defs.h.
* Source: https://github.com/torvalds/linux/blob/v6.18/include/linux/backing-dev-defs.h
* Kernel excerpt; not a standalone program.
* The book's exact source tag is not asserted.
*/
struct bdi_writeback {
struct backing_dev_info *bdi; /* our parent bdi */
unsigned long state; /* Always use atomic bitops on this */
unsigned long last_old_flush; /* last old data flush */
struct list_head b_dirty; /* dirty inodes */
struct list_head b_io; /* parked for writeback */
struct list_head b_more_io; /* parked for more writeback */
struct list_head b_dirty_time; /* time stamps are dirty */
spinlock_t list_lock; /* protects the b_* lists */
atomic_t writeback_inodes; /* number of inodes under writeback */
struct percpu_counter stat[NR_WB_STAT_ITEMS];
unsigned long bw_time_stamp; /* last time write bw is updated */
unsigned long dirtied_stamp;
unsigned long written_stamp; /* pages written at bw_time_stamp */
unsigned long write_bandwidth; /* the estimated write bandwidth */
unsigned long avg_write_bandwidth; /* further smoothed write bw, > 0 */
/*
* The base dirty throttle rate, re-calculated on every 200ms.
* All the bdi tasks' dirty rate will be curbed under it.
* @dirty_ratelimit tracks the estimated @balanced_dirty_ratelimit
* in small steps and is much more smooth/stable than the latter.
*/
unsigned long dirty_ratelimit;
unsigned long balanced_dirty_ratelimit;
struct fprop_local_percpu completions;
int dirty_exceeded;
enum wb_reason start_all_reason;
spinlock_t work_lock; /* protects work_list & dwork scheduling */
struct list_head work_list;
struct delayed_work dwork; /* work item used for writeback */
struct delayed_work bw_dwork; /* work item used for bandwidth estimate */
struct list_head bdi_node; /* anchored at bdi->wb_list */
#ifdef CONFIG_CGROUP_WRITEBACK
struct percpu_ref refcnt; /* used only for !root wb's */
struct fprop_local_percpu memcg_completions;
struct cgroup_subsys_state *memcg_css; /* the associated memcg */
struct cgroup_subsys_state *blkcg_css; /* and blkcg */
struct list_head memcg_node; /* anchored at memcg->cgwb_list */
struct list_head blkcg_node; /* anchored at blkcg->cgwb_list */
struct list_head b_attached; /* attached inodes, protected by list_lock */
struct list_head offline_node; /* anchored at offline_cgwbs */
struct work_struct switch_work; /* work used to perform inode switching
* to this wb */
struct llist_head switch_wbs_ctxs; /* queued contexts for
* writeback switching */
union {
struct work_struct release_work;
struct rcu_head rcu;
};
#endif
};
b_io는 writeback을 위해 골라 둔 inode, b_more_io는 추가 처리가 필요한 inode를 담습니다. 후자가 반드시 다음 periodic interval까지 기다리는 전용 목록은 아닙니다. worker가 현재 work 안에서 다시 선택할 수도 있으므로 언제 옮기는지는 dispatch·requeue 조건과 함께 읽어야 합니다. b_dirty_time은 data dirty와 구별하는 timestamp-only 목록입니다.
stat는 현재 dirty·writeback 양과 누적량 등 서로 다른 counter를, write_bandwidth와 avg_write_bandwidth는 완료량에서 추정한 bandwidth를 유지합니다. dirty_ratelimit과 balanced_dirty_ratelimit은 writer 제어용 rate입니다. bw_time_stamp, dirtied_stamp, written_stamp는 그 추정 구간의 기준이며 단순히 마지막으로 수정한 파일의 시각이 아닙니다.
cgroup writeback 활성 시에는 wb 자체의 수명 참조, memcg·blkcg 연결과 inode 이동용 상태가 추가됩니다. b_attached도 root wb의 일반 dirty 목록과 같지 않습니다. 오래된 구조체에 보이는 dirty_sleep을 위 현재 구조체의 필드라고 덧붙이지 않습니다. request의 완료, inode의 writeback 개수, wb 객체 수명을 각각 나타내는 state가 한 객체 안에 있지만 같은 counter는 아닙니다.
delayed wakeup은 더 빠른 예약을 뒤로 밀지 않습니다
첫 dirty inode가 생기면 wb_wakeup_delayed()가 주기적인 확인을 예약할 수 있습니다. v6.18에서는 dirty_writeback_interval의 centisecond를 millisecond로 바꾸고 jiffies로 변환합니다. 이 함수는 현재 fs/fs-writeback.c에 있습니다.
여기서 queue_delayed_work()를 사용하는 이유는 이미 더 이르게 예약된 작업을 뒤로 미루지 않기 위해서입니다. 반면 wb_wakeup()은 mod_delayed_work(..., 0)으로 대기를 줄여 즉시 실행 가능한 상태로 만듭니다. 즉시 실행 가능하다는 뜻이지 호출자 stack에서 callback을 바로 실행한다는 뜻은 아닙니다.
두 경로 모두 work_lock과 등록 상태 검사를 사용합니다. dirty가 생기는 CPU, 명시적 sync 요청을 넣는 CPU, unregister하는 CPU가 함께 존재할 수 있으므로 단순한 timer 설정 문제가 아닙니다. v6.18 fs/fs-writeback.c
아래는 두 helper의 전체입니다. 예전 source의 mm/backing-dev.c 위치와 현재 위치를 혼동하지 않도록, 여기서는 둘 다 위의 v6.18 fs/fs-writeback.c를 기준으로 읽습니다. callback의 재예약이지 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_wakeup_delayed(struct bdi_writeback *wb)
{
unsigned long timeout;
timeout = msecs_to_jiffies(dirty_writeback_interval * 10);
spin_lock_irq(&wb->work_lock);
if (test_bit(WB_registered, &wb->state))
queue_delayed_work(bdi_wq, &wb->dwork, timeout);
spin_unlock_irq(&wb->work_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 wb_wakeup(struct bdi_writeback *wb)
{
spin_lock_irq(&wb->work_lock);
if (test_bit(WB_registered, &wb->state))
mod_delayed_work(bdi_wq, &wb->dwork, 0);
spin_unlock_irq(&wb->work_lock);
}
shutdown은 반대편에서 같은 등록 상태를 내립니다. v6.18 wb_shutdown() 전체는 work_lock 아래에서 WB_registered를 test-and-clear하고, 이미 내려간 객체면 반환합니다. 그 뒤 wb 목록 연결을 정리하고 dwork를 즉시 실행 가능하게 만든 다음 work가 끝날 때까지 flush합니다. 마지막에는 bandwidth 갱신 work도 flush합니다. 이 과정이 끝나기 전에 객체를 해제해도 된다는 뜻이 아닙니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete scope from Linux v6.18 mm/backing-dev.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/backing-dev.c
* Kernel excerpt; not a standalone program.
* The book exact source tag is not asserted.
*/
static void wb_shutdown(struct bdi_writeback *wb)
{
/* Make sure nobody queues further work */
spin_lock_irq(&wb->work_lock);
if (!test_and_clear_bit(WB_registered, &wb->state)) {
spin_unlock_irq(&wb->work_lock);
return;
}
spin_unlock_irq(&wb->work_lock);
cgwb_remove_from_bdi_list(wb);
/*
* Drain work list and shutdown the delayed_work. !WB_registered
* tells wb_workfn() that @wb is dying and its work_list needs to
* be drained no matter what.
*/
mod_delayed_work(bdi_wq, &wb->dwork, 0);
flush_delayed_work(&wb->dwork);
WARN_ON(!list_empty(&wb->work_list));
flush_delayed_work(&wb->bw_dwork);
}
등록 bit가 내려간 상태에서도 worker가 기존 pending 요청을 drain하도록 하는 이유가 여기 있습니다. 새 요청을 막는 검사와 이미 받은 요청의 completion을 정리하는 일은 함께 필요합니다.
work 객체는 목표와 완료 통지를 담습니다
wb_writeback_work는 writeback_control 전체의 복사본이 아닙니다. nr_pages, sync_mode, cyclic·background·kupdate·sync 조건, 이유인 reason을 담고, 특정 filesystem으로 제한할 때 sb를 지정합니다. 실제 inode를 처리할 때 더 구체적인 writeback_control을 만듭니다.
추가로 list는 pending work 연결, done은 완료 통지, auto_free는 work 자체의 해제 정책입니다. stack에 있는 work를 worker가 임의로 free하면 안 되고, heap에 할당해 비동기 전달한 work를 영원히 남겨서도 안 됩니다. 목표를 담는 필드와 수명을 담는 필드가 같은 구조 안에 있는 이유입니다.
다음은 v6.18 struct wb_writeback_work 전체입니다. nr_pages는 work 전체의 잔여 목표이며 inode마다 만드는 wbc.nr_to_write와 별도로 차감합니다. sb == NULL이면 특정 superblock 하나로 제한하지 않습니다.
struct_wb_writeback_work_v6_18.h
// 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
*/
struct wb_writeback_work {
long nr_pages;
struct super_block *sb;
enum writeback_sync_modes sync_mode;
unsigned int tagged_writepages:1;
unsigned int for_kupdate:1;
unsigned int range_cyclic:1;
unsigned int for_background:1;
unsigned int for_sync:1; /* sync(2) WB_SYNC_ALL writeback */
unsigned int auto_free:1; /* free on completion */
enum wb_reason reason; /* why was writeback initiated? */
struct list_head list; /* pending work list */
struct wb_completion *done; /* set if the caller waits */
};
wb_queue_work()는 완료 counter를 먼저 늘리고 등록 상태 아래에서 work를 목록 끝에 연결한 뒤 worker를 깨웁니다. 이미 unregister된 객체라면 그냥 버리는 것이 아니라 finish_writeback_work()를 거쳐 해제·완료 accounting을 정리합니다. 기다리는 쪽이 영원히 남은 counter를 기다려서는 안 되기 때문입니다.
// 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_queue_work(struct bdi_writeback *wb,
struct wb_writeback_work *work)
{
trace_writeback_queue(wb, work);
if (work->done)
atomic_inc(&work->done->cnt);
spin_lock_irq(&wb->work_lock);
if (test_bit(WB_registered, &wb->state)) {
list_add_tail(&work->list, &wb->work_list);
mod_delayed_work(bdi_wq, &wb->dwork, 0);
} else
finish_writeback_work(work);
spin_unlock_irq(&wb->work_lock);
}
completion이 끝나도 별도의 I/O wait가 필요할 수 있습니다
finish_writeback_work()는 필요한 경우 work를 해제하고 done의 count를 줄입니다. count가 0이 되면 waiter들을 깨웁니다. counter를 내린 뒤에는 done의 수명이 끝날 수 있으므로 더 접근해서는 안 된다는 제약도 있습니다.
이 완료가 뜻하는 것은 전달한 writeback work의 처리가 끝났다는 것입니다. 모든 device I/O의 종료와 무조건 같은 시점은 아닙니다. filesystem sync 경로는 진행 중인 inode의 writeback을 별도로 기다려 자신의 계약을 완성합니다.
아래는 v6.18 finish_writeback_work()와 wb_wait_for_completion() 전체입니다. completion은 초기 count 1에서 시작합니다. 발행할 각 work가 count를 더하고, 발행을 끝낸 caller가 초기 count를 내려야 빠른 worker가 나머지 요청 발행 전에 전체 완료라고 판단하지 않게 됩니다. worker는 work를 먼저 free할 수 있으므로 done을 미리 저장하며, count를 내린 뒤에는 completion 자체를 다시 읽지 않습니다.
// 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 finish_writeback_work(struct wb_writeback_work *work)
{
struct wb_completion *done = work->done;
if (work->auto_free)
kfree(work);
if (done) {
wait_queue_head_t *waitq = done->waitq;
/* @done can't be accessed after the following dec */
if (atomic_dec_and_test(&done->cnt))
wake_up_all(waitq);
}
}
wb_wait_for_completion_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_wait_for_completion(struct wb_completion *done)
{
atomic_dec(&done->cnt); /* put down the initial count */
wait_event(*done->waitq, !atomic_read(&done->cnt));
}
sync_inodes_sb()는 superblock 범위를 지정한 integrity work와 completion을 사용하고, 이어 wait_sb_inodes()로 이미 제출된 작업도 기다립니다. 그 전에 시작되어 dirty 목록에서는 사라진 inode의 I/O도 sync 대상에 들어갈 수 있기 때문입니다. s_sync_lock은 여러 sync caller가 이 wait 목록을 불필요하게 경쟁하며 순회하지 않도록 조율합니다.
하나의 BDI가 여러 writeback 객체를 가질 수 있습니다
writeback_inodes_sb() 계열은 동기 완료를 요구하지 않는 work를 준비할 수 있습니다. cgroup writeback을 지원하는 경우 bdi_split_work_to_wbs()는 이 목표를 관련 writeback 객체들에 나누어 전달합니다. 이때 page 수는 관련 bandwidth 등을 고려해 분배할 수 있으며 단순히 모든 객체에 같은 전체 수를 복제하는 것이 아닙니다.
이 과정의 allocation 실패도 작업 유실로 끝내지 않습니다. 필요한 문맥에서 비동기 work 할당을 시도하고 실패하면 stack work를 이용해 완료를 기다리며 진행하는 fallback이 있습니다. 이는 원래 비동기 요청이 자원 부족 상황에서 일시적으로 동기적인 비용을 가질 수 있음을 보여 줍니다.
따라서 API를 읽을 때에는 세 단계를 나누어야 합니다. 요청을 pending list에 넣었는지, worker가 inode의 I/O를 제출했는지, 요청한 동기화 수준의 완료를 실제로 기다렸는지입니다. 같은 workqueue를 거친다는 사실만으로 이 세 단계의 계약이 같아지지는 않습니다.
구체적인 분배·allocation 실패와 두 종류의 wait는 superblock sync의 요청 수명에서 전체 함수와 함께 이어서 살펴봅니다. 위 kernel code는 각 source의 GPL 조건을 따르는 전체 함수·선언이며, 주변 kernel 선언이 필요한 code이므로 독립 실행 프로그램은 아닙니다. v6.18 GPL-2.0
참고 자료
- Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 10, File System and Background Writeback.
- Linux v6.18 mm/backing-dev.c: writeback workqueue와 객체 초기화.
- Linux v6.18 fs/fs-writeback.c: wakeup, work queueing·completion, superblock 요청 분배와 wait.