From b16b35e074138dfca71145468050d65a7949b912 Mon Sep 17 00:00:00 2001 From: David Sterba Date: Mon, 28 Nov 2022 21:13:21 +0100 Subject: [PATCH] btrfs-progs: inspect: new command map-swapfile Verify if a given file is suitable for a swapfile and print the physical offset (ie. the ultimate on-device physical offset), and the resume offset value (physical / page size). This can be the kernel parameter or written to /sys/power/resume_offset before hibernation. Option -r or --resume-offset prints just the value. Copied and simplified from Omar Sandoval's tool to print extents: https://github.com/osandov/osandov-linux/blob/master/scripts/btrfs_map_physical.c Issue: #544 Issue: #533 Signed-off-by: David Sterba --- btrfs-completion | 2 +- cmds/inspect.c | 414 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 415 insertions(+), 1 deletion(-) diff --git a/btrfs-completion b/btrfs-completion index 3c0165bb..af9530a5 100644 --- a/btrfs-completion +++ b/btrfs-completion @@ -29,7 +29,7 @@ _btrfs() commands_device='scan add delete remove ready stats usage' commands_scrub='start cancel resume status' commands_rescue='chunk-recover super-recover zero-log create-control-device' - commands_inspect_internal='inode-resolve logical-resolve subvolid-resolve rootid min-dev-size dump-tree dump-super tree-stats' + commands_inspect_internal='inode-resolve logical-resolve subvolid-resolve rootid min-dev-size dump-tree dump-super tree-stats map-swapfile' commands_property='get set list' commands_quota='enable disable rescan' commands_qgroup='assign remove create destroy show limit clear-stale' diff --git a/cmds/inspect.c b/cmds/inspect.c index d7e2aa48..5e327e74 100644 --- a/cmds/inspect.c +++ b/cmds/inspect.c @@ -16,6 +16,11 @@ #include "kerncompat.h" #include +#include +#include +#include +#include +#include #include #include #include @@ -23,6 +28,7 @@ #include #include #include +#include #include "kernel-lib/list.h" #include "kernel-lib/sizes.h" #include "kernel-shared/ctree.h" @@ -1108,6 +1114,413 @@ out_nomem: } static DEFINE_SIMPLE_COMMAND(inspect_list_chunks, "list-chunks"); +static const char * const cmd_inspect_map_swapfile_usage[] = { + "btrfs inspect-internal map-swapfile ", + "Print physical offset of first block and resume offset if file is suitable as swapfile", + "Print physical offset of first block and resume offset if file is suitable as swapfile.", + "All conditions of a swapfile extents are verified if they could pass kernel tests.", + "Use the value of resume offset for /sys/power/resume_offset, this depends on the", + "page size that's detected on this system.", + "", + "-r|--resume-offset print only the value of resume_offset", + NULL +}; + +struct stripe { + u64 devid; + u64 offset; +}; + +struct chunk { + u64 offset; + u64 length; + u64 stripe_len; + u64 type; + struct stripe *stripes; + size_t num_stripes; + size_t sub_stripes; +}; + +struct chunk_tree { + struct chunk *chunks; + size_t num_chunks; +}; + +static int read_chunk_tree(int fd, struct chunk **chunks, size_t *num_chunks) +{ + struct btrfs_ioctl_search_args search = { + .key = { + .tree_id = BTRFS_CHUNK_TREE_OBJECTID, + .min_objectid = BTRFS_FIRST_CHUNK_TREE_OBJECTID, + .min_type = BTRFS_CHUNK_ITEM_KEY, + .min_offset = 0, + .max_objectid = BTRFS_FIRST_CHUNK_TREE_OBJECTID, + .max_type = BTRFS_CHUNK_ITEM_KEY, + .max_offset = (u64)-1, + .min_transid = 0, + .max_transid = (u64)-1, + .nr_items = 0, + }, + }; + size_t items_pos = 0, buf_off = 0; + size_t capacity = 0; + int ret; + + *chunks = NULL; + *num_chunks = 0; + for (;;) { + const struct btrfs_ioctl_search_header *header; + const struct btrfs_chunk *item; + struct chunk *chunk; + size_t i; + + if (items_pos >= search.key.nr_items) { + search.key.nr_items = 4096; + ret = ioctl(fd, BTRFS_IOC_TREE_SEARCH, &search); + if (ret == -1) { + perror("BTRFS_IOC_TREE_SEARCH"); + return -1; + } + items_pos = 0; + buf_off = 0; + + if (search.key.nr_items == 0) + break; + } + + header = (struct btrfs_ioctl_search_header *)(search.buf + buf_off); + if (header->type != BTRFS_CHUNK_ITEM_KEY) + goto next; + + item = (void *)(header + 1); + if (*num_chunks >= capacity) { + struct chunk *tmp; + + if (capacity == 0) + capacity = 1; + else + capacity *= 2; + tmp = realloc(*chunks, capacity * sizeof(**chunks)); + if (!tmp) { + perror("realloc"); + return -1; + } + *chunks = tmp; + } + + chunk = &(*chunks)[*num_chunks]; + chunk->offset = header->offset; + chunk->length = le64_to_cpu(item->length); + chunk->stripe_len = le64_to_cpu(item->stripe_len); + chunk->type = le64_to_cpu(item->type); + chunk->num_stripes = le16_to_cpu(item->num_stripes); + chunk->sub_stripes = le16_to_cpu(item->sub_stripes); + chunk->stripes = calloc(chunk->num_stripes, + sizeof(*chunk->stripes)); + if (!chunk->stripes) { + perror("calloc"); + return -1; + } + (*num_chunks)++; + + for (i = 0; i < chunk->num_stripes; i++) { + const struct btrfs_stripe *stripe; + + stripe = &item->stripe + i; + chunk->stripes[i].devid = le64_to_cpu(stripe->devid); + chunk->stripes[i].offset = le64_to_cpu(stripe->offset); + } + +next: + items_pos++; + buf_off += sizeof(*header) + header->len; + if (header->offset == (u64)-1) + break; + else + search.key.min_offset = header->offset + 1; + } + return 0; +} + +static struct chunk *find_chunk(struct chunk *chunks, size_t num_chunks, u64 logical) +{ + size_t lo, hi; + + if (!num_chunks) + return NULL; + + lo = 0; + hi = num_chunks - 1; + while (lo <= hi) { + size_t mid = lo + (hi - lo) / 2; + + if (logical < chunks[mid].offset) + hi = mid - 1; + else if (logical >= chunks[mid].offset + chunks[mid].length) + lo = mid + 1; + else + return &chunks[mid]; + } + return NULL; +} + +static int map_physical_start(int fd, struct chunk *chunks, size_t num_chunks, + u64 *physical_start) +{ + struct btrfs_ioctl_search_args search = { + .key = { + .min_type = BTRFS_EXTENT_DATA_KEY, + .max_type = BTRFS_EXTENT_DATA_KEY, + .min_offset = 0, + .max_offset = (u64)-1, + .min_transid = 0, + .max_transid = (u64)-1, + .nr_items = 0, + }, + }; + struct btrfs_ioctl_ino_lookup_args args = { + .treeid = 0, + .objectid = BTRFS_FIRST_FREE_OBJECTID, + }; + size_t items_pos = 0, buf_off = 0; + struct stat st; + int ret; + u64 valid_devid = (u64)-1; + + *physical_start = (u64)-1; + + ret = fstat(fd, &st); + if (ret == -1) { + error("cannot fstat file: %m"); + return -errno; + } + if (!S_ISREG(st.st_mode)) { + error("not a regular file"); + return -EINVAL; + } + + ret = ioctl(fd, BTRFS_IOC_INO_LOOKUP, &args); + if (ret == -1) { + error("cannot lookup parent subvolume: %m"); + return -errno; + } + + search.key.tree_id = args.treeid; + search.key.min_objectid = search.key.max_objectid = st.st_ino; + for (;;) { + const struct btrfs_ioctl_search_header *header; + const struct btrfs_file_extent_item *item; + u8 type; + u64 logical_offset = 0; + struct chunk *chunk = NULL; + + if (items_pos >= search.key.nr_items) { + search.key.nr_items = 4096; + ret = ioctl(fd, BTRFS_IOC_TREE_SEARCH, &search); + if (ret == -1) { + error("cannot search tree: %m"); + return -errno; + } + items_pos = 0; + buf_off = 0; + + if (search.key.nr_items == 0) + break; + } + + header = (struct btrfs_ioctl_search_header *)(search.buf + buf_off); + if (header->type != BTRFS_EXTENT_DATA_KEY) + goto next; + + item = (void *)(header + 1); + + type = item->type; + if (type == BTRFS_FILE_EXTENT_REG || + type == BTRFS_FILE_EXTENT_PREALLOC) { + logical_offset = le64_to_cpu(item->disk_bytenr); + if (logical_offset) { + /* Regular extent */ + chunk = find_chunk(chunks, num_chunks, logical_offset); + if (!chunk) { + error("cannot find chunk containing %llu", + (unsigned long long)logical_offset); + return -ENOENT; + } + } else { + error("file with holes"); + ret = -EINVAL; + goto out; + } + } else { + if (type == BTRFS_FILE_EXTENT_INLINE) + error("file with inline extent"); + else + error("unknown extent type: %u", type); + ret = -EINVAL; + goto out; + } + + if (item->compression != 0) { + error("compressed extent: %u", item->compression); + ret = -EINVAL; + goto out; + } + if (item->encryption != 0) { + error("file with encryption: %u", item->encryption); + ret = -EINVAL; + goto out; + } + if (item->other_encoding != 0) { + error("file with other_encoding: %u", le16_to_cpu(item->other_encoding)); + ret = -EINVAL; + goto out; + } + + /* Only single profile */ + if ((chunk->type & BTRFS_BLOCK_GROUP_PROFILE_MASK) != 0) { + error("unsupported block group profile: %llu", + (unsigned long long)(chunk->type & BTRFS_BLOCK_GROUP_PROFILE_MASK)); + ret = -EINVAL; + goto out; + } + + if (valid_devid == (u64)-1) { + valid_devid = chunk->stripes[0].devid; + } else { + if (valid_devid != chunk->stripes[0].devid) { + error("file stored on multiple devices"); + break; + } + } + if (*physical_start == (u64)-1) { + u64 offset; + u64 stripe_nr; + u64 stripe_offset; + u64 stripe_index; + + offset = logical_offset - chunk->offset; + stripe_nr = offset / chunk->stripe_len; + stripe_offset = offset - stripe_nr * chunk->stripe_len; + + stripe_index = stripe_nr % chunk->num_stripes; + stripe_nr /= chunk->num_stripes; + + *physical_start = chunk->stripes[stripe_index].offset + + stripe_nr * chunk->stripe_len + stripe_offset; + } + +next: + items_pos++; + buf_off += sizeof(*header) + header->len; + if (header->offset == (u64)-1) + break; + else + search.key.min_offset = header->offset + 1; + } + return 0; + +out: + return ret; +} + +static int cmd_inspect_map_swapfile(const struct cmd_struct *cmd, + int argc, char **argv) +{ + int fd, ret; + struct chunk *chunks = NULL; + size_t num_chunks = 0, i; + struct statfs stfs; + long flags; + u64 physical_start; + long page_size = sysconf(_SC_PAGESIZE); + bool resume_offset = false; + + optind = 0; + while (1) { + static const struct option long_options[] = { + { "resume-offset", no_argument, NULL, 'r' }, + { NULL, 0, NULL, 0 } + }; + int c; + + c = getopt_long(argc, argv, "r", long_options, NULL); + if (c == -1) + break; + + switch (c) { + case 'r': + resume_offset = true; + break; + default: + usage_unknown_option(cmd, argv); + } + } + if (check_argc_exact(argc - optind, 1)) + return 1; + + fd = open(argv[optind], O_RDONLY); + if (fd == -1) { + error("cannot open %s: %m", argv[optind]); + ret = 1; + goto out; + } + + /* Quick checks before extent enumeration. */ + ret = fstatfs(fd, &stfs); + if (ret == -1) { + error("cannot statfs file: %m"); + ret = 1; + goto out; + } + if (stfs.f_type != BTRFS_SUPER_MAGIC) { + error("not a file on BTRFS"); + ret = 1; + goto out; + } + + ret = ioctl(fd, FS_IOC_GETFLAGS, &flags); + if (ret == -1) { + error("cannot verify file flags/attributes: %m"); + ret = 1; + goto out; + } + if (!(flags & FS_NOCOW_FL)) { + error("file is not NOCOW"); + ret = 1; + goto out; + } + if (flags & FS_COMPR_FL) { + error("file with COMP attribute"); + ret = 1; + goto out; + } + + ret = read_chunk_tree(fd, &chunks, &num_chunks); + if (ret == -1) + goto out; + + ret = map_physical_start(fd, chunks, num_chunks, &physical_start); + if (ret == 0) { + if (resume_offset) { + printf("%llu\n", physical_start / page_size); + } else { + pr_verbose(LOG_DEFAULT, "Physical start: %12llu\n", + physical_start); + pr_verbose(LOG_DEFAULT, "Resume offset: %12llu\n", + physical_start / page_size); + } + } + +out: + for (i = 0; i < num_chunks; i++) + free(chunks[i].stripes); + + free(chunks); + close(fd); + return !!ret; +} +static DEFINE_SIMPLE_COMMAND(inspect_map_swapfile, "map-swapfile"); + static const char inspect_cmd_group_info[] = "query various internal information"; @@ -1117,6 +1530,7 @@ static const struct cmd_group inspect_cmd_group = { &cmd_struct_inspect_logical_resolve, &cmd_struct_inspect_subvolid_resolve, &cmd_struct_inspect_rootid, + &cmd_struct_inspect_map_swapfile, &cmd_struct_inspect_min_dev_size, &cmd_struct_inspect_dump_tree, &cmd_struct_inspect_dump_super,