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 공간인 이유를 보여 주는 초기화입니다.

bdev_alloc_v6_18.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 * Complete definition from Linux v6.18 block/bdev.c.
 * Source: https://github.com/torvalds/linux/blob/v6.18/block/bdev.c
 * Copyright (C) 1991, 1992  Linus Torvalds
 * Copyright (C) 2001  Andrea Arcangeli <andrea@suse.de> SuSE
 * Copyright (C) 2016 - 2020 Christoph Hellwig
 * Kernel excerpt; not a standalone user-space program.
 * The book's exact source tag is not asserted.
 */

struct block_device *bdev_alloc(struct gendisk *disk, u8 partno)
{
	struct block_device *bdev;
	struct inode *inode;

	inode = new_inode(blockdev_superblock);
	if (!inode)
		return NULL;
	inode->i_mode = S_IFBLK;
	inode->i_rdev = 0;
	inode->i_data.a_ops = &def_blk_aops;
	mapping_set_gfp_mask(&inode->i_data, GFP_USER);

	bdev = I_BDEV(inode);
	mutex_init(&bdev->bd_fsfreeze_mutex);
	spin_lock_init(&bdev->bd_size_lock);
	mutex_init(&bdev->bd_holder_lock);
	atomic_set(&bdev->__bd_flags, partno);
	bdev->bd_mapping = &inode->i_data;
	bdev->bd_queue = disk->queue;
	if (partno && bdev_test_flag(disk->part0, BD_HAS_SUBMIT_BIO))
		bdev_set_flag(bdev, BD_HAS_SUBMIT_BIO);
	bdev->bd_stats = alloc_percpu(struct disk_stats);
	if (!bdev->bd_stats) {
		iput(inode);
		return NULL;
	}
	bdev->bd_disk = disk;
	return bdev;
}

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

bdev_add_v6_18.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 * Complete definition from Linux v6.18 block/bdev.c.
 * Source: https://github.com/torvalds/linux/blob/v6.18/block/bdev.c
 * Copyright (C) 1991, 1992  Linus Torvalds
 * Copyright (C) 2001  Andrea Arcangeli <andrea@suse.de> SuSE
 * Copyright (C) 2016 - 2020 Christoph Hellwig
 * Kernel excerpt; not a standalone user-space program.
 * The book's exact source tag is not asserted.
 */

void bdev_add(struct block_device *bdev, dev_t dev)
{
	struct inode *inode = BD_INODE(bdev);
	if (bdev_stable_writes(bdev))
		mapping_set_stable_writes(bdev->bd_mapping);
	bdev->bd_dev = dev;
	inode->i_rdev = dev;
	inode->i_ino = dev;
	insert_inode_hash(inode);
}

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

blkdev_open_v6_18.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 * Complete definition from Linux v6.18 block/fops.c.
 * Source: https://github.com/torvalds/linux/blob/v6.18/block/fops.c
 * Copyright (C) 1991, 1992  Linus Torvalds
 * Copyright (C) 2001  Andrea Arcangeli <andrea@suse.de> SuSE
 * Copyright (C) 2016 - 2020 Christoph Hellwig
 * Kernel excerpt; not a standalone user-space program.
 * The book's exact source tag is not asserted.
 */

static int blkdev_open(struct inode *inode, struct file *filp)
{
	struct block_device *bdev;
	blk_mode_t mode;
	int ret;

	mode = file_to_blk_mode(filp);
	/* Use the file as the holder. */
	if (mode & BLK_OPEN_EXCL)
		filp->private_data = filp;
	ret = bdev_permission(inode->i_rdev, mode, filp->private_data);
	if (ret)
		return ret;

	bdev = blkdev_get_no_open(inode->i_rdev, true);
	if (!bdev)
		return -ENXIO;

	if (bdev_can_atomic_write(bdev))
		filp->f_mode |= FMODE_CAN_ATOMIC_WRITE;
	if (blk_get_integrity(bdev->bd_disk))
		filp->f_mode |= FMODE_HAS_METADATA;

	ret = bdev_open(bdev, mode, filp->private_data, NULL, filp);
	if (ret)
		blkdev_put_no_open(bdev);
	return ret;
}

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 전체 정의입니다.

bdev_open_v6_18.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 * Complete definition from Linux v6.18 block/bdev.c.
 * Source: https://github.com/torvalds/linux/blob/v6.18/block/bdev.c
 * Copyright (C) 1991, 1992  Linus Torvalds
 * Copyright (C) 2001  Andrea Arcangeli <andrea@suse.de> SuSE
 * Copyright (C) 2016 - 2020 Christoph Hellwig
 * Kernel excerpt; not a standalone user-space program.
 * The book's exact source tag is not asserted.
 */

int bdev_open(struct block_device *bdev, blk_mode_t mode, void *holder,
	      const struct blk_holder_ops *hops, struct file *bdev_file)
{
	bool unblock_events = true;
	struct gendisk *disk = bdev->bd_disk;
	int ret;

	if (holder) {
		mode |= BLK_OPEN_EXCL;
		ret = bd_prepare_to_claim(bdev, holder, hops);
		if (ret)
			return ret;
	} else {
		if (WARN_ON_ONCE(mode & BLK_OPEN_EXCL))
			return -EIO;
	}

	disk_block_events(disk);

	mutex_lock(&disk->open_mutex);
	ret = -ENXIO;
	if (!disk_live(disk))
		goto abort_claiming;
	if (!try_module_get(disk->fops->owner))
		goto abort_claiming;
	ret = -EBUSY;
	if (!bdev_may_open(bdev, mode))
		goto put_module;
	if (bdev_is_partition(bdev))
		ret = blkdev_get_part(bdev, mode);
	else
		ret = blkdev_get_whole(bdev, mode);
	if (ret)
		goto put_module;
	bdev_claim_write_access(bdev, mode);
	if (holder) {
		bd_finish_claiming(bdev, holder, hops);

		/*
		 * Block event polling for write claims if requested.  Any write
		 * holder makes the write_holder state stick until all are
		 * released.  This is good enough and tracking individual
		 * writeable reference is too fragile given the way @mode is
		 * used in blkdev_get/put().
		 */
		if ((mode & BLK_OPEN_WRITE) &&
		    !bdev_test_flag(bdev, BD_WRITE_HOLDER) &&
		    (disk->event_flags & DISK_EVENT_FLAG_BLOCK_ON_EXCL_WRITE)) {
			bdev_set_flag(bdev, BD_WRITE_HOLDER);
			unblock_events = false;
		}
	}
	mutex_unlock(&disk->open_mutex);

	if (unblock_events)
		disk_unblock_events(disk);

	bdev_file->f_flags |= O_LARGEFILE;
	bdev_file->f_mode |= FMODE_CAN_ODIRECT;
	if (bdev_nowait(bdev))
		bdev_file->f_mode |= FMODE_NOWAIT;
	if (mode & BLK_OPEN_RESTRICT_WRITES)
		bdev_file->f_mode |= FMODE_WRITE_RESTRICTED;
	bdev_file->f_mapping = bdev->bd_mapping;
	bdev_file->f_wb_err = filemap_sample_wb_err(bdev_file->f_mapping);
	bdev_file->private_data = holder;

	return 0;
put_module:
	module_put(disk->fops->owner);
abort_claiming:
	if (holder)
		bd_abort_claiming(bdev, holder);
	mutex_unlock(&disk->open_mutex);
	disk_unblock_events(disk);
	return ret;
}

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()의 bufferram = nr_blockdev_pages()와 meminfo 출력에서 확인할 수 있습니다. meminfo는 이 값을 page 단위에서 kB 표시로 변환합니다.

nr_blockdev_pages_v6_18.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 * Complete definition from Linux v6.18 block/bdev.c.
 * Source: https://github.com/torvalds/linux/blob/v6.18/block/bdev.c
 * Copyright (C) 1991, 1992  Linus Torvalds
 * Copyright (C) 2001  Andrea Arcangeli <andrea@suse.de> SuSE
 * Copyright (C) 2016 - 2020 Christoph Hellwig
 * Kernel excerpt; not a standalone user-space program.
 * The book's exact source tag is not asserted.
 */

long nr_blockdev_pages(void)
{
	struct inode *inode;
	long ret = 0;

	spin_lock(&blockdev_superblock->s_inode_list_lock);
	list_for_each_entry(inode, &blockdev_superblock->s_inodes, i_sb_list)
		ret += inode->i_mapping->nrpages;
	spin_unlock(&blockdev_superblock->s_inode_list_lock);

	return ret;
}

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의 전체 정의입니다.

bdev_getblk_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.
 */

struct buffer_head *bdev_getblk(struct block_device *bdev, sector_t block,
		unsigned size, gfp_t gfp)
{
	struct buffer_head *bh;

	if (gfpflags_allow_blocking(gfp))
		bh = __find_get_block_nonatomic(bdev, block, size);
	else
		bh = __find_get_block(bdev, block, size);

	might_alloc(gfp);
	if (bh)
		return bh;

	return __getblk_slow(bdev, block, size, gfp);
}

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

__getblk_gfp_v6_0.c

// 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.
 */

struct buffer_head *
__getblk_gfp(struct block_device *bdev, sector_t block,
	     unsigned size, gfp_t gfp)
{
	struct buffer_head *bh = __find_get_block(bdev, block, size);

	might_sleep();
	if (bh == NULL)
		bh = __getblk_slow(bdev, block, size, gfp);
	return bh;
}

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

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

find_get_block_common_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 struct buffer_head *
find_get_block_common(struct block_device *bdev, sector_t block,
			unsigned size, bool atomic)
{
	struct buffer_head *bh = lookup_bh_lru(bdev, block, size);

	if (bh == NULL) {
		/* __find_get_block_slow will mark the page accessed */
		bh = __find_get_block_slow(bdev, block, atomic);
		if (bh)
			bh_lru_install(bh);
	} else
		touch_buffer(bh);

	return bh;
}

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

lookup_bh_lru_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 struct buffer_head *
lookup_bh_lru(struct block_device *bdev, sector_t block, unsigned size)
{
	struct buffer_head *ret = NULL;
	unsigned int i;

	check_irqs_on();
	bh_lru_lock();
	if (cpu_is_isolated(smp_processor_id())) {
		bh_lru_unlock();
		return NULL;
	}
	for (i = 0; i < BH_LRU_SIZE; i++) {
		struct buffer_head *bh = __this_cpu_read(bh_lrus.bhs[i]);

		if (bh && bh->b_blocknr == block && bh->b_bdev == bdev &&
		    bh->b_size == size) {
			if (i) {
				while (i) {
					__this_cpu_write(bh_lrus.bhs[i],
						__this_cpu_read(bh_lrus.bhs[i - 1]));
					i--;
				}
				__this_cpu_write(bh_lrus.bhs[0], bh);
			}
			get_bh(bh);
			ret = bh;
			break;
		}
	}
	bh_lru_unlock();
	return ret;
}

느린 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은 반환 전에 정리합니다.

__find_get_block_slow_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 struct buffer_head *
__find_get_block_slow(struct block_device *bdev, sector_t block, bool atomic)
{
	struct address_space *bd_mapping = bdev->bd_mapping;
	const int blkbits = bd_mapping->host->i_blkbits;
	struct buffer_head *ret = NULL;
	pgoff_t index;
	struct buffer_head *bh;
	struct buffer_head *head;
	struct folio *folio;
	int all_mapped = 1;
	static DEFINE_RATELIMIT_STATE(last_warned, HZ, 1);

	index = ((loff_t)block << blkbits) / PAGE_SIZE;
	folio = __filemap_get_folio(bd_mapping, index, FGP_ACCESSED, 0);
	if (IS_ERR(folio))
		goto out;

	/*
	 * Folio lock protects the buffers. Callers that cannot block
	 * will fallback to serializing vs try_to_free_buffers() via
	 * the i_private_lock.
	 */
	if (atomic)
		spin_lock(&bd_mapping->i_private_lock);
	else
		folio_lock(folio);

	head = folio_buffers(folio);
	if (!head)
		goto out_unlock;
	/*
	 * Upon a noref migration, the folio lock serializes here;
	 * otherwise bail.
	 */
	if (test_bit_acquire(BH_Migrate, &head->b_state)) {
		WARN_ON(!atomic);
		goto out_unlock;
	}

	bh = head;
	do {
		if (!buffer_mapped(bh))
			all_mapped = 0;
		else if (bh->b_blocknr == block) {
			ret = bh;
			get_bh(bh);
			goto out_unlock;
		}
		bh = bh->b_this_page;
	} while (bh != head);

	/* we might be here because some of the buffers on this page are
	 * not mapped.  This is due to various races between
	 * file io on the block device and getblk.  It gets dealt with
	 * elsewhere, don't buffer_error if we had some unmapped buffers
	 */
	ratelimit_set_flags(&last_warned, RATELIMIT_MSG_ON_RELEASE);
	if (all_mapped && __ratelimit(&last_warned)) {
		printk("__find_get_block_slow() failed. block=%llu, "
		       "b_blocknr=%llu, b_state=0x%08lx, b_size=%zu, "
		       "device %pg blocksize: %d\n",
		       (unsigned long long)block,
		       (unsigned long long)bh->b_blocknr,
		       bh->b_state, bh->b_size, bdev,
		       1 << blkbits);
	}
out_unlock:
	if (atomic)
		spin_unlock(&bd_mapping->i_private_lock);
	else
		folio_unlock(folio);
	folio_put(folio);
out:
	return ret;
}

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

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

__getblk_slow_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 struct buffer_head *
__getblk_slow(struct block_device *bdev, sector_t block,
	     unsigned size, gfp_t gfp)
{
	bool blocking = gfpflags_allow_blocking(gfp);

	if (WARN_ON_ONCE(!IS_ALIGNED(size, bdev_logical_block_size(bdev)))) {
		printk(KERN_ERR "getblk(): block size %d not aligned to logical block size %d\n",
		       size, bdev_logical_block_size(bdev));
		return NULL;
	}

	for (;;) {
		struct buffer_head *bh;

		if (!grow_buffers(bdev, block, size, gfp))
			return NULL;

		if (blocking)
			bh = __find_get_block_nonatomic(bdev, block, size);
		else
			bh = __find_get_block(bdev, block, size);
		if (bh)
			return bh;
	}
}

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

grow_buffers_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 bool grow_buffers(struct block_device *bdev, sector_t block,
		unsigned size, gfp_t gfp)
{
	loff_t pos;

	/*
	 * Check for a block which lies outside our maximum possible
	 * pagecache index.
	 */
	if (check_mul_overflow(block, (sector_t)size, &pos) || pos > MAX_LFS_FILESIZE) {
		printk(KERN_ERR "%s: requested out-of-range block %llu for device %pg\n",
			__func__, (unsigned long long)block,
			bdev);
		return false;
	}

	/* Create a folio with the proper size buffers */
	return grow_dev_folio(bdev, block, pos / PAGE_SIZE, size, gfp);
}

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과 참조를 모두 정리합니다.

grow_dev_folio_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 bool grow_dev_folio(struct block_device *bdev, sector_t block,
		pgoff_t index, unsigned size, gfp_t gfp)
{
	struct address_space *mapping = bdev->bd_mapping;
	struct folio *folio;
	struct buffer_head *bh;
	sector_t end_block = 0;

	folio = __filemap_get_folio(mapping, index,
			FGP_LOCK | FGP_ACCESSED | FGP_CREAT, gfp);
	if (IS_ERR(folio))
		return false;

	bh = folio_buffers(folio);
	if (bh) {
		if (bh->b_size == size) {
			end_block = folio_init_buffers(folio, bdev, size);
			goto unlock;
		}

		/*
		 * Retrying may succeed; for example the folio may finish
		 * writeback, or buffers may be cleaned.  This should not
		 * happen very often; maybe we have old buffers attached to
		 * this blockdev's page cache and we're trying to change
		 * the block size?
		 */
		if (!try_to_free_buffers(folio)) {
			end_block = ~0ULL;
			goto unlock;
		}
	}

	bh = folio_alloc_buffers(folio, size, gfp | __GFP_ACCOUNT);
	if (!bh)
		goto unlock;

	/*
	 * Link the folio to the buffers and initialise them.  Take the
	 * lock to be atomic wrt __find_get_block(), which does not
	 * run under the folio lock.
	 */
	spin_lock(&mapping->i_private_lock);
	link_dev_buffers(folio, bh);
	end_block = folio_init_buffers(folio, bdev, size);
	spin_unlock(&mapping->i_private_lock);
unlock:
	folio_unlock(folio);
	folio_put(folio);
	return block < end_block;
}

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

folio_init_buffers_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 sector_t folio_init_buffers(struct folio *folio,
		struct block_device *bdev, unsigned size)
{
	struct buffer_head *head = folio_buffers(folio);
	struct buffer_head *bh = head;
	bool uptodate = folio_test_uptodate(folio);
	sector_t block = div_u64(folio_pos(folio), size);
	sector_t end_block = blkdev_max_block(bdev, size);

	do {
		if (!buffer_mapped(bh)) {
			bh->b_end_io = NULL;
			bh->b_private = NULL;
			bh->b_bdev = bdev;
			bh->b_blocknr = block;
			if (uptodate)
				set_buffer_uptodate(bh);
			if (block < end_block)
				set_buffer_mapped(bh);
		}
		block++;
		bh = bh->b_this_page;
	} while (bh != head);

	/*
	 * Caller needs to validate requested block against end of device.
	 */
	return end_block;
}

Data와 buffer_head는 따로 할당합니다

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

folio_alloc_buffers_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.
 */

struct buffer_head *folio_alloc_buffers(struct folio *folio, unsigned long size,
					gfp_t gfp)
{
	struct buffer_head *bh, *head;
	long offset;
	struct mem_cgroup *memcg, *old_memcg;

	/* The folio lock pins the memcg */
	memcg = folio_memcg(folio);
	old_memcg = set_active_memcg(memcg);

	head = NULL;
	offset = folio_size(folio);
	while ((offset -= size) >= 0) {
		bh = alloc_buffer_head(gfp);
		if (!bh)
			goto no_grow;

		bh->b_this_page = head;
		bh->b_blocknr = -1;
		head = bh;

		bh->b_size = size;

		/* Link the buffer to its folio */
		folio_set_bh(bh, folio, offset);
	}
out:
	set_active_memcg(old_memcg);
	return head;
/*
 * In case anything failed, we just free everything we got.
 */
no_grow:
	if (head) {
		do {
			bh = head;
			head = head->b_this_page;
			free_buffer_head(bh);
		} while (head);
	}

	goto out;
}

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

alloc_buffer_head_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.
 */

struct buffer_head *alloc_buffer_head(gfp_t gfp_flags)
{
	struct buffer_head *ret = kmem_cache_zalloc(bh_cachep, gfp_flags);
	if (ret) {
		INIT_LIST_HEAD(&ret->b_assoc_buffers);
		spin_lock_init(&ret->b_uptodate_lock);
		preempt_disable();
		__this_cpu_inc(bh_accounting.nr);
		recalc_bh_state();
		preempt_enable();
	}
	return ret;
}

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

참고 자료