파일의 일부 byte를 고쳤는데 folio 전체가 아직 최신 내용으로 채워지지는 않았을 수 있습니다. 반대로 folio 전체가 메모리에 있어도 그중 일부 block만 dirty일 수 있습니다. 파일을 보관하는 단위와 device에 보낼 범위를 같은 것으로 취급하면 이 두 상황을 표현하기 어렵습니다.
buffer_head는 이런 차이를 block 단위 상태로 나타내는 전통적인 방법입니다. 모든 filesystem이 이 구조를 사용하는 것은 아닙니다. 여기서는 upstream v6.18의 buffer 기반 경로를 따라, 메모리의 어느 범위가 어떤 device block에 대응하며 누가 I/O 완료를 책임지는지 살펴보겠습니다.
주소 세 가지를 구별합니다
b_blocknr는 device에서의 block 번호이고 b_size는 그 block을 다루는 byte 수입니다. b_data는 메모리 쪽 데이터 위치와 관계된 필드입니다. 여기에 BIO의 sector 번호가 더해집니다. 이 셋은 단위부터 다릅니다. 예를 들어 block 번호가 10이고 block 크기가 4096 byte라면 BIO에서 쓰는 512-byte sector 단위 시작 위치는 80입니다.
실제로 v6.18 submit_bh_wbc()는 b_blocknr * (b_size >> 9)로 sector를 설정합니다. 여기의 512 byte는 block layer 주소 단위이지 device의 physical sector가 언제나 512 byte라는 뜻이 아닙니다. filesystem block 크기, logical sector 크기, base page 크기도 별도로 구별해야 합니다. 특정 buffer 경로의 제약을 모든 filesystem의 block 크기 상한으로 일반화해서는 안 됩니다. v6.18 fs/buffer.c
block device의 cache와 보통 파일의 cache도 동일한 index 공간은 아닙니다. 보통 파일은 자신의 inode mapping에서 파일 offset을 기준으로 cached data를 찾습니다. block device mapping은 device의 주소 공간을 다룹니다. /dev의 special inode와 device의 내부 inode를 무조건 같은 객체로 보면 이 구분이 사라집니다. mount된 filesystem 뒤에서 raw device 내용을 임의로 바꾸는 것을 cache coherency가 보장된 파일 수정 방법으로 취급해서는 안 됩니다.
하나의 folio에 여러 상태를 붙입니다
b_this_page는 같은 folio에 붙은 buffer들을 circular list로 연결합니다. b_state에는 uptodate, dirty, locked, mapped, delay, unwritten 등 서로 다른 상태가 들어갑니다. mapped는 실제 block의 대응이 있다는 뜻이고, uptodate는 해당 메모리 내용의 유효성입니다. dirty는 저장해야 할 변경이 남았다는 뜻이므로 어느 하나가 다른 상태를 대신하지 않습니다.
메모리의 연결도 버전을 구별해야 합니다. 예전 설명에서는 struct page *b_page가 중심이지만 v6.18의 제출 경로는 bh->b_folio와 folio 안의 offset으로 BIO에 메모리 범위를 추가합니다. 큰 folio를 다룰 때 buffer 하나의 크기와 folio 전체 크기를 같다고 가정하면 잘못된 범위를 보게 됩니다.
b_bdev는 대상 device, b_end_io는 완료 callback, b_private는 callback 등에 필요한 부가 상태를 연결합니다. b_count는 buffer 자체의 참조 수입니다. folio의 참조 수와 같은 counter가 아닙니다. b_assoc_buffers와 b_assoc_map은 inode와 관련된 buffer 목록 관리에 사용됩니다. 여러 buffer의 완료 결과를 모아 folio 상태로 올릴 때에는 b_uptodate_lock 같은 별도 동기화도 필요합니다.
b_assoc_map은 buffer가 속한 folio의 mapping을 그대로 복제하는 필드가 아닙니다. 다른 mapping과의 association을 나타낼 수 있습니다. 현재 구조에는 b_page와 b_folio가 union으로 들어 있습니다. sector_t라는 C type만으로 b_blocknr의 단위를 512 byte라고 결정할 수도 없습니다. 단위는 그 필드와 연산의 계약으로 읽어야 합니다. 다음은 v6.18 buffer_head.h의 전체 구조체입니다.
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Complete declaration from Linux v6.18 include/linux/buffer_head.h.
* Source: https://github.com/torvalds/linux/blob/v6.18/include/linux/buffer_head.h
* Kernel declaration; not a standalone header.
*/
struct buffer_head {
unsigned long b_state; /* buffer state bitmap (see above) */
struct buffer_head *b_this_page;/* circular list of page's buffers */
union {
struct page *b_page; /* the page this bh is mapped to */
struct folio *b_folio; /* the folio this bh is mapped to */
};
sector_t b_blocknr; /* start block number */
size_t b_size; /* size of mapping */
char *b_data; /* pointer to data within the page */
struct block_device *b_bdev;
bh_end_io_t *b_end_io; /* I/O completion */
void *b_private; /* reserved for b_end_io */
struct list_head b_assoc_buffers; /* associated with another mapping */
struct address_space *b_assoc_map; /* mapping this buffer is
associated with */
atomic_t b_count; /* users using this buffer_head */
spinlock_t b_uptodate_lock; /* Used by the first bh in a page, to
* serialise IO completion of other
* buffers in the page */
};
상태 bit도 전체를 보면 서로 대체할 수 없다는 점이 더 분명합니다. BH_Delay는 아직 disk block이 할당되지 않은 경우이고 BH_Unwritten은 할당되었지만 기록되지 않은 extent 상태와 관련됩니다. BH_New는 새 mapping, BH_Write_EIO는 write 오류를 추적합니다. BH_Async_Read와 BH_Async_Write는 비동기 완료 집계에, BH_Meta와 BH_Prio는 request 성격에 참여합니다. BH_PrivateStart는 자체 상태 bit가 아니라 다른 사용자가 private bit를 배정할 수 있는 시작 위치입니다.
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Complete declaration from Linux v6.18 include/linux/buffer_head.h.
* Source: https://github.com/torvalds/linux/blob/v6.18/include/linux/buffer_head.h
* Kernel declaration; not a standalone header.
*/
enum bh_state_bits {
BH_Uptodate, /* Contains valid data */
BH_Dirty, /* Is dirty */
BH_Lock, /* Is locked */
BH_Req, /* Has been submitted for I/O */
BH_Mapped, /* Has a disk mapping */
BH_New, /* Disk mapping was newly created by get_block */
BH_Async_Read, /* Is under end_buffer_async_read I/O */
BH_Async_Write, /* Is under end_buffer_async_write I/O */
BH_Delay, /* Buffer is not yet allocated on disk */
BH_Boundary, /* Block is followed by a discontiguity */
BH_Write_EIO, /* I/O error on write */
BH_Unwritten, /* Buffer is allocated on disk but not written */
BH_Quiet, /* Buffer Error Prinks to be quiet */
BH_Meta, /* Buffer contains metadata */
BH_Prio, /* Buffer should be submitted with REQ_PRIO */
BH_Defer_Completion, /* Defer AIO completion to workqueue */
BH_Migrate, /* Buffer is being migrated (norefs) */
BH_PrivateStart,/* not a state bit, but the first bit available
* for private allocation by other entities
*/
};
buffer의 생명주기와 데이터의 생명주기를 구별하는 것이 중요합니다. buffer 참조를 놓았다고 backing memory가 그 자리에서 반드시 해제되는 것은 아니며, folio를 잠갔다고 모든 buffer의 개별 I/O가 완료된 것도 아닙니다. buffer를 private data로 붙이는 경로와 일시적인 I/O 표현으로만 사용하는 경로도 있습니다.
block을 얻는 것과 읽는 것은 다릅니다
sb_getblk() 계열은 해당 block에 대응하는 buffer를 얻는 쪽입니다. 내용이 uptodate라는 보장은 별도로 확인해야 합니다. sb_bread() 계열은 읽어 사용할 수 있는 내용을 요구합니다. 아래는 구현코드가 아닌 호출관계입니다.
sb_bread()
-> __bread_gfp()
-> bdev_getblk()
-> __bread_slow() [when not uptodate]
-> lock_buffer()
-> submit_bh()
-> wait_on_buffer()
__bread_slow()는 lock을 얻은 뒤 uptodate를 다시 검사합니다. 기다리는 동안 다른 reader가 같은 buffer를 채웠을 수 있기 때문입니다. 이미 준비되었다면 unlock하고 돌려줍니다. 아니면 I/O용 참조를 추가하고 end_buffer_read_sync를 지정한 뒤 read를 제출합니다.
wait_on_buffer()는 BH_Lock이 풀리기를 기다리는 것이지 호출자에게 buffer lock을 획득시켜 주는 함수가 아닙니다. 깨어난 뒤 uptodate를 확인해야 성공한 read인지 알 수 있습니다. 실패하면 brelse()로 호출자 참조를 정리하고 NULL을 반환합니다. lock 대기의 종료, I/O의 성공, buffer의 소유권을 같은 사건으로 읽으면 오류 처리를 놓치게 됩니다. v6.18 __bread_slow와 __bread_gfp
할당에 쓰는 GFP 조건도 부수적인 선택이 아닙니다. I/O나 filesystem 재진입을 허용할 수 있는 문맥인지에 따라 호출 경로의 allocation 제약이 달라집니다. 메모리 부족을 해결하려다 자신이 끝내야 할 I/O를 다시 기다리는 의존관계를 만들어서는 안 됩니다.
네 wrapper의 전체 정의는 다음과 같습니다. sb_bread()와 _unmovable()의 차이는 cache data를 둘 memory의 allocation 조건입니다. buffer_head slab 객체 자체가 이동 가능한 객체로 바뀐다는 뜻은 아닙니다. sb_getblk_gfp()는 직접 GFP mask를 넘기지만 __bread_gfp()의 인자는 완전한 allocation mask를 넘기는 interface가 아닙니다.
// SPDX-License-Identifier: GPL-2.0
/*
* Complete definition from Linux v6.18 include/linux/buffer_head.h.
* Source: https://github.com/torvalds/linux/blob/v6.18/include/linux/buffer_head.h
* Kernel excerpt; not a standalone user-space program.
* The book's exact source tag is not asserted.
*/
static inline struct buffer_head *
sb_bread(struct super_block *sb, sector_t block)
{
return __bread_gfp(sb->s_bdev, block, sb->s_blocksize, __GFP_MOVABLE);
}
// SPDX-License-Identifier: GPL-2.0
/*
* Complete definition from Linux v6.18 include/linux/buffer_head.h.
* Source: https://github.com/torvalds/linux/blob/v6.18/include/linux/buffer_head.h
* Kernel excerpt; not a standalone user-space program.
* The book's exact source tag is not asserted.
*/
static inline struct buffer_head *
sb_bread_unmovable(struct super_block *sb, sector_t block)
{
return __bread_gfp(sb->s_bdev, block, sb->s_blocksize, 0);
}
// SPDX-License-Identifier: GPL-2.0
/*
* Complete definition from Linux v6.18 include/linux/buffer_head.h.
* Source: https://github.com/torvalds/linux/blob/v6.18/include/linux/buffer_head.h
* Kernel excerpt; not a standalone user-space program.
* The book's exact source tag is not asserted.
*/
static inline struct buffer_head *sb_getblk(struct super_block *sb,
sector_t block)
{
return __getblk(sb->s_bdev, block, sb->s_blocksize);
}
// SPDX-License-Identifier: GPL-2.0
/*
* Complete definition from Linux v6.18 include/linux/buffer_head.h.
* Source: https://github.com/torvalds/linux/blob/v6.18/include/linux/buffer_head.h
* Kernel excerpt; not a standalone user-space program.
* The book's exact source tag is not asserted.
*/
static inline struct buffer_head *sb_getblk_gfp(struct super_block *sb,
sector_t block, gfp_t gfp)
{
return bdev_getblk(sb->s_bdev, block, sb->s_blocksize, gfp);
}
sb_getblk()가 거치는 현재 __getblk()는 mapping의 mask에서 filesystem 재진입을 제한하고 MOVABLE과 NOFAIL을 더합니다. __bread_gfp()도 mapping 제약과 NOFAIL을 결합합니다. 따라서 원래 인자에 0을 넘기는 것과 allocation 자체에 모든 GFP flag가 0인 것은 다릅니다. NOFAIL이 read I/O의 성공까지 보장하지도 않습니다.
// SPDX-License-Identifier: GPL-2.0
/*
* Complete definition from Linux v6.18 include/linux/buffer_head.h.
* Source: https://github.com/torvalds/linux/blob/v6.18/include/linux/buffer_head.h
* Kernel excerpt; not a standalone user-space program.
* The book's exact source tag is not asserted.
*/
static inline struct buffer_head *__getblk(struct block_device *bdev,
sector_t block, unsigned size)
{
gfp_t gfp;
gfp = mapping_gfp_constraint(bdev->bd_mapping, ~__GFP_FS);
gfp |= __GFP_MOVABLE | __GFP_NOFAIL;
return bdev_getblk(bdev, block, size, gfp);
}
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete definition from Linux v6.18 fs/buffer.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/fs/buffer.c
* Copyright (C) 1991, 1992, 2002 Linus Torvalds
* Kernel excerpt; not a standalone user-space program.
* The book's exact source tag is not asserted.
*/
struct buffer_head *__bread_gfp(struct block_device *bdev, sector_t block,
unsigned size, gfp_t gfp)
{
struct buffer_head *bh;
gfp |= mapping_gfp_constraint(bdev->bd_mapping, ~__GFP_FS);
/*
* Prefer looping in the allocator rather than here, at least that
* code knows what it's doing.
*/
gfp |= __GFP_NOFAIL;
bh = bdev_getblk(bdev, block, size, gfp);
if (likely(bh) && !buffer_uptodate(bh))
bh = __bread_slow(bh);
return bh;
}
아래 느린 read와 완료 helper는 v6.18 fs/buffer.c의 전체 정의입니다. caller가 가진 buffer 참조와 I/O가 추가한 참조를 구별하면서 읽으면 read 실패 때 어떤 참조를 놓는지 확인할 수 있습니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete definition from Linux v6.18 fs/buffer.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/fs/buffer.c
* Copyright (C) 1991, 1992, 2002 Linus Torvalds
* Kernel excerpt; not a standalone user-space program.
* The book's exact source tag is not asserted.
*/
static struct buffer_head *__bread_slow(struct buffer_head *bh)
{
lock_buffer(bh);
if (buffer_uptodate(bh)) {
unlock_buffer(bh);
return bh;
} else {
get_bh(bh);
bh->b_end_io = end_buffer_read_sync;
submit_bh(REQ_OP_READ, bh);
wait_on_buffer(bh);
if (buffer_uptodate(bh))
return bh;
}
brelse(bh);
return NULL;
}
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete definition from Linux v6.18 fs/buffer.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/fs/buffer.c
* Copyright (C) 1991, 1992, 2002 Linus Torvalds
* Kernel excerpt; not a standalone user-space program.
* The book's exact source tag is not asserted.
*/
void end_buffer_read_sync(struct buffer_head *bh, int uptodate)
{
put_bh(bh);
__end_buffer_read_notouch(bh, uptodate);
}
__end_buffer_read_notouch_v6_18.c
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete definition from Linux v6.18 fs/buffer.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/fs/buffer.c
* Copyright (C) 1991, 1992, 2002 Linus Torvalds
* Kernel excerpt; not a standalone user-space program.
* The book's exact source tag is not asserted.
*/
static void __end_buffer_read_notouch(struct buffer_head *bh, int uptodate)
{
if (uptodate) {
set_buffer_uptodate(bh);
} else {
/* This happens, due to failed read-ahead attempts. */
clear_buffer_uptodate(bh);
}
unlock_buffer(bh);
}
cache miss는 folio와 buffer들을 함께 준비합니다
v6.18의 block cache 진입점은 bdev_getblk()입니다. 예전의 __getblk_gfp()와 grow_dev_page() 이름을 현재 호출 stack에 그대로 넣지 않아야 합니다. 먼저 device·block 번호·크기가 일치하는 buffer를 CPU별 작은 LRU에서 찾고, 없으면 block-device page cache를 검색합니다. LRU hit에서도 caller를 위한 buffer 참조를 확보하며, cache miss에만 할당 경로가 필요합니다.
느린 경로인 __getblk_slow()는 요청 크기가 device의 logical block 크기에 정렬되었는지 검사합니다. grow_buffers()는 block 번호와 크기의 곱이 overflow하거나 표현 가능한 file 위치를 넘지 않는지도 확인합니다. 유효한 요청이면 grow_dev_folio()가 locked cache folio를 얻고 그 folio에 필요한 buffer들을 준비합니다.
기존 buffer 크기가 같으면 mapping 정보를 초기화해 재사용할 수 있습니다. 크기가 다르면 기존 buffer를 해제할 수 있는지 확인해야 하며, dirty나 사용 중인 상태 때문에 곧바로 바꾸지 못할 수 있습니다. 새 buffer들은 folio 내 offset을 가리키게 만들고 circular list로 연결한 뒤 i_private_lock 아래에서 붙입니다. lookup이 folio lock만으로 직렬화되지 않는 경로도 고려한 조치입니다.
이 초기화는 device block과 메모리의 대응을 만드는 것이지 내용을 읽는 I/O가 아닙니다. 요청 block이 device 끝을 넘는지도 검사해야 합니다. helper가 재시도 가능한 상태를 반환했다고 이미 원하는 buffer를 찾았다고 단정하지 않고, 상위 loop가 lookup을 다시 수행합니다. v6.18 bdev_getblk와 grow_dev_folio
이 lookup과 allocation의 전체 함수들은 block-device cache에서 buffer를 찾는 과정에서 별도로 연결합니다. 일반 파일 mapping과 device mapping, buffer LRU 참조와 folio 참조를 나누어 보는 단계입니다.
write-end에서 dirty로 만드는 것은 writeback 완료가 아닙니다
buffer 기반 buffered write에서는 사용자 데이터가 복사된 범위와 buffer의 경계를 맞추는 작업이 필요합니다. v6.18의 block_commit_write()가 하는 일은 그중 하나입니다. 이름의 commit을 device에 영구 저장했다는 뜻으로 읽지 않아야 합니다.
함수는 folio에 연결된 buffer들을 돌면서 [from, to)와 겹치는 buffer를 uptodate로 표시하고 dirty로 만듭니다. 겹치지 않는 buffer 가운데 아직 uptodate가 아닌 것이 있으면 partial 상태를 남깁니다. 새 block을 나타내던 BH_New도 정리합니다. 마지막에 유효하지 않은 buffer가 남지 않았을 때에만 folio 전체를 uptodate로 올릴 수 있습니다.
예를 들어 folio 안의 네 buffer 중 두 번째 것만 고쳤다면, 나머지 세 buffer가 이미 유효한지에 따라 folio 전체의 uptodate 여부가 달라집니다. 수정한 buffer만 dirty라는 사실과 folio 전체 내용을 읽을 수 있다는 사실은 별개입니다. 이 과정은 buffered write의 write-end 처리에 속하며, 나중에 제출할 writeback I/O의 완료 callback이 아닙니다. v6.18 block_commit_write와 block_write_end
mark_buffer_dirty()도 단순히 bit 하나만 설정하지 않습니다. buffer가 처음 dirty로 전환될 때 folio와 mapping, inode 쪽의 dirty 추적이 이어져야 합니다. buffer가 dirty인데 상위 검색 구조에서 찾을 수 없다면 flusher가 작업을 발견하지 못하기 때문입니다. 반대로 이미 dirty인 buffer를 다시 수정했다고 매번 최초 dirty와 같은 accounting을 더해서도 안 됩니다.
다음은 현재 write-end helper와 dirty helper 전체입니다. block_commit_write()는 buffer가 없으면 반환하는 조건도 갖습니다. mark_buffer_dirty()의 이미-dirty fast path는 barrier 뒤에 dirty를 다시 검사합니다. 앞선 buffer 수정이 마지막 dirty 검사보다 뒤로 밀려 writeback과의 경합에서 변경을 놓치지 않도록 하는 memory-ordering 조건이며, 단순히 CPU pipeline을 빠르게 하는 최적화가 아닙니다. buffer bit가 처음 바뀌었을 때에도 folio가 이미 dirty였는지에 따라 상위 최초 dirty accounting이 달라집니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete definition from Linux v6.18 fs/buffer.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/fs/buffer.c
* Copyright (C) 1991, 1992, 2002 Linus Torvalds
* Kernel excerpt; not a standalone user-space program.
* The book's exact source tag is not asserted.
*/
void block_commit_write(struct folio *folio, size_t from, size_t to)
{
size_t block_start, block_end;
bool partial = false;
unsigned blocksize;
struct buffer_head *bh, *head;
bh = head = folio_buffers(folio);
if (!bh)
return;
blocksize = bh->b_size;
block_start = 0;
do {
block_end = block_start + blocksize;
if (block_end <= from || block_start >= to) {
if (!buffer_uptodate(bh))
partial = true;
} else {
set_buffer_uptodate(bh);
mark_buffer_dirty(bh);
}
if (buffer_new(bh))
clear_buffer_new(bh);
block_start = block_end;
bh = bh->b_this_page;
} while (bh != head);
/*
* If this is a partial write which happened to make all buffers
* uptodate then we can optimize away a bogus read_folio() for
* the next read(). Here we 'discover' whether the folio went
* uptodate as a result of this (potentially partial) write.
*/
if (!partial)
folio_mark_uptodate(folio);
}
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete definition from Linux v6.18 fs/buffer.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/fs/buffer.c
* Copyright (C) 1991, 1992, 2002 Linus Torvalds
* Kernel excerpt; not a standalone user-space program.
* The book's exact source tag is not asserted.
*/
void mark_buffer_dirty(struct buffer_head *bh)
{
WARN_ON_ONCE(!buffer_uptodate(bh));
trace_block_dirty_buffer(bh);
/*
* Very *carefully* optimize the it-is-already-dirty case.
*
* Don't let the final "is it dirty" escape to before we
* perhaps modified the buffer.
*/
if (buffer_dirty(bh)) {
smp_mb();
if (buffer_dirty(bh))
return;
}
if (!test_set_buffer_dirty(bh)) {
struct folio *folio = bh->b_folio;
struct address_space *mapping = NULL;
if (!folio_test_set_dirty(folio)) {
mapping = folio->mapping;
if (mapping)
__folio_mark_dirty(folio, mapping, 0);
}
if (mapping)
__mark_inode_dirty(mapping->host, I_DIRTY_PAGES);
}
}
예전 page 기반 이름인 __block_commit_write()와 비교하려면 다음 v6.0 전체 정의를 사용할 수 있습니다. 인자와 반환형이 현재 함수와 다르지만 이것도 write-end 처리이지 writeback의 완료 함수는 아닙니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete definition from Linux v6.0 fs/buffer.c.
* Source: https://github.com/torvalds/linux/blob/v6.0/fs/buffer.c
* Copyright (C) 1991, 1992, 2002 Linus Torvalds
* Kernel excerpt; not a standalone user-space program.
* The book's exact source tag is not asserted.
*/
static int __block_commit_write(struct inode *inode, struct page *page,
unsigned from, unsigned to)
{
unsigned block_start, block_end;
int partial = 0;
unsigned blocksize;
struct buffer_head *bh, *head;
bh = head = page_buffers(page);
blocksize = bh->b_size;
block_start = 0;
do {
block_end = block_start + blocksize;
if (block_end <= from || block_start >= to) {
if (!buffer_uptodate(bh))
partial = 1;
} else {
set_buffer_uptodate(bh);
mark_buffer_dirty(bh);
}
if (buffer_new(bh))
clear_buffer_new(bh);
block_start = block_end;
bh = bh->b_this_page;
} while (bh != head);
/*
* If this is a partial write which happened to make all buffers
* uptodate then we can optimize away a bogus read_folio() for
* the next read(). Here we 'discover' whether the page went
* uptodate as a result of this (potentially partial) write.
*/
if (!partial)
SetPageUptodate(page);
return 0;
}
개별 bread 호출이 모든 buffer를 각각 uptodate로 만들었더라도 그 사실만으로 backing folio의 uptodate bit가 자동 설정되지는 않습니다. 나중에 block_read_full_folio() 같은 집계 경로가 모든 buffer의 유효성을 확인하고 folio 상태를 올릴 수 있습니다. buffer와 folio의 bit를 같은 저장 장소로 생각해서는 안 됩니다.
BIO 제출 이후의 수명은 callback까지 이어집니다
submit_bh_wbc()는 locked, mapped 상태이고 완료 callback이 있으며 delay나 unwritten 상태가 아닌 buffer를 요구합니다. 이런 전제는 아직 block 배치가 결정되지 않은 buffer를 그대로 device에 보내서는 안 된다는 뜻입니다.
이 함수는 BIO를 만들고 device sector, folio와 offset, 완료 callback과 private pointer를 연결합니다. metadata나 priority 상태는 request flag에도 반영합니다. writeback context가 있으면 cgroup 소유권 accounting도 이어집니다. 마지막의 submit_bio()는 제출이지 대기까지 포함한 완료가 아닙니다. 완료 시에는 BIO가 보관한 buffer pointer로 돌아와 b_end_io를 호출하고 BIO 참조를 정리합니다.
read 완료는 성공 여부에 따라 uptodate를 설정하거나 지운 뒤 buffer lock을 풀어 waiter를 깨웁니다. 이때 참조를 놓는 순서는 잠금 해제와 경쟁하는 다른 사용자를 고려해야 합니다. v6.18 end_buffer_read_sync()는 자신의 buffer 참조를 먼저 놓고, buffer를 더 건드리지 않도록 설계된 완료 helper로 상태와 unlock을 처리합니다. 오래된 코드의 순서를 현재 코드에 그대로 대입해서는 안 됩니다. v6.18 end_buffer_read_sync와 end_bio_bh_io_sync
아래는 제출 wrapper, 본체, BIO 완료의 전체입니다. guard_bio_eod()는 device 끝을 가로지르는 요청을 다루고, encryption context와 writeback의 cgroup accounting도 본체에 포함됩니다. write 요청을 다시 제출할 때 이전 write 오류를 지우는 조건은 BH_Req가 이미 있었는지와 operation을 함께 봅니다. 원래 block 배치가 없는 delay·unwritten buffer를 제출하지 말아야 한다는 앞의 전제는 이 코드의 BUG 검사에서 확인할 수 있습니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete definition from Linux v6.18 fs/buffer.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/fs/buffer.c
* Copyright (C) 1991, 1992, 2002 Linus Torvalds
* Kernel excerpt; not a standalone user-space program.
* The book's exact source tag is not asserted.
*/
void submit_bh(blk_opf_t opf, struct buffer_head *bh)
{
submit_bh_wbc(opf, bh, WRITE_LIFE_NOT_SET, NULL);
}
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete definition from Linux v6.18 fs/buffer.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/fs/buffer.c
* Copyright (C) 1991, 1992, 2002 Linus Torvalds
* Kernel excerpt; not a standalone user-space program.
* The book's exact source tag is not asserted.
*/
static void submit_bh_wbc(blk_opf_t opf, struct buffer_head *bh,
enum rw_hint write_hint,
struct writeback_control *wbc)
{
const enum req_op op = opf & REQ_OP_MASK;
struct bio *bio;
BUG_ON(!buffer_locked(bh));
BUG_ON(!buffer_mapped(bh));
BUG_ON(!bh->b_end_io);
BUG_ON(buffer_delay(bh));
BUG_ON(buffer_unwritten(bh));
/*
* Only clear out a write error when rewriting
*/
if (test_set_buffer_req(bh) && (op == REQ_OP_WRITE))
clear_buffer_write_io_error(bh);
if (buffer_meta(bh))
opf |= REQ_META;
if (buffer_prio(bh))
opf |= REQ_PRIO;
bio = bio_alloc(bh->b_bdev, 1, opf, GFP_NOIO);
fscrypt_set_bio_crypt_ctx_bh(bio, bh, GFP_NOIO);
bio->bi_iter.bi_sector = bh->b_blocknr * (bh->b_size >> 9);
bio->bi_write_hint = write_hint;
bio_add_folio_nofail(bio, bh->b_folio, bh->b_size, bh_offset(bh));
bio->bi_end_io = end_bio_bh_io_sync;
bio->bi_private = bh;
/* Take care of bh's that straddle the end of the device */
guard_bio_eod(bio);
if (wbc) {
wbc_init_bio(wbc, bio);
wbc_account_cgroup_owner(wbc, bh->b_folio, bh->b_size);
}
submit_bio(bio);
}
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete definition from Linux v6.18 fs/buffer.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/fs/buffer.c
* Copyright (C) 1991, 1992, 2002 Linus Torvalds
* Kernel excerpt; not a standalone user-space program.
* The book's exact source tag is not asserted.
*/
static void end_bio_bh_io_sync(struct bio *bio)
{
struct buffer_head *bh = bio->bi_private;
if (unlikely(bio_flagged(bio, BIO_QUIET)))
set_bit(BH_Quiet, &bh->b_state);
bh->b_end_io(bh, !bio->bi_status);
bio_put(bio);
}
read completion의 순서 차이는 다음 v6.0 정의와 직접 비교할 수 있습니다. 과거 함수는 helper 뒤에 put_bh()를 호출합니다. 현재 함수의 순서를 설명하면서 이 예전 전체 정의를 섞어 쓰지 않습니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete definition from Linux v6.0 fs/buffer.c.
* Source: https://github.com/torvalds/linux/blob/v6.0/fs/buffer.c
* Copyright (C) 1991, 1992, 2002 Linus Torvalds
* Kernel excerpt; not a standalone user-space program.
* The book's exact source tag is not asserted.
*/
void end_buffer_read_sync(struct buffer_head *bh, int uptodate)
{
__end_buffer_read_notouch(bh, uptodate);
put_bh(bh);
}
dirty cache를 device에 제출하는 일과 device cache를 flush하는 일도 다릅니다. v6.18 blkdev_issue_flush()는 data vector가 없는 write BIO에 REQ_PREFLUSH를 붙여 제출하고 완료를 기다립니다. 이 함수 하나가 filesystem의 모든 dirty folio를 찾아 쓰거나 전체 sync의 순서를 대신하지는 않습니다.
// SPDX-License-Identifier: GPL-2.0
/*
* Complete definition from Linux v6.18 block/blk-flush.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/block/blk-flush.c
* Copyright (C) 2011 Max Planck Institute for Gravitational Physics
* Copyright (C) 2011 Tejun Heo <tj@kernel.org>
* Kernel excerpt; not a standalone user-space program.
* The book's exact source tag is not asserted.
*/
int blkdev_issue_flush(struct block_device *bdev)
{
struct bio bio;
bio_init(&bio, bdev, NULL, 0, REQ_OP_WRITE | REQ_PREFLUSH);
return submit_bio_wait(&bio);
}
filesystem이 모두 이 방식으로 block마다 buffer를 유지해야 하는 것은 아닙니다. iomap은 byte 범위와 extent mapping을 중심으로 I/O를 구성하는 다른 공통 기반을 제공합니다. 따라서 buffer_head를 이해하는 목적은 모든 filesystem의 내부를 하나로 단정하는 데 있지 않습니다. cached byte의 유효성, block 배치, dirty 추적, 비동기 완료라는 책임이 어떻게 분리되는지 읽을 수 있게 되는 데 있습니다. v6.18 iomap design
참고 자료
- Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 9의 buffer 및 block I/O 설명.
- Linux v6.18 fs/buffer.c: buffer lookup, cached write-end, BIO 제출과 완료.
- Linux v6.18 block/bdev.c: block device와 inode의 관리.
- Linux v6.18 iomap design: range 기반 filesystem I/O와 cache coherency.