From: Simon Glass <simon.glass@canonical.com> Add support.c with buffer_head I/O infrastructure for ext4l: - Buffer cache for caching buffer_heads across lookups - Buffer allocation/free functions - Block I/O functions (sb_getblk, sb_bread, brelse, submit_bh, bh_read) This keeps interface.c focused on the U-Boot filesystem layer interface. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- fs/ext4l/Makefile | 2 +- fs/ext4l/ext4_uboot.h | 36 ++- fs/ext4l/interface.c | 60 ++++- fs/ext4l/stub.c | 18 +- fs/ext4l/support.c | 428 ++++++++++++++++++++++++++++++++++++ include/linux/buffer_head.h | 8 +- 6 files changed, 528 insertions(+), 24 deletions(-) create mode 100644 fs/ext4l/support.c diff --git a/fs/ext4l/Makefile b/fs/ext4l/Makefile index e99b900ca6d..7bb843c3fad 100644 --- a/fs/ext4l/Makefile +++ b/fs/ext4l/Makefile @@ -3,7 +3,7 @@ # Makefile for the ext4l filesystem (Linux port) # -obj-y := interface.o stub.o +obj-y := interface.o support.o stub.o obj-y += balloc.o bitmap.o block_validity.o dir.o ext4_jbd2.o extents.o \ extents_status.o file.o fsmap.o fsync.o hash.o ialloc.o \ diff --git a/fs/ext4l/ext4_uboot.h b/fs/ext4l/ext4_uboot.h index 7c65aa8567e..dfa362389a8 100644 --- a/fs/ext4l/ext4_uboot.h +++ b/fs/ext4l/ext4_uboot.h @@ -313,9 +313,9 @@ extern struct user_namespace init_user_ns; #define __bforget(bh) do { } while (0) #define mark_buffer_dirty_inode(bh, i) do { } while (0) #define mark_buffer_dirty(bh) do { } while (0) -#define lock_buffer(bh) do { } while (0) -#define unlock_buffer(bh) do { } while (0) -#define sb_getblk(sb, block) ((struct buffer_head *)NULL) +#define lock_buffer(bh) set_buffer_locked(bh) +#define unlock_buffer(bh) clear_buffer_locked(bh) +struct buffer_head *sb_getblk(struct super_block *sb, sector_t block); #define test_clear_buffer_dirty(bh) ({ (void)(bh); 0; }) #define wait_on_bit_io(addr, bit, mode) do { (void)(addr); (void)(bit); (void)(mode); } while (0) @@ -1026,7 +1026,7 @@ static inline unsigned long memweight(const void *ptr, size_t bytes) #define rwsem_is_locked(sem) (1) /* Buffer operations */ -#define sb_getblk_gfp(sb, blk, gfp) ((struct buffer_head *)NULL) +#define sb_getblk_gfp(sb, blk, gfp) sb_getblk((sb), (blk)) #define bh_uptodate_or_lock(bh) (1) /* ext4_read_bh is stubbed in interface.c */ @@ -1870,7 +1870,14 @@ struct file_system_type { #define FS_ALLOW_IDMAP 32 /* Buffer read sync */ -#define end_buffer_read_sync NULL +static inline void end_buffer_read_sync(struct buffer_head *bh, int uptodate) +{ + if (uptodate) + set_buffer_uptodate(bh); + else + clear_buffer_uptodate(bh); + unlock_buffer(bh); +} #define REQ_OP_READ 0 /* Superblock flags */ @@ -2377,7 +2384,7 @@ void dquot_free_block(struct inode *inode, loff_t nr); /* Block device file operations - stubs */ #define set_blocksize(f, size) ({ (void)(f); (void)(size); 0; }) -#define __bread(bdev, block, size) ({ (void)(bdev); (void)(block); (void)(size); (struct buffer_head *)NULL; }) +struct buffer_head *__bread(struct block_device *bdev, sector_t block, unsigned size); /* Trace stubs for super.c */ #define trace_ext4_sync_fs(sb, wait) do { (void)(sb); (void)(wait); } while (0) @@ -2823,7 +2830,16 @@ struct wait_bit_entry { #define filemap_fdatawait_range_keep_errors(m, s, e) \ ({ (void)(m); (void)(s); (void)(e); 0; }) #define crc32_be(crc, p, len) crc32(crc, p, len) -#define free_buffer_head(bh) kfree(bh) +void free_buffer_head(struct buffer_head *bh); + +/* ext4l support functions (support.c) */ +void bh_cache_clear(void); +int ext4l_read_block(sector_t block, size_t size, void *buffer); + +/* ext4l interface functions (interface.c) */ +struct blk_desc *ext4l_get_blk_dev(void); +struct disk_partition *ext4l_get_partition(void); + #define sb_is_blkdev_sb(sb) ({ (void)(sb); 0; }) /* DEFINE_WAIT stub - creates a wait queue entry */ @@ -2850,7 +2866,7 @@ struct wait_bit_entry { #define trace_jbd2_lock_buffer_stall(...) do { } while (0) /* JBD2 journal.c stubs */ -#define alloc_buffer_head(gfp) ((struct buffer_head *)kzalloc(sizeof(struct buffer_head), gfp)) +struct buffer_head *alloc_buffer_head(gfp_t gfp_mask); #define __getblk(bdev, block, size) ({ (void)(bdev); (void)(block); (void)(size); (struct buffer_head *)NULL; }) #define bmap(inode, block) ({ (void)(inode); (void)(block); 0; }) #define trace_jbd2_update_log_tail(j, t, b, f) \ @@ -2897,8 +2913,8 @@ loff_t seq_lseek(struct file *f, loff_t o, int w); do { (void)(j); (void)(f); } while (0) /* Block device operations for journal.c */ -#define bh_read(bh, flags) ({ (void)(bh); (void)(flags); 0; }) -#define bh_read_nowait(bh, flags) do { (void)(bh); (void)(flags); } while (0) +int bh_read(struct buffer_head *bh, int flags); +#define bh_read_nowait(bh, flags) bh_read(bh, flags) #define bh_readahead_batch(n, bhs, f) do { (void)(n); (void)(bhs); (void)(f); } while (0) #define truncate_inode_pages_range(m, s, e) \ do { (void)(m); (void)(s); (void)(e); } while (0) diff --git a/fs/ext4l/interface.c b/fs/ext4l/interface.c index 4652f8c835f..e1458ea6bfe 100644 --- a/fs/ext4l/interface.c +++ b/fs/ext4l/interface.c @@ -5,13 +5,13 @@ * Copyright 2025 Canonical Ltd * Written by Simon Glass <simon.glass@canonical.com> * - * This provides the minimal interface between U-Boot and the ext4l driver. + * This provides the interface between U-Boot's filesystem layer and + * the ext4l driver. */ #include <blk.h> #include <part.h> #include <malloc.h> -#include <asm/byteorder.h> #include <linux/errno.h> #include <linux/jbd2.h> #include <linux/types.h> @@ -23,6 +23,58 @@ static struct blk_desc *ext4l_dev_desc; static struct disk_partition ext4l_part; +/* Global block device tracking for buffer I/O */ +static struct blk_desc *ext4l_blk_dev; +static struct disk_partition ext4l_partition; +static int ext4l_mounted; + +/** + * ext4l_get_blk_dev() - Get the current block device + * Return: Block device descriptor or NULL if not mounted + */ +struct blk_desc *ext4l_get_blk_dev(void) +{ + if (!ext4l_mounted) + return NULL; + return ext4l_blk_dev; +} + +/** + * ext4l_get_partition() - Get the current partition info + * Return: Partition info pointer + */ +struct disk_partition *ext4l_get_partition(void) +{ + return &ext4l_partition; +} + +/** + * ext4l_set_blk_dev() - Set the block device for ext4l operations + * @blk_dev: Block device descriptor + * @partition: Partition info (can be NULL for whole disk) + */ +void ext4l_set_blk_dev(struct blk_desc *blk_dev, struct disk_partition *partition) +{ + ext4l_blk_dev = blk_dev; + if (partition) + memcpy(&ext4l_partition, partition, sizeof(struct disk_partition)); + else + memset(&ext4l_partition, 0, sizeof(struct disk_partition)); + ext4l_mounted = 1; +} + +/** + * ext4l_clear_blk_dev() - Clear block device (unmount) + */ +void ext4l_clear_blk_dev(void) +{ + /* Clear buffer cache before unmounting */ + bh_cache_clear(); + + ext4l_blk_dev = NULL; + ext4l_mounted = 0; +} + int ext4l_probe(struct blk_desc *fs_dev_desc, struct disk_partition *fs_partition) { @@ -37,6 +89,9 @@ int ext4l_probe(struct blk_desc *fs_dev_desc, if (!fs_dev_desc) return -EINVAL; + /* Set up block device for buffer I/O */ + ext4l_set_blk_dev(fs_dev_desc, fs_partition); + /* Initialise journal subsystem if enabled */ if (IS_ENABLED(CONFIG_EXT4_JOURNAL)) { ret = jbd2_journal_init_global(); @@ -162,5 +217,6 @@ err_exit_es: void ext4l_close(void) { + ext4l_clear_blk_dev(); ext4l_dev_desc = NULL; } diff --git a/fs/ext4l/stub.c b/fs/ext4l/stub.c index e13685bf8fa..eff54bb540c 100644 --- a/fs/ext4l/stub.c +++ b/fs/ext4l/stub.c @@ -309,20 +309,14 @@ void *bdev_file_open_by_dev(dev_t dev, int flags, void *holder, return ERR_PTR(-ENODEV); } -struct buffer_head *bdev_getblk(struct block_device *bdev, sector_t block, - unsigned int size, gfp_t gfp) -{ - return NULL; -} +/* bdev_getblk implemented in interface.c */ int trylock_buffer(struct buffer_head *bh) { return 1; } -void submit_bh(int op, struct buffer_head *bh) -{ -} +/* submit_bh implemented in interface.c */ /* NFS export stubs */ struct dentry *generic_fh_to_parent(struct super_block *sb, struct fid *fid, @@ -519,6 +513,14 @@ void fs_put_dax(void *dax, void *holder) /* Block size */ int sb_set_blocksize(struct super_block *sb, int size) { + /* Validate block size */ + if (size != 1024 && size != 2048 && size != 4096) + return 0; + + /* Update superblock fields */ + sb->s_blocksize = size; + sb->s_blocksize_bits = ffs(size) - 1; + return size; } diff --git a/fs/ext4l/support.c b/fs/ext4l/support.c new file mode 100644 index 00000000000..0765065a99f --- /dev/null +++ b/fs/ext4l/support.c @@ -0,0 +1,428 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Internal support functions for ext4l filesystem + * + * Copyright 2025 Canonical Ltd + * Written by Simon Glass <simon.glass@canonical.com> + * + * This provides internal support functions for the ext4l driver, + * including buffer_head I/O and buffer cache. + */ + +#include <blk.h> +#include <part.h> +#include <malloc.h> +#include <linux/errno.h> +#include <linux/types.h> + +#include "ext4_uboot.h" +#include "ext4.h" + +/* + * Buffer cache implementation + * + * Linux's sb_getblk() returns the same buffer_head for the same block number, + * allowing flags like BH_Verified, BH_Uptodate, etc. to persist across calls. + * This is critical for ext4's bitmap validation which sets buffer_verified() + * and expects it to remain set on subsequent lookups. + */ +#define BH_CACHE_BITS 8 +#define BH_CACHE_SIZE (1 << BH_CACHE_BITS) +#define BH_CACHE_MASK (BH_CACHE_SIZE - 1) + +struct bh_cache_entry { + struct buffer_head *bh; + struct bh_cache_entry *next; +}; + +static struct bh_cache_entry *bh_cache[BH_CACHE_SIZE]; + +static inline unsigned int bh_cache_hash(sector_t block) +{ + return (unsigned int)(block & BH_CACHE_MASK); +} + +/** + * bh_cache_lookup() - Look up a buffer in the cache + * @block: Block number to look up + * @size: Expected block size + * Return: Buffer head if found with matching size, NULL otherwise + */ +static struct buffer_head *bh_cache_lookup(sector_t block, size_t size) +{ + unsigned int hash = bh_cache_hash(block); + struct bh_cache_entry *entry; + + for (entry = bh_cache[hash]; entry; entry = entry->next) { + if (entry->bh && entry->bh->b_blocknr == block && + entry->bh->b_size == size) { + atomic_inc(&entry->bh->b_count); + return entry->bh; + } + } + return NULL; +} + +/** + * bh_cache_insert() - Insert a buffer into the cache + * @bh: Buffer head to insert + */ +static void bh_cache_insert(struct buffer_head *bh) +{ + unsigned int hash = bh_cache_hash(bh->b_blocknr); + struct bh_cache_entry *entry; + + /* Check if already in cache */ + for (entry = bh_cache[hash]; entry; entry = entry->next) { + if (entry->bh && entry->bh->b_blocknr == bh->b_blocknr) + return; /* Already cached */ + } + + entry = malloc(sizeof(struct bh_cache_entry)); + if (!entry) + return; /* Silently fail - cache is optional */ + + entry->bh = bh; + entry->next = bh_cache[hash]; + bh_cache[hash] = entry; + + /* Add a reference to keep the buffer alive in cache */ + atomic_inc(&bh->b_count); +} + +/** + * bh_cache_clear() - Clear the entire buffer cache + * + * Called on unmount to free all cached buffers. + */ +void bh_cache_clear(void) +{ + int i; + struct bh_cache_entry *entry, *next; + + for (i = 0; i < BH_CACHE_SIZE; i++) { + for (entry = bh_cache[i]; entry; entry = next) { + next = entry->next; + if (entry->bh) { + /* Release the cache's reference */ + if (atomic_dec_and_test(&entry->bh->b_count)) + free_buffer_head(entry->bh); + } + free(entry); + } + bh_cache[i] = NULL; + } +} + +/** + * alloc_buffer_head() - Allocate a buffer_head structure + * @gfp_mask: Allocation flags (ignored in U-Boot) + * Return: Pointer to buffer_head or NULL on error + */ +struct buffer_head *alloc_buffer_head(gfp_t gfp_mask) +{ + struct buffer_head *bh; + + bh = malloc(sizeof(struct buffer_head)); + if (!bh) + return NULL; + + memset(bh, 0, sizeof(struct buffer_head)); + + /* Note: b_data will be allocated when needed by read functions */ + atomic_set(&bh->b_count, 1); + + return bh; +} + +/** + * alloc_buffer_head_with_data() - Allocate a buffer_head with data buffer + * @size: Size of the data buffer to allocate + * Return: Pointer to buffer_head or NULL on error + */ +static struct buffer_head *alloc_buffer_head_with_data(size_t size) +{ + struct buffer_head *bh; + + bh = malloc(sizeof(struct buffer_head)); + if (!bh) + return NULL; + + memset(bh, 0, sizeof(struct buffer_head)); + + bh->b_data = malloc(size); + if (!bh->b_data) { + free(bh); + return NULL; + } + + bh->b_size = size; + /* Allocate a folio for kmap_local_folio() to work */ + bh->b_folio = malloc(sizeof(struct folio)); + if (bh->b_folio) { + memset(bh->b_folio, 0, sizeof(struct folio)); + bh->b_folio->data = bh->b_data; + } + atomic_set(&bh->b_count, 1); + /* Mark that this buffer owns its b_data and should free it */ + set_bit(BH_OwnsData, &bh->b_state); + + return bh; +} + +/** + * free_buffer_head() - Free a buffer_head + * @bh: Buffer head to free + * + * Only free b_data if BH_OwnsData is set. Shadow buffers created by + * jbd2_journal_write_metadata_buffer() share b_data with the original + * buffer and should not free it. + */ +void free_buffer_head(struct buffer_head *bh) +{ + if (!bh) + return; + + /* Only free b_data if this buffer owns it */ + if (bh->b_data && test_bit(BH_OwnsData, &bh->b_state)) + free(bh->b_data); + if (bh->b_folio) + free(bh->b_folio); + free(bh); +} + +/** + * ext4l_read_block() - Read a block from the block device + * @block: Block number (filesystem block, not sector) + * @size: Block size in bytes + * @buffer: Destination buffer + * Return: 0 on success, negative on error + */ +int ext4l_read_block(sector_t block, size_t size, void *buffer) +{ + struct blk_desc *blk_dev; + struct disk_partition *part; + lbaint_t sector; + lbaint_t sector_count; + unsigned long n; + + blk_dev = ext4l_get_blk_dev(); + part = ext4l_get_partition(); + if (!blk_dev) + return -EIO; + + /* Convert block to sector */ + sector = (block * size) / blk_dev->blksz + part->start; + sector_count = size / blk_dev->blksz; + + if (sector_count == 0) + sector_count = 1; + + n = blk_dread(blk_dev, sector, sector_count, buffer); + if (n != sector_count) + return -EIO; + + return 0; +} + +/** + * sb_getblk() - Get a buffer, using cache if available + * @sb: Super block + * @block: Block number + * Return: Buffer head or NULL on error + */ +struct buffer_head *sb_getblk(struct super_block *sb, sector_t block) +{ + struct buffer_head *bh; + + if (!sb) + return NULL; + + /* Check cache first - must match block number AND size */ + bh = bh_cache_lookup(block, sb->s_blocksize); + if (bh) + return bh; + + /* Allocate new buffer */ + bh = alloc_buffer_head_with_data(sb->s_blocksize); + if (!bh) + return NULL; + + bh->b_blocknr = block; + bh->b_bdev = sb->s_bdev; + bh->b_size = sb->s_blocksize; + + /* Don't read - just allocate with zeroed data */ + memset(bh->b_data, '\0', bh->b_size); + + /* Add to cache */ + bh_cache_insert(bh); + + return bh; +} + +/** + * sb_bread() - Read a block via super_block + * @sb: Super block + * @block: Block number to read + * Return: Buffer head or NULL on error + */ +struct buffer_head *sb_bread(struct super_block *sb, sector_t block) +{ + struct buffer_head *bh; + int ret; + + if (!sb) + return NULL; + + bh = sb_getblk(sb, block); + if (!bh) + return NULL; + + /* If buffer is already up-to-date, return it without re-reading */ + if (buffer_uptodate(bh)) + return bh; + + bh->b_blocknr = block; + bh->b_bdev = sb->s_bdev; + bh->b_size = sb->s_blocksize; + + ret = ext4l_read_block(block, sb->s_blocksize, bh->b_data); + if (ret) { + brelse(bh); + return NULL; + } + + /* Mark buffer as up-to-date */ + set_buffer_uptodate(bh); + + return bh; +} + +/** + * brelse() - Release a buffer_head + * @bh: Buffer head to release + */ +void brelse(struct buffer_head *bh) +{ + if (!bh) + return; + + if (atomic_dec_and_test(&bh->b_count)) + free_buffer_head(bh); +} + +/** + * __brelse() - Release a buffer_head (alternate API) + * @bh: Buffer head to release + */ +void __brelse(struct buffer_head *bh) +{ + brelse(bh); +} + +/** + * bdev_getblk() - Get buffer via block_device + * @bdev: Block device + * @block: Block number + * @size: Block size + * @gfp: Allocation flags + * Return: Buffer head or NULL + */ +struct buffer_head *bdev_getblk(struct block_device *bdev, sector_t block, + unsigned size, gfp_t gfp) +{ + struct buffer_head *bh; + + /* Check cache first - must match block number AND size */ + bh = bh_cache_lookup(block, size); + if (bh) + return bh; + + bh = alloc_buffer_head_with_data(size); + if (!bh) + return NULL; + + bh->b_blocknr = block; + bh->b_bdev = bdev; + bh->b_size = size; + + /* Don't read - just allocate with zeroed data */ + memset(bh->b_data, 0, bh->b_size); + + /* Add to cache */ + bh_cache_insert(bh); + + return bh; +} + +/** + * __bread() - Read a block via block_device + * @bdev: Block device + * @block: Block number to read + * @size: Block size + * Return: Buffer head or NULL on error + */ +struct buffer_head *__bread(struct block_device *bdev, sector_t block, + unsigned size) +{ + struct buffer_head *bh; + int ret; + + bh = alloc_buffer_head_with_data(size); + if (!bh) + return NULL; + + bh->b_blocknr = block; + bh->b_bdev = bdev; + bh->b_size = size; + + ret = ext4l_read_block(block, size, bh->b_data); + if (ret) { + free_buffer_head(bh); + return NULL; + } + + /* Mark buffer as up-to-date */ + set_bit(BH_Uptodate, &bh->b_state); + + return bh; +} + +/** + * submit_bh() - Submit a buffer_head for I/O + * @op: Operation (REQ_OP_READ, REQ_OP_WRITE, etc.) + * @bh: Buffer head to submit + */ +void submit_bh(int op, struct buffer_head *bh) +{ + int ret; + int op_type = op & 0xff; /* Mask out flags, keep operation type */ + + if (op_type == REQ_OP_READ) { + ret = ext4l_read_block(bh->b_blocknr, bh->b_size, bh->b_data); + if (ret) { + clear_buffer_uptodate(bh); + return; + } + set_buffer_uptodate(bh); + } else if (op_type == REQ_OP_WRITE) { + /* Write support not implemented yet */ + clear_buffer_uptodate(bh); + } +} + +/** + * bh_read() - Read a buffer_head from disk + * @bh: Buffer head to read + * @flags: Read flags + * Return: 0 on success, negative on error + */ +int bh_read(struct buffer_head *bh, int flags) +{ + if (!bh || !bh->b_data) + return -EINVAL; + + submit_bh(REQ_OP_READ | flags, bh); + return buffer_uptodate(bh) ? 0 : -EIO; +} diff --git a/include/linux/buffer_head.h b/include/linux/buffer_head.h index b7596a74108..0f8f5b6caf1 100644 --- a/include/linux/buffer_head.h +++ b/include/linux/buffer_head.h @@ -40,6 +40,8 @@ enum bh_state_bits { BH_PrivateStart,/* not a state bit, but the first bit available * for private allocation by other entities */ + /* U-Boot specific: marks buffer owns b_data and should free it */ + BH_OwnsData = BH_PrivateStart, }; #define MAX_BUF_PER_PAGE (PAGE_SIZE / 512) @@ -176,8 +178,8 @@ static inline void put_bh(struct buffer_head *bh) atomic_dec(&bh->b_count); } -/* Stubs for U-Boot */ -#define brelse(bh) do { if (bh) put_bh(bh); } while (0) -#define __brelse(bh) do { put_bh(bh); } while (0) +/* Buffer release functions - implemented in ext4l/interface.c */ +void brelse(struct buffer_head *bh); +void __brelse(struct buffer_head *bh); #endif /* _LINUX_BUFFER_HEAD_H */ -- 2.43.0