From: Zygo Blaxell Date: Sun, 6 Sep 2026 13:01:40 +0000 (-0400) Subject: btrfs: stripe_alloc: tell a dead tree block from a live one in the metadata RMW audit X-Git-Url: http://git.hungrycats.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=062244e10af738406ed953b01e1f460c52ce2cdc;p=linux btrfs: stripe_alloc: tell a dead tree block from a live one in the metadata RMW audit The audit reports any older-generation header under a read-modify-write as a write hole, and counted over six hundred per metadata fill test. Since runs close at every transaction and freed stripes are reclaimed whole, most such headers belong to blocks that died with their stripe: the stripe was claimed fully free, the dead blocks kept their headers, and a torn parity write over them damages nothing a reader can reach. Ask the committed extent tree, the witness a degraded read after a crash would consult, whether the block is still referenced. Count and report a live one as before (now with its bytenr and reference count), and count a dead one as meta_rmw_ghost without a warning. A sector the run has already re-allocated in the current transaction carries an extent item of the current generation for the new block while the old header is still on disk (the new write has not landed): that is not a live block either, so the item generation must match the header generation. Without that check every single-stripe run of the current transaction reported its dead predecessors as holes (90 of 135 warnings in one fill test; the other 45 were the raid5 system chunk). Assisted-by: Claude:claude-fable-5 --- diff --git a/fs/btrfs/fs.h b/fs/btrfs/fs.h index 10b6624895bf8..b25577bef4fbd 100644 --- a/fs/btrfs/fs.h +++ b/fs/btrfs/fs.h @@ -840,6 +840,8 @@ struct btrfs_fs_info { */ atomic64_t meta_rmw_cur; atomic64_t meta_rmw_old; + atomic64_t meta_rmw_ghost; /* older header, block dead in the + committed extent tree */ atomic64_t meta_rmw_free; atomic64_t congestion_short; } stripe_park_stats; diff --git a/fs/btrfs/raid56.c b/fs/btrfs/raid56.c index 59d5d04c02637..7ebeb08f77615 100644 --- a/fs/btrfs/raid56.c +++ b/fs/btrfs/raid56.c @@ -2875,6 +2875,66 @@ static bool rmw_peek_eb_generation(struct btrfs_raid_bio *rbio, u32 i, return found_bytenr == logical && !bad_fsid; } +/* + * Is @bytenr a tree block the committed extent tree still references? Read + * the commit root without locking, as the caching thread does, so this is + * safe from the raid56 worker with no transaction to join; a lookup that + * cannot answer counts the block as live. + */ +static bool rmw_block_referenced(struct btrfs_fs_info *fs_info, u64 bytenr, + u64 header_gen, u64 *refs, u64 *extent_gen) +{ + struct btrfs_root *extent_root = btrfs_extent_root(fs_info, bytenr); + struct btrfs_path *path; + struct btrfs_key key; + bool live = true; + int ret; + + *refs = 0; + *extent_gen = 0; + path = btrfs_alloc_path(); + if (!path) + return true; + path->search_commit_root = 1; + path->skip_locking = 1; + key.objectid = bytenr; + key.type = BTRFS_METADATA_ITEM_KEY; + key.offset = (u64)-1; + down_read(&fs_info->commit_root_sem); + ret = btrfs_search_slot(NULL, extent_root, &key, path, 0, 0); + if (ret > 0) + ret = btrfs_previous_extent_item(extent_root, path, bytenr); + if (ret == 0) { + btrfs_item_key_to_cpu(path->nodes[0], &key, path->slots[0]); + if (key.objectid == bytenr && + (key.type == BTRFS_METADATA_ITEM_KEY || + key.type == BTRFS_EXTENT_ITEM_KEY)) { + struct btrfs_extent_item *ei; + + ei = btrfs_item_ptr(path->nodes[0], path->slots[0], + struct btrfs_extent_item); + *refs = btrfs_extent_refs(path->nodes[0], ei); + *extent_gen = btrfs_extent_generation(path->nodes[0], ei); + /* + * An extent item at this bytenr from a LATER + * transaction than the header on disk belongs to a + * new block allocated here whose write has not landed + * yet; the header is the dead previous occupant. + * Only an item of the header's own generation makes + * the block on disk live. + */ + live = (*refs != 0 && *extent_gen == header_gen); + } else { + live = false; + } + } else if (ret > 0) { + live = false; + } + up_read(&fs_info->commit_root_sem); + btrfs_free_path(path); + return live; +} + /* * Decide whether an uncovered read-modify-write is a write hole or only a * cost, by looking at what is actually in the stripe. @@ -2951,10 +3011,36 @@ static void audit_uncovered_rmw(struct btrfs_raid_bio *rbio) atomic64_inc(&fs_info->stripe_park_stats.meta_rmw_cur); continue; } - atomic64_inc(&fs_info->stripe_park_stats.meta_rmw_old); - btrfs_warn_rl(fs_info, -"read-modify-write of stripe %llu rewrites parity over committed tree block at generation %llu while writing generation %llu: write hole", - rbio->bioc->full_stripe_logical, gen, write_gen); + /* + * An older header is only a write hole if the block is still + * referenced in the committed extent tree: a stripe reclaimed + * after every block in it died is claimed fully free and its + * dead blocks keep their headers, and a torn write over those + * damages nothing a reader can reach. The committed tree is + * the witness a degraded read after a crash would consult, so + * ask it (no transaction: delayed refs of the running one are + * not yet what a crash would see). + */ + { + const int stripe_nr = i / rbio->stripe_nsectors; + const int sectornr = i % rbio->stripe_nsectors; + const u64 logical = rbio->bioc->full_stripe_logical + + (u64)stripe_nr * BTRFS_STRIPE_LEN + + (u64)sectornr * fs_info->sectorsize; + u64 refs; + u64 egen; + + if (!rmw_block_referenced(fs_info, logical, gen, &refs, + &egen)) { + atomic64_inc(&fs_info->stripe_park_stats.meta_rmw_ghost); + continue; + } + atomic64_inc(&fs_info->stripe_park_stats.meta_rmw_old); + btrfs_warn_rl(fs_info, +"read-modify-write of stripe %llu rewrites parity over committed tree block %llu at generation %llu (refs %llu, extent generation %llu) while writing generation %llu: write hole", + rbio->bioc->full_stripe_logical, logical, + gen, refs, egen, write_gen); + } } } diff --git a/fs/btrfs/sysfs.c b/fs/btrfs/sysfs.c index a769aeb26c8ee..3ce0fe46640b4 100644 --- a/fs/btrfs/sysfs.c +++ b/fs/btrfs/sysfs.c @@ -1250,6 +1250,7 @@ static ssize_t btrfs_stripe_park_stats_show(struct kobject *kobj, "meta_rmw %lld\n" "meta_rmw_cur %lld\n" "meta_rmw_old %lld\n" + "meta_rmw_ghost %lld\n" "meta_rmw_free %lld\n" "congestion_short %lld\n" "parked_now %d\n", @@ -1270,6 +1271,7 @@ static ssize_t btrfs_stripe_park_stats_show(struct kobject *kobj, atomic64_read(&fs_info->stripe_park_stats.meta_rmw), atomic64_read(&fs_info->stripe_park_stats.meta_rmw_cur), atomic64_read(&fs_info->stripe_park_stats.meta_rmw_old), + atomic64_read(&fs_info->stripe_park_stats.meta_rmw_ghost), atomic64_read(&fs_info->stripe_park_stats.meta_rmw_free), atomic64_read(&fs_info->stripe_park_stats.congestion_short), atomic_read(&fs_info->stripe_parked_now));