# Flusher의 oldest-first는 목록 방향과 superblock 묶음을 함께 봅니다

list의 head에 추가했는데 함수 이름에는 tail이 들어갑니다. 가장 오래된 inode부터 처리한다면서 다른 filesystem의 더 새로운 inode가 먼저 나올 수도 있습니다. writeback 목록을 읽다가 생기는 이 두 의문은 consumer의 방향과 superblock별 그룹화를 구별하면 풀립니다.

여기서 다루는 원소는 BIO나 folio가 아니라 inode의 `i_io_list`입니다. Linux v6.18의 `wb->list_lock` 아래에서 이동하며, inode의 상태를 바꿀 때에는 해당 `i_lock`도 사용합니다. 아래 code는 [v6.18 `fs/fs-writeback.c`](https://github.com/torvalds/linux/blob/v6.18/fs/fs-writeback.c)의 전체 함수이며 GPL-2.0-only kernel code입니다.

## b_more_io를 먼저 옮겨도 나중에 넣은 항목이 앞에 보입니다

`queue_io()`는 `b_more_io`를 `b_io`의 head 쪽으로 splice하고 원래 목록을 비웁니다. 이어서 `b_dirty`에서 cutoff 이전 inode를 골라 `b_io`의 head 쪽에 붙입니다. consumer가 tail부터 꺼내므로, 처음에 옮겨 둔 추가 처리 대상이 뒤에 남아 먼저 선택됩니다. 여기서 먼저라는 말은 모든 device의 I/O 완료 순서를 보장한다는 뜻은 아닙니다.

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

실제 caller들은 `b_io`가 비었을 때 새 queueing을 합니다. 기존 dispatch batch를 매번 해체해 새 dirty inode와 섞는 방식이 아닙니다. timestamp-only 목록인 `b_dirty_time`도 처리하지만 기준은 별도입니다. `for_sync`이면 전달받은 cutoff를 사용하고, 아니면 현재 jiffies에서 `dirtytime_expire_interval * HZ`를 뺀 기준으로 만료된 inode를 고릅니다.

## 가장 오래된 inode부터 고른 뒤 같은 filesystem을 묶습니다

`move_expired_inodes()`는 delaying queue의 tail에서 inode를 얻습니다. 그 inode가 cutoff 뒤에 dirty가 되었다면 종료합니다. 목록이 시간순으로 유지되므로 그 앞에는 더 새로운 inode가 있다는 판단입니다. 선택한 inode를 임시 목록의 head로 옮기면서 `I_SYNC_QUEUED`를 설정합니다.

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

tail부터 꺼내 head에 넣는 두 방향이 결합되므로, 임시 목록에서도 같은 inode들의 상대 순서는 보존됩니다. superblock이 하나뿐이면 그 목록을 그대로 dispatch queue로 splice합니다. 여러 superblock을 발견한 경우에는 임시 목록의 tail inode와 같은 superblock의 원소들을 역방향으로 찾아 dispatch queue의 head에 묶어 넣습니다. 이 두 번째 이동에서는 이미 `I_SYNC_QUEUED`로 writeback 목록 관리가 이 경로의 통제 아래 있으므로 매번 `i_lock`을 다시 얻지 않습니다. 바깥 `wb->list_lock`까지 없어지는 뜻은 아닙니다.

함수의 반환형은 `int`이며 이동한 inode 수를 돌려줍니다. caller가 그 값의 0 여부만 사용한다고 해서 함수 자체가 bool을 반환한다고 설명하지 않습니다. block-device 전용 superblock은 일반 filesystem 사이의 그룹화 필요성을 판단하는 검사에서 특별 취급됩니다.

## 그룹화는 전역 시간순과 다릅니다

아래 예시에서 X와 Y는 서로 다른 superblock입니다. X4가 가장 최근이고 Y1이 가장 오래되었으며 모두 cutoff 이전이라고 가정합니다. 기존 `b_more_io`에는 R이 있고 `b_io`는 비어 있습니다.

![두 superblock의 inode를 묶은 뒤 tail부터 처리하는 순서](assets/inode_queue_order.png)

그림은 네 inode의 그룹화와 R의 우선 재선택을 독립적인 예로 나타냅니다. dirty-time 목록, concurrent redirty, freeing inode, lock 실패와 I/O 오류는 생략했습니다.

Y1이 가장 오래되므로 Y 묶음이 먼저 처리할 쪽에 자리 잡고, 그 안에서는 Y1 다음 Y3입니다. 그다음 X2, X4로 이어집니다. 따라서 전역 시간순 Y1, X2, Y3, X4와는 다릅니다. 같은 superblock 안의 나이 순서를 보존하면서 unmount 보호를 확보한 filesystem 묶음마다 처리한다는 정책입니다. R은 그 앞에서 재선택되며 다음 periodic timer까지 무조건 미루지 않습니다.

## cutoff 비교는 jiffies wrap도 고려합니다

`inode_dirtied_after()`의 핵심은 `time_after(inode->dirtied_when, t)`입니다. 일반 정수의 대소 비교로 바꾸어서는 안 됩니다. 32-bit 경로에는 오래 갱신되지 않은 시각이 wrap 때문에 미래처럼 보이는 경우를 완화하려고 현재 jiffies와의 비교도 들어 있습니다.

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

이 cutoff는 inode의 dirty 시각을 대상으로 합니다. folio마다 모든 store의 시각을 측정하는 snapshot이 아닙니다. 같은 inode가 저장 도중 다시 dirty가 되었을 때 어떤 목록으로 돌려보낼지는 [inode 재등록 조건](08-inode-writeback-requeue.md)에서 별도로 결정합니다.

## 목록 상태와 bandwidth 합도 함께 바뀝니다

`wb_io_lists_populated()`는 이미 dirty-I/O 상태이면 false를 돌려줍니다. 처음 그 상태가 되면 `WB_has_dirty_io`를 세우고 BDI의 active bandwidth 합에 이 wb의 평균 bandwidth를 더합니다. queue helper는 반환값을 무시하지만, 첫 dirty를 만든 경로는 이 전환을 wakeup 판단에도 사용합니다.

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

반대로 `wb_io_lists_depopulated()`는 `b_dirty`, `b_io`, `b_more_io`가 모두 비었을 때 bit와 합을 정리합니다. timestamp-only 또는 cgroup의 attached 목록까지 모두 비었는지를 검사하는 함수는 아닙니다. `state`의 bit 하나가 모든 folio의 dirty 상태를 대신하지 않는 이유입니다.

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

목록이 비었다는 관측, worker가 당장 수행할 work의 유무, 이미 제출한 folio의 I/O 완료를 끝까지 구별해야 합니다. 그 구분을 유지하면 linked list의 물리적 head·tail과 정책상 다음 차례라는 표현도 혼동하지 않게 됩니다.

## 참고 자료

- Lorenzo Stoakes, *The Linux Memory Manager*, Early Access 2025, Chapter 10, inode queueing과 Flusher Thread Operation.
- [Linux v6.18 `fs/fs-writeback.c`](https://github.com/torvalds/linux/blob/v6.18/fs/fs-writeback.c): `queue_io()`, `move_expired_inodes()`, cutoff와 목록 통계 helper의 전체 정의.
- [Linux v6.18 GPL-2.0](https://github.com/torvalds/linux/blob/v6.18/LICENSES/preferred/GPL-2.0): code license text. 주변 kernel 선언을 전제로 하며 독립 실행 예제가 아닙니다.
