# Writeback 요청은 worker에 어떻게 전달될까요?

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](https://github.com/torvalds/linux/blob/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](https://www.kernel.org/doc/html/v6.18/core-api/workqueue.html)

<!-- include-code:code/default_bdi_init_v6_18.c -->

각 `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를 받을 수 있는 객체와 같게 보아서는 안 됩니다.

<!-- include-code:code/wb_init_v6_18.c -->

## wb의 inode 목록과 요청 목록은 다릅니다

아래는 [v6.18 `struct bdi_writeback` 전체](https://github.com/torvalds/linux/blob/v6.18/include/linux/backing-dev-defs.h)입니다. `b_dirty`, `b_io`, `b_more_io`, `b_dirty_time`은 inode의 목록이며 `list_lock`으로 보호합니다. `work_list`는 worker에게 전달할 요청의 목록이고 `work_lock`은 그 목록과 delayed-work 예약을 보호합니다. 두 목록의 원소와 lock을 바꿔 읽으면 수명과 lock ordering도 잘못 따라가게 됩니다.

<!-- include-code:code/struct_bdi_writeback_v6_18.h -->

`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](https://github.com/torvalds/linux/blob/v6.18/fs/fs-writeback.c)

아래는 두 helper의 전체입니다. 예전 source의 `mm/backing-dev.c` 위치와 현재 위치를 혼동하지 않도록, 여기서는 둘 다 위의 v6.18 `fs/fs-writeback.c`를 기준으로 읽습니다. callback의 재예약이지 C 함수의 재귀 호출이 아닙니다.

<!-- include-code:code/wb_wakeup_delayed_v6_18.c -->

<!-- include-code:code/wb_wakeup_v6_18.c -->

shutdown은 반대편에서 같은 등록 상태를 내립니다. v6.18 `wb_shutdown()` 전체는 `work_lock` 아래에서 `WB_registered`를 test-and-clear하고, 이미 내려간 객체면 반환합니다. 그 뒤 wb 목록 연결을 정리하고 `dwork`를 즉시 실행 가능하게 만든 다음 work가 끝날 때까지 flush합니다. 마지막에는 bandwidth 갱신 work도 flush합니다. 이 과정이 끝나기 전에 객체를 해제해도 된다는 뜻이 아닙니다.

<!-- include-code:code/wb_shutdown_v6_18.c -->

등록 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 하나로 제한하지 않습니다.

<!-- include-code:code/struct_wb_writeback_work_v6_18.h -->

`wb_queue_work()`는 완료 counter를 먼저 늘리고 등록 상태 아래에서 work를 목록 끝에 연결한 뒤 worker를 깨웁니다. 이미 unregister된 객체라면 그냥 버리는 것이 아니라 `finish_writeback_work()`를 거쳐 해제·완료 accounting을 정리합니다. 기다리는 쪽이 영원히 남은 counter를 기다려서는 안 되기 때문입니다.

<!-- include-code:code/wb_queue_work_v6_18.c -->

## 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 자체를 다시 읽지 않습니다.

<!-- include-code:code/finish_writeback_work_v6_18.c -->

<!-- include-code:code/wb_wait_for_completion_v6_18.c -->

`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의 요청 수명](16-superblock-sync-work.md)에서 전체 함수와 함께 이어서 살펴봅니다. 위 kernel code는 각 source의 GPL 조건을 따르는 전체 함수·선언이며, 주변 kernel 선언이 필요한 code이므로 독립 실행 프로그램은 아닙니다. [v6.18 GPL-2.0](https://github.com/torvalds/linux/blob/v6.18/LICENSES/preferred/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](https://github.com/torvalds/linux/blob/v6.18/mm/backing-dev.c): writeback workqueue와 객체 초기화.
- [Linux v6.18 fs/fs-writeback.c](https://github.com/torvalds/linux/blob/v6.18/fs/fs-writeback.c): wakeup, work queueing·completion, superblock 요청 분배와 wait.
