]> git.hungrycats.org Git - linux/commitdiff
btrfs: add BTRFS_LOGICAL_INO_ARGS_COMMIT_ROOT to resolve against the commit roots 6.18/topics/logical-ino
authorZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Sat, 19 Sep 2026 16:31:42 +0000 (12:31 -0400)
committerZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Sat, 19 Sep 2026 16:31:42 +0000 (12:31 -0400)
The LOGICAL_INO ioctl is the only caller of iterate_extent_inodes() that
walks the live roots.  Doing so attaches to the running transaction and
holds a tree mod log sequence for the whole walk, and while any sequence
is live every node-level tree change in the filesystem is recorded in the
tree mod log.  The log is trimmed only below the oldest live sequence and
has no shrinker, so its size is the modification rate times the lifetime
of the slowest walker.

That product has no bound.  A walk over an extent shared by 32047
snapshots takes most of a second on its own, and when the walker then has
to wait for the commit thread -- on the extent root, or on the mutex of a
delayed ref head with tens of thousands of pending refs -- it holds its
sequence for as long as the wait lasts.  Meanwhile relocation of a block
group shared by all those snapshots COWs a path in every reloc tree for
every extent, and each root COW logs one element per key of the old root.
On a 16 GiB machine running a dedupe daemon that issues LOGICAL_INO
continuously, unreclaimable slab reached 13.6 GiB about an hour into such
a relocation, with four ioctl callers still holding their sequences behind
the commit thread; every allocation then went to direct reclaim with tree
locks held and the machine stopped responding.

Add a v2 flag, alongside BTRFS_LOGICAL_INO_ARGS_IGNORE_OFFSET, that makes
the walk use the commit roots, as scrub and send already do through the
same function.  The answer lags the live tree by at most one transaction,
which a caller of this ioctl cannot distinguish from the live tree changing
after the call returns, and the walk no longer takes a sequence, logs
nothing, and does not contend with the commit thread for tree locks or
delayed ref heads.  Callers that want the live view keep it by default.

Assisted-by: Claude:claude-fable-5-1
fs/btrfs/backref.c
fs/btrfs/backref.h
fs/btrfs/ioctl.c
include/uapi/linux/btrfs.h

index 0c47b4570a7bf8b1214b401c19148409bee7f7f4..3e8ce1b20bc4d8ce7f1171b66826ca274e35c4d1 100644 (file)
@@ -2545,7 +2545,8 @@ static int build_ino_list(u64 inum, u64 offset, u64 num_bytes, u64 root, void *c
 }
 
 int iterate_inodes_from_logical(u64 logical, struct btrfs_fs_info *fs_info,
-                               void *ctx, bool ignore_offset)
+                               void *ctx, bool ignore_offset,
+                               bool search_commit_root)
 {
        struct btrfs_backref_walk_ctx walk_ctx = { 0 };
        int ret;
@@ -2571,7 +2572,18 @@ int iterate_inodes_from_logical(u64 logical, struct btrfs_fs_info *fs_info,
                walk_ctx.extent_item_pos = logical - found_key.objectid;
        walk_ctx.fs_info = fs_info;
 
-       return iterate_extent_inodes(&walk_ctx, false, build_ino_list, ctx);
+       /*
+        * A live-root walk attaches to the running transaction and holds a
+        * tree mod log sequence until it is done, and while any sequence is
+        * live every node-level tree change in the filesystem is logged, with
+        * nothing freeing the log below the oldest sequence.  A caller that
+        * resolves extents shared by tens of thousands of snapshots, on a
+        * filesystem busy relocating them, can hold that sequence for as long
+        * as it waits behind the commit thread; the log then grows without
+        * bound.  BTRFS_LOGICAL_INO_ARGS_COMMIT_ROOT lets such a caller take
+        * the commit-root walk scrub and send already use here instead.
+        */
+       return iterate_extent_inodes(&walk_ctx, search_commit_root, build_ino_list, ctx);
 }
 
 static int inode_to_path(u64 inum, u32 name_len, unsigned long name_off,
index 25d51c2460703b042ef6f19ee93275daa41f24a2..85ab5ff1d8ad03fbd64a22fa4e6cc232ed212cbc 100644 (file)
@@ -226,7 +226,8 @@ int iterate_extent_inodes(struct btrfs_backref_walk_ctx *ctx,
                          iterate_extent_inodes_t *iterate, void *user_ctx);
 
 int iterate_inodes_from_logical(u64 logical, struct btrfs_fs_info *fs_info,
-                               void *ctx, bool ignore_offset);
+                               void *ctx, bool ignore_offset,
+                               bool search_commit_root);
 
 int paths_from_inode(u64 inum, struct inode_fs_paths *ipath);
 
index b702f625c753bc6d0ba54c4b4aa3c43f395b3eca..00baf2bb44a5b19ef522913f25ba1a1aa93e19ab 100644 (file)
@@ -3345,6 +3345,7 @@ static long btrfs_ioctl_logical_to_ino(struct btrfs_fs_info *fs_info,
        struct btrfs_ioctl_logical_ino_args *loi;
        struct btrfs_data_container *inodes = NULL;
        bool ignore_offset;
+       bool commit_root = false;
 
        if (!capable(CAP_SYS_ADMIN))
                return -EPERM;
@@ -3363,11 +3364,13 @@ static long btrfs_ioctl_logical_to_ino(struct btrfs_fs_info *fs_info,
                        goto out_loi;
                }
                /* Only accept flags we have defined so far */
-               if (loi->flags & ~(BTRFS_LOGICAL_INO_ARGS_IGNORE_OFFSET)) {
+               if (loi->flags & ~(BTRFS_LOGICAL_INO_ARGS_IGNORE_OFFSET |
+                                  BTRFS_LOGICAL_INO_ARGS_COMMIT_ROOT)) {
                        ret = -EINVAL;
                        goto out_loi;
                }
                ignore_offset = loi->flags & BTRFS_LOGICAL_INO_ARGS_IGNORE_OFFSET;
+               commit_root = loi->flags & BTRFS_LOGICAL_INO_ARGS_COMMIT_ROOT;
                size = min_t(u32, loi->size, SZ_16M);
        }
 
@@ -3377,7 +3380,8 @@ static long btrfs_ioctl_logical_to_ino(struct btrfs_fs_info *fs_info,
                goto out_loi;
        }
 
-       ret = iterate_inodes_from_logical(loi->logical, fs_info, inodes, ignore_offset);
+       ret = iterate_inodes_from_logical(loi->logical, fs_info, inodes,
+                                         ignore_offset, commit_root);
        if (ret == -EINVAL)
                ret = -ENOENT;
        if (ret < 0)
index fb89c5c37eb045f3c8b726db13ca13bac5eb6e29..2ebe95333629ce41e052fd9153c60b4ccbda72ad 100644 (file)
@@ -730,6 +730,15 @@ struct btrfs_ioctl_logical_ino_args {
  * Requires logical == extent bytenr.
  */
 #define BTRFS_LOGICAL_INO_ARGS_IGNORE_OFFSET   (1ULL << 0)
+/*
+ * Resolve against the commit roots: the answer is as of the last committed
+ * transaction.  The walk then takes no tree mod log sequence and does not
+ * contend with the running transaction for tree locks or delayed ref heads,
+ * which matters to a caller resolving extents shared by many snapshots while
+ * the filesystem is busy -- the live-root walk pins every node-level tree
+ * change made in the meantime in memory until it finishes.
+ */
+#define BTRFS_LOGICAL_INO_ARGS_COMMIT_ROOT     (1ULL << 1)
 
 enum btrfs_dev_stat_values {
        /* disk I/O failure stats */