# Superblock sync는 work 완료와 I/O 완료를 따로 기다립니다

같은 storage를 사용하는 두 filesystem을 생각해 보겠습니다. 한쪽에 `syncfs()`를 호출했다고 다른 filesystem의 모든 dirty inode까지 같은 요청으로 저장할 필요는 없습니다. 반대로 해당 filesystem의 inode가 dirty 목록에서 사라졌다는 이유만으로 sync를 끝내서도 안 됩니다. 이미 제출되어 device에서 진행 중인 writeback이 남을 수 있기 때문입니다.

Linux v6.18의 superblock writeback은 범위가 담긴 work를 wb들에 분배하고, worker의 요청 처리가 끝나는 시점과 실제 folio writeback이 끝나는 시점을 나누어 기다립니다. 이 글의 code는 [공식 v6.18 `fs/fs-writeback.c`](https://github.com/torvalds/linux/blob/v6.18/fs/fs-writeback.c)의 전체 함수입니다. kernel 선언이 필요한 GPL-2.0-only code이며 독립 실행 예제는 아닙니다.

## WB_SYNC_NONE도 caller가 전혀 기다리지 않는 뜻은 아닙니다

`writeback_inodes_sb()`는 전역의 `get_nr_dirty_pages()`로 목표량을 구하고, `writeback_inodes_sb_nr()`를 거쳐 `__writeback_inodes_sb_nr()`로 내려갑니다. inode 개수를 읽는 helper가 아닙니다. 이 수는 특정 superblock의 정확한 dirty page 수를 잠가서 센 값도 아니며, 실제로 몇 page를 제출할지 보장하지 않습니다.

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

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

내부 함수는 `WB_SYNC_NONE`, `tagged_writepages = 1`, 지정한 `sb`와 목표량으로 work를 만듭니다. dirty I/O가 없는 BDI와 noop BDI는 일찍 반환합니다. `s_umount`는 caller가 이미 보호해야 하며 WARN은 그 계약의 위반을 드러내는 검사입니다. 이 함수가 unmount와의 race를 복구하거나 여기서 lock을 새로 잡아 주는 것은 아닙니다.

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

끝의 `wb_wait_for_completion()`은 의도적입니다. `WB_SYNC_NONE`이어도 stack에 있는 completion을 worker가 나중에 건드리도록 남기고 반환할 수는 없습니다. 요청 처리의 완료는 기다리지만, 이 API는 제출한 모든 BIO의 완료까지 기다리겠다는 data-integrity 계약을 제공하지 않습니다. asynchronous라는 설명은 이 두 wait를 구별한 뒤 읽어야 합니다.

## Sync는 이미 clean이 된 inode의 I/O도 놓치지 않습니다

`sync_inodes_sb()`의 work는 `WB_SYNC_ALL`, `nr_pages = LONG_MAX`, `for_sync = 1`, non-cyclic 전체 범위를 사용합니다. `for_sync`는 단순히 sync mode를 한 번 더 적은 flag가 아닙니다. inode별 I/O 대기의 일부를 바깥 superblock 경로에서 담당한다는 제어 흐름과 연결됩니다.

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

이 함수는 `!bdi_has_dirty_io()`만 보고 반환하지 않습니다. dirty bit를 지우고 이미 I/O 중인 inode도 기다려야 하고, 일반 dirty-I/O 검사에서 빠지는 timestamp-only inode도 sync해야 하기 때문입니다. noop BDI인 경우만 여기서 제외합니다.

`bdi_down_write_wb_switch_rwsem()`부터 대응하는 unlock까지는 inode가 wb를 바꾸는 경로와의 조율입니다. 그 안에서 work를 분배하고 completion을 기다린 뒤, `wait_sb_inodes()`를 별도로 호출합니다. 이 순서 때문에 work가 끝났다는 사실을 곧바로 device가 모든 내용을 받았다는 사실로 바꾸면 안 됩니다.

## Completion의 초기 count는 요청 발행 구간을 보호합니다

다음은 [v6.18 `backing-dev-defs.h`](https://github.com/torvalds/linux/blob/v6.18/include/linux/backing-dev-defs.h)의 completion 선언과 초기화 macro 전체입니다. 이 header의 라이선스 표기는 GPL-2.0입니다.

```c
struct wb_completion {
	atomic_t		cnt;
	wait_queue_head_t	*waitq;
};

#define __WB_COMPLETION_INIT(_waitq)	\
	(struct wb_completion){ .cnt = ATOMIC_INIT(1), .waitq = (_waitq) }

#define WB_COMPLETION_INIT(bdi)		__WB_COMPLETION_INIT(&(bdi)->wb_waitq)

#define DEFINE_WB_COMPLETION(cmpl, bdi)	\
	struct wb_completion cmpl = WB_COMPLETION_INIT(bdi)
```

초기 count 1은 caller가 아직 work들을 발행 중이라는 표시입니다. 각 `wb_queue_work()`가 count를 늘리고, worker의 `finish_writeback_work()`가 줄입니다. 발행을 마친 caller는 `wb_wait_for_completion()`에서 초기 count를 하나 내려놓고 0을 기다립니다. 요청이 하나도 발행되지 않은 경우에도 이 규칙으로 정상 종료합니다. count와 wait queue의 역할은 [worker에게 요청을 전달하는 과정](06-writeback-workqueue.md)에서 전체 helper로 확인할 수 있습니다.

## 여러 wb에 나눌 때 allocation 실패도 수명 문제입니다

`bdi_split_work_to_wbs()`는 RCU 아래에서 BDI의 wb 목록을 순회합니다. 일반 dirty I/O가 없더라도 `WB_SYNC_ALL`이고 dirty-time 목록이 남으면 대상이 됩니다. `skip_if_busy`가 true이면 이미 writeback 중인 wb를 건너뛸 수 있지만, 모든 sync가 그런 skip을 허용하는 것은 아닙니다.

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

work는 RCU read-side 구간에서 `GFP_ATOMIC`으로 할당합니다. 성공하면 base work를 복사하되 해당 wb의 목표량과 `auto_free = 1`을 설정해 queue합니다. 이 allocation이 실패했다고 요청을 버리지 않습니다. 먼저 `wb_tryget()`으로 shutdown되지 않은 wb의 참조를 얻고, stack fallback work를 queue한 뒤 RCU lock을 놓고 fallback completion을 기다립니다. 실패한 `wb_tryget()`이면 그 wb는 건너뜁니다.

fallback의 동기적이라는 말은 같은 caller가 filesystem writeback을 직접 실행한다는 뜻이 아닙니다. worker에 넘긴 stack 객체가 더는 사용되지 않을 때까지 caller가 기다린다는 뜻입니다. 이후 순회를 재개하려고 wb 참조를 유지하며, 다음 단계 또는 마지막에 그 참조를 놓습니다. RCU를 든 채 잠들거나 stack work를 남기고 반환하지 않는 두 조건이 함께 필요합니다.

목표량을 나누는 `wb_split_bdi_pages()` 전체도 함께 볼 수 있습니다.

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

`LONG_MAX`는 그대로 넘깁니다. finite 목표량은 wb의 `avg_write_bandwidth`와 BDI의 active bandwidth 합의 비율로 올림 분배합니다. 합이 0이거나 이 wb의 bandwidth가 합 이상이면 원래 목표량을 그대로 사용합니다. 각 몫을 올림하므로 모든 몫의 합이 원래 값과 정확히 같다는 conservation 규칙은 없습니다. cgroup writeback을 쓰지 않으면 보통 embedded root wb 하나를 처리하지만, 그렇다고 이 호출이 무의미해지는 것은 아닙니다.

## 마지막 wait는 inode 참조를 얻고 lock을 놓은 뒤 수행합니다

`wait_sb_inodes()`는 `s_sync_lock`으로 다른 sync 순회를 조율하고, `s_inode_wblist_lock` 아래에서 현재 `s_inodes_wb`를 임시 목록으로 splice합니다. 뒤늦게 writeback을 시작하는 inode를 무한히 따라가지 않기 위한 경계입니다. 현재 필드명은 superblock의 `s_inodes_wb`이며 각 inode의 연결 node는 `i_wb_list`입니다.

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

각 inode를 다시 원래 목록으로 옮겨 놓은 뒤 WRITEBACK mark를 확인합니다. mark와 목록이 순간적으로 어긋난 inode는 완료 경로가 제거하도록 두고 넘어갑니다. 실제 대기 대상이면 목록 spinlock을 놓고 `i_lock` 아래에서 freeing·new 상태를 제외한 후 `__iget()`으로 참조를 얻습니다. 그 뒤 RCU read lock까지 놓고 `filemap_fdatawait_keep_errors()`를 호출합니다.

`iput()` 뒤에는 예전 inode pointer를 계속 따라가지 않고 lock과 RCU를 다시 얻어 목록에서 다음 대상을 고릅니다. error를 보존하는 helper를 사용하는 이유도 중요합니다. 시스템 sync가 error 상태를 지워 버려 뒤의 application `fsync()`가 오류를 놓치게 해서는 안 됩니다. wait가 끝났다는 조건, 오류를 관찰하는 주체, metadata와 device flush를 보장하는 최종 callback은 별개의 책임입니다.

## 참고 자료

- Lorenzo Stoakes, *The Linux Memory Manager*, Early Access 2025, Chapter 10, File System and Background Writeback 및 File Writeback.
- [Linux v6.18 `fs/fs-writeback.c`](https://github.com/torvalds/linux/blob/v6.18/fs/fs-writeback.c): superblock work, wb 분배, completion과 inode I/O wait의 전체 구현.
- [Linux v6.18 `backing-dev-defs.h`](https://github.com/torvalds/linux/blob/v6.18/include/linux/backing-dev-defs.h): completion 선언과 초기화.
- [Linux v6.18 GPL-2.0](https://github.com/torvalds/linux/blob/v6.18/LICENSES/preferred/GPL-2.0): 인용한 kernel code의 license text.
