summaryrefslogtreecommitdiff
path: root/fs/btrfs/relocation.c
diff options
context:
space:
mode:
authorMark Harmstone <mark@harmstone.com>2026-01-07 14:09:10 +0000
committerDavid Sterba <dsterba@suse.com>2026-02-03 07:54:35 +0100
commit979e1dc3d69e4c825eec05d05d9567b251f6ec23 (patch)
tree0cc0e7fae6a08faf681dd94d550d567fe2fed8c5 /fs/btrfs/relocation.c
parent18ba649928711539dd124b4bf7682696b3f2e4a8 (diff)
downloadlinux-next-979e1dc3d69e4c825eec05d05d9567b251f6ec23.tar.gz
linux-next-979e1dc3d69e4c825eec05d05d9567b251f6ec23.zip
btrfs: handle deletions from remapped block group
Handle the case where we free an extent from a block group that has the REMAPPED flag set. Because the remap tree is orthogonal to the extent tree, for data this may be within any number of identity remaps or actual remaps. If we're freeing a metadata node, this will be wholly inside one or the other. btrfs_remove_extent_from_remap_tree() searches the remap tree for the remaps that cover the range in question, then calls remove_range_from_remap_tree() for each one, to punch a hole in the remap and adjust the free-space tree. For an identity remap, remove_range_from_remap_tree() will adjust the block group's `identity_remap_count` if this changes. If it reaches zero we mark the block group as fully remapped. For an identity remap, remove_range_from_remap_tree() will adjust the block group's `identity_remap_count` if this changes. If it reaches zero we mark the block group as fully remapped. Fully remapped block groups have their chunk stripes removed and their device extents freed, which makes the disk space available again to the chunk allocator. This happens asynchronously: in the cleaner thread for sync discard and nodiscard, and (in a later patch) in the discard worker for async discard. Reviewed-by: Boris Burkov <boris@bur.io> Signed-off-by: Mark Harmstone <mark@harmstone.com> Signed-off-by: David Sterba <dsterba@suse.com>
Diffstat (limited to 'fs/btrfs/relocation.c')
-rw-r--r--fs/btrfs/relocation.c429
1 files changed, 429 insertions, 0 deletions
diff --git a/fs/btrfs/relocation.c b/fs/btrfs/relocation.c
index 6de508323dbd..e0558b2cd0b4 100644
--- a/fs/btrfs/relocation.c
+++ b/fs/btrfs/relocation.c
@@ -37,6 +37,7 @@
#include "super.h"
#include "tree-checker.h"
#include "raid-stripe-tree.h"
+#include "free-space-tree.h"
/*
* Relocation overview
@@ -3859,6 +3860,177 @@ static const char *stage_to_string(enum reloc_stage stage)
return "unknown";
}
+static void adjust_block_group_remap_bytes(struct btrfs_trans_handle *trans,
+ struct btrfs_block_group *bg, s64 diff)
+{
+ struct btrfs_fs_info *fs_info = trans->fs_info;
+ bool bg_already_dirty = true;
+ bool mark_unused = false;
+
+ spin_lock(&bg->lock);
+ bg->remap_bytes += diff;
+ if (bg->used == 0 && bg->remap_bytes == 0)
+ mark_unused = true;
+ spin_unlock(&bg->lock);
+
+ if (mark_unused)
+ btrfs_mark_bg_unused(bg);
+
+ spin_lock(&trans->transaction->dirty_bgs_lock);
+ if (list_empty(&bg->dirty_list)) {
+ list_add_tail(&bg->dirty_list, &trans->transaction->dirty_bgs);
+ bg_already_dirty = false;
+ btrfs_get_block_group(bg);
+ }
+ spin_unlock(&trans->transaction->dirty_bgs_lock);
+
+ /* Modified block groups are accounted for in the delayed_refs_rsv. */
+ if (!bg_already_dirty)
+ btrfs_inc_delayed_refs_rsv_bg_updates(fs_info);
+}
+
+static int remove_chunk_stripes(struct btrfs_trans_handle *trans,
+ struct btrfs_chunk_map *chunk_map,
+ struct btrfs_path *path)
+{
+ struct btrfs_fs_info *fs_info = trans->fs_info;
+ struct btrfs_key key;
+ struct extent_buffer *leaf;
+ struct btrfs_chunk *chunk;
+ int ret;
+
+ key.objectid = BTRFS_FIRST_CHUNK_TREE_OBJECTID;
+ key.type = BTRFS_CHUNK_ITEM_KEY;
+ key.offset = chunk_map->start;
+
+ btrfs_reserve_chunk_metadata(trans, false);
+
+ ret = btrfs_search_slot(trans, fs_info->chunk_root, &key, path, 0, 1);
+ if (ret) {
+ if (ret == 1) {
+ btrfs_release_path(path);
+ ret = -ENOENT;
+ }
+ btrfs_trans_release_chunk_metadata(trans);
+ return ret;
+ }
+
+ leaf = path->nodes[0];
+
+ chunk = btrfs_item_ptr(leaf, path->slots[0], struct btrfs_chunk);
+ btrfs_set_chunk_num_stripes(leaf, chunk, 0);
+ btrfs_set_chunk_sub_stripes(leaf, chunk, 0);
+
+ btrfs_truncate_item(trans, path, offsetof(struct btrfs_chunk, stripe), 1);
+
+ btrfs_mark_buffer_dirty(trans, leaf);
+
+ btrfs_release_path(path);
+ btrfs_trans_release_chunk_metadata(trans);
+
+ return 0;
+}
+
+int btrfs_last_identity_remap_gone(struct btrfs_chunk_map *chunk_map,
+ struct btrfs_block_group *bg)
+{
+ struct btrfs_fs_info *fs_info = bg->fs_info;
+ struct btrfs_trans_handle *trans;
+ int ret;
+ unsigned int num_items;
+ BTRFS_PATH_AUTO_FREE(path);
+
+ path = btrfs_alloc_path();
+ if (!path)
+ return -ENOMEM;
+
+ /*
+ * One item for each entry we're removing in the dev extents tree, and
+ * another for each device. DUP chunks are all on one device,
+ * everything else has one device per stripe.
+ */
+ if (bg->flags & BTRFS_BLOCK_GROUP_DUP)
+ num_items = chunk_map->num_stripes + 1;
+ else
+ num_items = 2 * chunk_map->num_stripes;
+
+ trans = btrfs_start_transaction_fallback_global_rsv(fs_info->tree_root, num_items);
+ if (IS_ERR(trans))
+ return PTR_ERR(trans);
+
+ ret = btrfs_remove_dev_extents(trans, chunk_map);
+ if (unlikely(ret)) {
+ btrfs_abort_transaction(trans, ret);
+ return ret;
+ }
+
+ mutex_lock(&trans->fs_info->chunk_mutex);
+ for (unsigned int i = 0; i < chunk_map->num_stripes; i++) {
+ ret = btrfs_update_device(trans, chunk_map->stripes[i].dev);
+ if (unlikely(ret)) {
+ mutex_unlock(&trans->fs_info->chunk_mutex);
+ btrfs_abort_transaction(trans, ret);
+ return ret;
+ }
+ }
+ mutex_unlock(&trans->fs_info->chunk_mutex);
+
+ write_lock(&trans->fs_info->mapping_tree_lock);
+ btrfs_chunk_map_device_clear_bits(chunk_map, CHUNK_ALLOCATED);
+ write_unlock(&trans->fs_info->mapping_tree_lock);
+
+ btrfs_remove_bg_from_sinfo(bg);
+
+ ret = remove_chunk_stripes(trans, chunk_map, path);
+ if (unlikely(ret)) {
+ btrfs_abort_transaction(trans, ret);
+ return ret;
+ }
+
+ ret = btrfs_commit_transaction(trans);
+ if (ret)
+ return ret;
+
+ return 0;
+}
+
+static void adjust_identity_remap_count(struct btrfs_trans_handle *trans,
+ struct btrfs_block_group *bg, int delta)
+{
+ struct btrfs_fs_info *fs_info = trans->fs_info;
+ bool bg_already_dirty = true;
+ bool mark_fully_remapped = false;
+
+ WARN_ON(delta < 0 && -delta > bg->identity_remap_count);
+
+ spin_lock(&bg->lock);
+
+ bg->identity_remap_count += delta;
+
+ if (bg->identity_remap_count == 0 &&
+ !test_bit(BLOCK_GROUP_FLAG_FULLY_REMAPPED, &bg->runtime_flags)) {
+ set_bit(BLOCK_GROUP_FLAG_FULLY_REMAPPED, &bg->runtime_flags);
+ mark_fully_remapped = true;
+ }
+
+ spin_unlock(&bg->lock);
+
+ spin_lock(&trans->transaction->dirty_bgs_lock);
+ if (list_empty(&bg->dirty_list)) {
+ list_add_tail(&bg->dirty_list, &trans->transaction->dirty_bgs);
+ bg_already_dirty = false;
+ btrfs_get_block_group(bg);
+ }
+ spin_unlock(&trans->transaction->dirty_bgs_lock);
+
+ /* Modified block groups are accounted for in the delayed_refs_rsv. */
+ if (!bg_already_dirty)
+ btrfs_inc_delayed_refs_rsv_bg_updates(fs_info);
+
+ if (mark_fully_remapped)
+ btrfs_mark_bg_fully_remapped(bg, trans);
+}
+
int btrfs_translate_remap(struct btrfs_fs_info *fs_info, u64 *logical, u64 *length)
{
int ret;
@@ -4463,3 +4635,260 @@ u64 btrfs_get_reloc_bg_bytenr(const struct btrfs_fs_info *fs_info)
logical = fs_info->reloc_ctl->block_group->start;
return logical;
}
+
+static int insert_remap_item(struct btrfs_trans_handle *trans, struct btrfs_path *path,
+ u64 old_addr, u64 length, u64 new_addr)
+{
+ int ret;
+ struct btrfs_fs_info *fs_info = trans->fs_info;
+ struct btrfs_key key;
+ struct btrfs_remap_item remap = { 0 };
+
+ if (old_addr == new_addr) {
+ /* Add new identity remap item. */
+ key.objectid = old_addr;
+ key.type = BTRFS_IDENTITY_REMAP_KEY;
+ key.offset = length;
+
+ ret = btrfs_insert_empty_item(trans, fs_info->remap_root, path,
+ &key, 0);
+ if (ret)
+ return ret;
+ } else {
+ /* Add new remap item. */
+ key.objectid = old_addr;
+ key.type = BTRFS_REMAP_KEY;
+ key.offset = length;
+
+ ret = btrfs_insert_empty_item(trans, fs_info->remap_root,
+ path, &key, sizeof(struct btrfs_remap_item));
+ if (ret)
+ return ret;
+
+ btrfs_set_stack_remap_address(&remap, new_addr);
+
+ write_extent_buffer(path->nodes[0], &remap,
+ btrfs_item_ptr_offset(path->nodes[0], path->slots[0]),
+ sizeof(struct btrfs_remap_item));
+
+ btrfs_release_path(path);
+
+ /* Add new backref item. */
+ key.objectid = new_addr;
+ key.type = BTRFS_REMAP_BACKREF_KEY;
+ key.offset = length;
+
+ ret = btrfs_insert_empty_item(trans, fs_info->remap_root,
+ path, &key,
+ sizeof(struct btrfs_remap_item));
+ if (ret)
+ return ret;
+
+ btrfs_set_stack_remap_address(&remap, old_addr);
+
+ write_extent_buffer(path->nodes[0], &remap,
+ btrfs_item_ptr_offset(path->nodes[0], path->slots[0]),
+ sizeof(struct btrfs_remap_item));
+ }
+
+ btrfs_release_path(path);
+
+ return 0;
+}
+
+/*
+ * Punch a hole in the remap item or identity remap item pointed to by path,
+ * for the range [hole_start, hole_start + hole_length).
+ */
+static int remove_range_from_remap_tree(struct btrfs_trans_handle *trans,
+ struct btrfs_path *path,
+ struct btrfs_block_group *bg,
+ u64 hole_start, u64 hole_length)
+{
+ int ret;
+ struct btrfs_fs_info *fs_info = trans->fs_info;
+ struct extent_buffer *leaf = path->nodes[0];
+ struct btrfs_key key;
+ u64 hole_end, new_addr, remap_start, remap_length, remap_end;
+ u64 overlap_length;
+ bool is_identity_remap;
+ int identity_count_delta = 0;
+
+ hole_end = hole_start + hole_length;
+
+ btrfs_item_key_to_cpu(leaf, &key, path->slots[0]);
+
+ is_identity_remap = (key.type == BTRFS_IDENTITY_REMAP_KEY);
+
+ remap_start = key.objectid;
+ remap_length = key.offset;
+ remap_end = remap_start + remap_length;
+
+ if (is_identity_remap) {
+ new_addr = remap_start;
+ } else {
+ struct btrfs_remap_item *remap_ptr;
+
+ remap_ptr = btrfs_item_ptr(leaf, path->slots[0], struct btrfs_remap_item);
+ new_addr = btrfs_remap_address(leaf, remap_ptr);
+ }
+
+ /* Delete old item. */
+ ret = btrfs_del_item(trans, fs_info->remap_root, path);
+ btrfs_release_path(path);
+ if (ret)
+ return ret;
+
+ if (is_identity_remap) {
+ identity_count_delta = -1;
+ } else {
+ /* Remove backref. */
+ key.objectid = new_addr;
+ key.type = BTRFS_REMAP_BACKREF_KEY;
+ key.offset = remap_length;
+
+ ret = btrfs_search_slot(trans, fs_info->remap_root, &key, path, -1, 1);
+ if (ret) {
+ if (ret == 1) {
+ btrfs_release_path(path);
+ ret = -ENOENT;
+ }
+ return ret;
+ }
+
+ ret = btrfs_del_item(trans, fs_info->remap_root, path);
+
+ btrfs_release_path(path);
+
+ if (ret)
+ return ret;
+ }
+
+ /* If hole_start > remap_start, re-add the start of the remap item. */
+ if (hole_start > remap_start) {
+ ret = insert_remap_item(trans, path, remap_start,
+ hole_start - remap_start, new_addr);
+ if (ret)
+ return ret;
+
+ if (is_identity_remap)
+ identity_count_delta++;
+ }
+
+ /* If hole_end < remap_end, re-add the end of the remap item. */
+ if (hole_end < remap_end) {
+ ret = insert_remap_item(trans, path, hole_end,
+ remap_end - hole_end,
+ hole_end - remap_start + new_addr);
+ if (ret)
+ return ret;
+
+ if (is_identity_remap)
+ identity_count_delta++;
+ }
+
+ if (identity_count_delta != 0)
+ adjust_identity_remap_count(trans, bg, identity_count_delta);
+
+ overlap_length = min_t(u64, hole_end, remap_end) -
+ max_t(u64, hole_start, remap_start);
+
+ if (!is_identity_remap) {
+ struct btrfs_block_group *dest_bg;
+
+ dest_bg = btrfs_lookup_block_group(fs_info, new_addr);
+ adjust_block_group_remap_bytes(trans, dest_bg, -overlap_length);
+ btrfs_put_block_group(dest_bg);
+ ret = btrfs_add_to_free_space_tree(trans,
+ hole_start - remap_start + new_addr,
+ overlap_length);
+ if (ret)
+ return ret;
+ }
+
+ ret = overlap_length;
+
+ return ret;
+}
+
+/*
+ * Return 1 if remove_range_from_remap_tree() has been called successfully,
+ * 0 if block group wasn't remapped, and a negative number on error.
+ */
+int btrfs_remove_extent_from_remap_tree(struct btrfs_trans_handle *trans,
+ struct btrfs_path *path,
+ u64 bytenr, u64 num_bytes)
+{
+ struct btrfs_fs_info *fs_info = trans->fs_info;
+ struct btrfs_key key, found_key;
+ struct extent_buffer *leaf;
+ struct btrfs_block_group *bg;
+ int ret, length;
+
+ if (!(btrfs_super_incompat_flags(fs_info->super_copy) &
+ BTRFS_FEATURE_INCOMPAT_REMAP_TREE))
+ return 0;
+
+ bg = btrfs_lookup_block_group(fs_info, bytenr);
+ if (!bg)
+ return 0;
+
+ mutex_lock(&fs_info->remap_mutex);
+
+ if (!(bg->flags & BTRFS_BLOCK_GROUP_REMAPPED)) {
+ mutex_unlock(&fs_info->remap_mutex);
+ btrfs_put_block_group(bg);
+ return 0;
+ }
+
+ do {
+ key.objectid = bytenr;
+ key.type = (u8)-1;
+ key.offset = (u64)-1;
+
+ ret = btrfs_search_slot(trans, fs_info->remap_root, &key, path, -1, 1);
+ if (ret < 0)
+ goto end;
+
+ leaf = path->nodes[0];
+ if (path->slots[0] == 0) {
+ ret = -ENOENT;
+ goto end;
+ }
+
+ path->slots[0]--;
+
+ btrfs_item_key_to_cpu(leaf, &found_key, path->slots[0]);
+
+ if (found_key.type != BTRFS_IDENTITY_REMAP_KEY &&
+ found_key.type != BTRFS_REMAP_KEY) {
+ ret = -ENOENT;
+ goto end;
+ }
+
+ if (bytenr < found_key.objectid ||
+ bytenr >= found_key.objectid + found_key.offset) {
+ ret = -ENOENT;
+ goto end;
+ }
+
+ length = remove_range_from_remap_tree(trans, path, bg, bytenr, num_bytes);
+ if (length < 0) {
+ ret = length;
+ goto end;
+ }
+
+ bytenr += length;
+ num_bytes -= length;
+ } while (num_bytes > 0);
+
+ ret = 1;
+
+end:
+ mutex_unlock(&fs_info->remap_mutex);
+
+ btrfs_put_block_group(bg);
+ btrfs_release_path(path);
+
+ return ret;
+}