# Block-device cache는 파일의 page cache와 무엇이 다를까요?

`sb_bread()`가 돌려준 buffer는 어디에 저장되어 있을까요? filesystem의 파일처럼 inode mapping을 사용하지만, 그 inode가 사용자 파일의 inode인 것은 아닙니다. device 전체의 byte offset을 다루는 cache와 파일 내부 offset을 다루는 cache를 구별해야 lookup index도 이해할 수 있습니다.

이 글은 Linux v6.18에서 device를 열어 얻는 mapping과 `bdev_getblk()`의 cache miss를 따라갑니다. buffer가 있다는 사실, buffer 참조를 얻었다는 사실, data가 uptodate라는 사실은 모두 다릅니다.

## Device inode와 열린 file을 연결합니다

partition도 독립된 `block_device`와 mapping을 가질 수 있습니다. 현재 `bdev_alloc()`은 내부 bdev pseudo-filesystem의 `blockdev_superblock`에 inode를 만들고 `inode->i_data`를 `bd_mapping`에 연결합니다. inode allocation 실패는 NULL이며, per-CPU 통계 allocation이 실패하면 앞서 만든 inode 참조를 놓고 NULL로 돌아갑니다. device의 raw data와 파일 cache가 별도 index 공간인 이유를 보여 주는 초기화입니다.

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

`bdev_add()`는 device 번호와 내부 inode의 `i_rdev`, `i_ino`를 연결하고 inode hash에 등록합니다. stable writes가 필요한 device라면 mapping에도 그 요구를 표시합니다. 다음은 [v6.18 `block/bdev.c`](https://github.com/torvalds/linux/blob/v6.18/block/bdev.c)의 전체 함수입니다.

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

device node를 여는 `blkdev_open()`은 권한을 검사하고 device 객체를 얻습니다. 객체가 없으면 `-ENXIO`입니다. 그 뒤 `bdev_open()`이 실패하면 앞에서 얻은 device 참조를 놓습니다. `/dev` node inode의 mapping이 최종 data cache라고 가정하면 이 연결을 놓칩니다. 아래는 [v6.18 `block/fops.c`](https://github.com/torvalds/linux/blob/v6.18/block/fops.c)의 전체 정의입니다.

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

`bdev_open()`은 holder가 있으면 exclusive claim을 준비하고 `disk->open_mutex` 아래에서 device 수명과 open 가능 여부, whole device 또는 partition open 결과를 확인합니다. 실패 시 module 참조, claim, mutex, event blocking을 되돌립니다. 성공하면 열린 file의 `f_mapping`을 `bdev->bd_mapping`으로 연결하고 현재 writeback 오류의 기준값도 저장합니다. 다음은 [v6.18 전체 정의](https://github.com/torvalds/linux/blob/v6.18/block/bdev.c#L926-L1002)입니다.

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

## Buffers 통계는 buffer_head 개수가 아닙니다

`nr_blockdev_pages()`는 `blockdev_superblock`의 inode 목록을 잠근 뒤 각 mapping의 `nrpages`를 합합니다. block-device cache가 가진 base-page 수를 합하는 경로이지 buffer_head slab 객체를 세거나 일반 파일 cache를 모두 합하는 함수가 아닙니다. `/proc/meminfo`의 `Buffers` 해석에서도 이 차이를 기억해야 합니다.

이 연결은 [v6.18 `si_meminfo()`](https://github.com/torvalds/linux/blob/v6.18/mm/show_mem.c)의 `bufferram = nr_blockdev_pages()`와 [meminfo 출력](https://github.com/torvalds/linux/blob/v6.18/fs/proc/meminfo.c)에서 확인할 수 있습니다. meminfo는 이 값을 page 단위에서 kB 표시로 변환합니다.

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

base page 하나에 여러 buffer가 있으면 buffer 개수와 page 수가 달라집니다. page 수에 page 크기를 곱한 data memory와 buffer 구조체 자체의 slab 사용량도 다릅니다. 여기서는 실제 cache를 비우거나 device를 변경하는 실험은 수행하지 않습니다.

## Caller 참조와 lock은 다릅니다

`bdev_getblk()`는 GFP mask가 sleep을 허용하는지에 따라 lookup helper를 고릅니다. 이미 찾았으면 반환하고 없으면 allocation 경로로 갑니다. 반환된 buffer에는 caller 참조가 있지만 lock이나 uptodate 보장은 없습니다. 사용을 마치면 `brelse()`로 참조를 놓습니다. 다음은 [v6.18 `fs/buffer.c`](https://github.com/torvalds/linux/blob/v6.18/fs/buffer.c)의 전체 정의입니다.

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

예전 `__getblk_gfp()`는 아래 [v6.0 정의](https://github.com/torvalds/linux/blob/v6.0/fs/buffer.c)와 비교할 수 있습니다. 처음부터 `__find_get_block()`을 부르고 `might_sleep()` 뒤 cache miss에서 느린 경로를 사용합니다. 현재의 atomic/non-atomic lookup 선택을 예전 코드에도 있었던 분기로 설명하지 않습니다.

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

## LRU와 folio 검색은 참조도 다릅니다

`find_get_block_common()`은 먼저 CPU별 작은 buffer LRU를 찾습니다. hit이면 접근 상태를 갱신하고, miss이면 block-device page cache에서 찾아 LRU에 넣습니다. LRU 자체의 참조와 lookup caller 참조는 별개입니다. 같은 buffer가 여러 CPU의 LRU에 있을 수도 있습니다.

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

`lookup_bh_lru()`는 device, block 번호, buffer 크기가 모두 맞는지 검사합니다. hit을 앞쪽으로 옮기고 `get_bh()`로 caller 참조를 추가합니다. 현재 isolated CPU는 이 LRU를 건너뜁니다. local interrupt 또는 preemption 보호는 CPU별 작은 배열을 지키는 것이며 device I/O 완료를 기다리는 lock이 아닙니다.

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

느린 lookup은 device mapping의 block 크기로 byte 위치와 folio index를 계산합니다. blocking caller는 folio lock을, atomic caller는 `i_private_lock`을 사용해 buffer 목록의 변경과 직렬화합니다. `BH_Migrate`도 acquire로 검사합니다. 원하는 mapped buffer를 찾으면 buffer 참조를 추가하고, 검색용 folio 참조와 lock은 반환 전에 정리합니다.

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

## Cache miss는 배치를 준비한 뒤 다시 찾습니다

`__getblk_slow()`는 요청 크기가 device logical block 크기에 정렬되었는지 검사합니다. 잘못되면 경고하고 `NULL`입니다. 올바르면 `grow_buffers()`와 lookup을 반복합니다. allocation helper의 true는 buffer를 이미 반환했다는 뜻이 아니라 lookup을 재시도할 수 있다는 뜻입니다.

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

`grow_buffers()`는 `block * size`의 overflow와 최대 file 위치를 검사하고 folio index를 계산합니다. 여기서 block의 단위는 `size`입니다. 이 값을 BIO의 512-byte sector 번호와 혼동하면 안 됩니다.

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

`grow_dev_folio()`는 `FGP_LOCK | FGP_ACCESSED | FGP_CREAT`로 folio를 얻습니다. 기존 buffer 크기가 같으면 초기화해 재사용하고, 다르면 `try_to_free_buffers()`로 기존 목록을 없앨 수 있어야 합니다. 실패 시 `end_block = ~0ULL`을 두는 분기는 상위 loop의 재시도를 허용하는 것이지 잘못된 block의 성공 반환이 아닙니다.

새 목록을 붙일 때에는 `i_private_lock`도 필요합니다. atomic lookup이 folio lock을 사용하지 않기 때문입니다. 끝에서는 이 경로의 folio lock과 참조를 모두 정리합니다.

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

`folio_init_buffers()`는 아직 mapped가 아닌 buffer의 callback과 private 상태를 초기화하고 device와 block 번호를 연결합니다. folio가 uptodate이면 buffer에도 반영합니다. device 끝 밖에는 mapped bit를 주지 않으며 반환한 끝 block으로 caller가 요청을 검사합니다. 이 초기화 자체는 read I/O가 아닙니다.

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

## Data와 buffer_head는 따로 할당합니다

folio memory는 page allocator와 page cache 경로가 마련합니다. `folio_alloc_buffers()`는 그 부분 범위를 나타내는 buffer_head들을 따로 할당합니다. offset을 뒤에서부터 줄이며 목록을 만들고 각 buffer를 folio와 연결합니다. 중간에 실패하면 지금까지 만든 buffer를 모두 free하고 active memcg 상태를 복원합니다.

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

buffer_head 자체는 `alloc_buffer_head()`의 `kmem_cache_zalloc()`으로 마련합니다. association list와 completion spinlock, CPU별 accounting도 초기화합니다. `sb_bread()`가 고른 MOVABLE 성격을 slab 객체 자체의 migration 보장으로 해석하지 않는 이유입니다.

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

device mapping과 buffer가 있어도 data read의 제출·완료와 uptodate 검사는 별도의 책임입니다. 반대로 raw device mapping의 내용을 바꾼다고 mount된 filesystem의 파일 cache가 자동으로 같은 내용을 갖는 것도 아닙니다. buffer lookup을 이해하는 데 필요한 것은 하나의 거대한 cache라는 가정이 아니라, 각 mapping의 index와 참조가 무엇을 뜻하는지 구별하는 일입니다.

## 참고 자료

- Lorenzo Stoakes, *The Linux Memory Manager*, Early Access 2025, Chapter 9의 block-device cache와 buffer API 설명.
- Linux v6.18: [block/bdev.c](https://github.com/torvalds/linux/blob/v6.18/block/bdev.c), [block/fops.c](https://github.com/torvalds/linux/blob/v6.18/block/fops.c), [fs/buffer.c](https://github.com/torvalds/linux/blob/v6.18/fs/buffer.c).
- Linux v6.0: [fs/buffer.c](https://github.com/torvalds/linux/blob/v6.0/fs/buffer.c)의 `__getblk_gfp()`.
