From: Aurelien DESBRIERES <aurelien@hackers.camp>
To: Aurelien DESBRIERES <aurelien@hackers.camp>,
Jonathan Corbet <corbet@lwn.net>,
Shuah Khan <skhan@linuxfoundation.org>,
Randy Dunlap <rdunlap@infradead.org>,
Alexander Viro <viro@zeniv.linux.org.uk>,
Christian Brauner <brauner@kernel.org>, Jan Kara <jack@suse.cz>
Cc: linux-fsdevel@vger.kernel.org, linux-kernel@vger.kernel.org,
linux-doc@vger.kernel.org, aurelien.desbrieres@gmail.com,
linux-api@vger.kernel.org
Subject: [RFC PATCH 15/18] beamfs: add scrubber and RS benchmark
Date: Mon, 19 Oct 2026 06:17:25 +0200 [thread overview]
Message-ID: <20261019041728.327785-16-aurelien@hackers.camp> (raw)
In-Reply-To: <20261019041728.327785-1-aurelien@hackers.camp>
beamfs corrects on read, so a block nobody reads accumulates upsets
until it is past the correction radius. scrub.c runs one kernel thread
per mounted volume, beamfs-scrub/<dev>, that walks the allocated
blocks at a bounded pace and decodes each one, so that drift is found
while it is still correctable. Its pace and counters are in
/sys/fs/beamfs/<dev>/, and the scrub_interval_ms module parameter sets
the pace for volumes mounted later.
rsbench.c times the codec alone: reading
/sys/kernel/debug/beamfs/rs_bench encodes and decodes in memory and
reports nanoseconds per block; rs_bench_iters sets the iteration
count.
Assisted-by: LLM
Signed-off-by: Aurelien DESBRIERES <aurelien@hackers.camp>
---
fs/beamfs/rsbench.c | 261 ++++++++
fs/beamfs/scrub.c | 1454 +++++++++++++++++++++++++++++++++++++++++++
2 files changed, 1715 insertions(+)
create mode 100644 fs/beamfs/rsbench.c
create mode 100644 fs/beamfs/scrub.c
diff --git a/fs/beamfs/rsbench.c b/fs/beamfs/rsbench.c
new file mode 100644
index 000000000000..c154dd04d498
--- /dev/null
+++ b/fs/beamfs/rsbench.c
@@ -0,0 +1,261 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * beamfs -- Reed-Solomon micro-benchmark
+ *
+ * Author: Aurelien DESBRIERES <aurelien@hackers.camp>
+ *
+ * What the codec costs, with no block layer underneath it.
+ *
+ * Every performance figure the harness produces so far is a
+ * filesystem figure: it contains the medium, the emulator and the
+ * codec together, and cannot say which of the three it is measuring.
+ * On the lab hardware that ambiguity is total -- beamfs writes at
+ * 1.8 MB/s on a USB stick and 1.5 MB/s on a SSD capable of 700, so
+ * the medium is plainly not the limit, but nothing said whether the
+ * remainder was TCG emulation or Reed-Solomon.
+ *
+ * This encodes and decodes in memory, in a loop, and reports
+ * nanoseconds per block. Subtracting it from the filesystem figure
+ * leaves everything else. A reviewer asking "how much of the overhead
+ * is the error correction" gets a number rather than an argument.
+ *
+ * Reading /sys/kernel/debug/beamfs/rs_bench runs the benchmark and
+ * returns the result; the iteration count is settable alongside it.
+ * Running on read is unusual for debugfs, and deliberate: a benchmark
+ * that needs a write to arm and a read to collect has a window in
+ * which two callers interleave, and this has no state to protect.
+ */
+
+#include <linux/fs.h>
+#include <linux/debugfs.h>
+#include <linux/slab.h>
+#include <linux/ktime.h>
+#include <linux/random.h>
+#include "beamfs.h"
+
+/* Blocks per run. Enough to swamp timer granularity under TCG, where
+ * one block takes tens of microseconds; small enough that the read
+ * returns while an operator is still watching.
+ */
+#define BEAMFS_RSBENCH_DEFAULT_ITERS 1000
+#define BEAMFS_RSBENCH_MAX_ITERS 1000000
+
+static struct dentry *beamfs_debugfs_root;
+static u32 beamfs_rsbench_iters = BEAMFS_RSBENCH_DEFAULT_ITERS;
+
+/*
+ * One encode and one decode over a block's payload: 3824 bytes, as
+ * BEAMFS_DATA_INLINE_SUBBLOCKS full-length RS(255,239) codewords.
+ *
+ * The decode runs on undamaged data. That is the case that matters:
+ * every read pays it, whether or not anything is wrong, because the
+ * syndromes have to be computed before the codec can say there is
+ * nothing to correct. Correction itself is rarer and costs more, but
+ * it is not what sets the steady-state read cost.
+ */
+static int beamfs_rsbench_run(u32 iters, u64 *enc_ns, u64 *dec_ns)
+{
+ u8 *data, *parity;
+ int *results;
+ u8 *positions;
+ ktime_t t0;
+ u32 i;
+ int ret = 0;
+
+ data = kvmalloc(BEAMFS_DATA_INLINE_BYTES, GFP_KERNEL);
+ parity = kvmalloc((size_t)BEAMFS_DATA_INLINE_SUBBLOCKS * BEAMFS_RS_PARITY,
+ GFP_KERNEL);
+ results = kcalloc(BEAMFS_DATA_INLINE_SUBBLOCKS, sizeof(*results),
+ GFP_KERNEL);
+ positions = kcalloc((size_t)BEAMFS_DATA_INLINE_SUBBLOCKS *
+ (BEAMFS_RS_PARITY / 2), sizeof(*positions),
+ GFP_KERNEL);
+ if (!data || !parity || !results || !positions) {
+ ret = -ENOMEM;
+ goto out;
+ }
+
+ get_random_bytes(data, BEAMFS_DATA_INLINE_BYTES);
+
+ t0 = ktime_get();
+ for (i = 0; i < iters; i++) {
+ ret = beamfs_rs_encode_region(data, BEAMFS_SUBBLOCK_DATA,
+ parity, BEAMFS_RS_PARITY,
+ BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_DATA_INLINE_SUBBLOCKS);
+ if (ret)
+ goto out;
+ cond_resched();
+ }
+ *enc_ns = ktime_to_ns(ktime_sub(ktime_get(), t0));
+
+ t0 = ktime_get();
+ for (i = 0; i < iters; i++) {
+ ret = beamfs_rs_decode_region(data, BEAMFS_SUBBLOCK_DATA,
+ parity, BEAMFS_RS_PARITY,
+ BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_DATA_INLINE_SUBBLOCKS,
+ results, positions,
+ BEAMFS_RS_PARITY / 2,
+ "bench");
+ if (ret < 0)
+ goto out;
+ cond_resched();
+ }
+ *dec_ns = ktime_to_ns(ktime_sub(ktime_get(), t0));
+ ret = 0;
+
+out:
+ kfree(positions);
+ kfree(results);
+ kvfree(parity);
+ kvfree(data);
+ return ret;
+}
+
+/*
+ * How long a burst each layout survives.
+ *
+ * Reported rather than asserted: a number in a file anyone can read
+ * beats a BUG_ON nobody triggers, and the answer is a property of the
+ * code, not of this volume.
+ */
+static void beamfs_rsbench_burst(struct seq_file *m)
+{
+ unsigned int subs = BEAMFS_DATA_INLINE_SUBBLOCKS;
+ size_t dlen = BEAMFS_SUBBLOCK_DATA;
+ u8 *blk, *par, *saved;
+ unsigned int burst;
+ unsigned int plain_limit = 0, woven_limit = 0;
+ size_t i;
+
+ blk = kmalloc(dlen * subs, GFP_KERNEL);
+ saved = kmalloc(dlen * subs, GFP_KERNEL);
+ par = kmalloc((size_t)subs * BEAMFS_RS_PARITY, GFP_KERNEL);
+ if (!blk || !saved || !par)
+ goto out;
+
+ for (i = 0; i < dlen * subs; i++)
+ saved[i] = (u8)(i * 31 + 7);
+
+ /*
+ * Walk the burst length up until each layout stops correcting.
+ * The first failure is the limit.
+ */
+ for (burst = 1; burst <= 160; burst++) {
+ int rc;
+
+ /* Contiguous. */
+ memcpy(blk, saved, dlen * subs);
+ beamfs_rs_encode_region(blk, dlen, par, BEAMFS_RS_PARITY,
+ dlen, subs);
+ for (i = 0; i < burst; i++)
+ blk[100 + i] ^= 0xff;
+ rc = beamfs_rs_decode_region(blk, dlen, par, BEAMFS_RS_PARITY,
+ dlen, subs, NULL, NULL, 0,
+ "rsbench");
+ if (rc >= 0 && plain_limit == burst - 1)
+ plain_limit = burst;
+
+ /* Interleaved. */
+ memcpy(blk, saved, dlen * subs);
+ beamfs_rs_encode_woven(blk, par, BEAMFS_RS_PARITY, dlen,
+ subs);
+ for (i = 0; i < burst; i++)
+ blk[100 + i] ^= 0xff;
+ rc = beamfs_rs_decode_woven(blk, par, BEAMFS_RS_PARITY, dlen,
+ subs, NULL, "rsbench");
+ if (rc >= 0 && woven_limit == burst - 1)
+ woven_limit = burst;
+ }
+
+ seq_printf(m, "BURST_BYTES_CONTIGUOUS=%u\n", plain_limit);
+ seq_printf(m, "BURST_BYTES_INTERLEAVED=%u\n", woven_limit);
+
+out:
+ kfree(par);
+ kfree(saved);
+ kfree(blk);
+}
+
+static int beamfs_rsbench_show(struct seq_file *m, void *v)
+{
+ u64 enc_ns = 0, dec_ns = 0;
+ u32 iters = READ_ONCE(beamfs_rsbench_iters);
+ u64 bytes;
+ int ret;
+
+ ret = beamfs_rsbench_run(iters, &enc_ns, &dec_ns);
+ if (ret) {
+ seq_printf(m, "error %d\n", ret);
+ return 0;
+ }
+
+ bytes = (u64)iters * BEAMFS_DATA_INLINE_BYTES;
+
+ /*
+ * Key=value, one per line: the harness parses this, and a
+ * format that needs a parser written for it is a format that
+ * will be parsed wrongly.
+ */
+ seq_printf(m, "ITERS=%u\n", iters);
+ seq_printf(m, "BLOCK_BYTES=%u\n", BEAMFS_DATA_INLINE_BYTES);
+ seq_printf(m, "SUBBLOCKS=%u\n", BEAMFS_DATA_INLINE_SUBBLOCKS);
+ seq_printf(m, "ENCODE_NS_TOTAL=%llu\n", enc_ns);
+ seq_printf(m, "DECODE_NS_TOTAL=%llu\n", dec_ns);
+ seq_printf(m, "ENCODE_NS_PER_BLOCK=%llu\n", enc_ns / iters);
+ seq_printf(m, "DECODE_NS_PER_BLOCK=%llu\n", dec_ns / iters);
+
+ /*
+ * And what a burst costs, either way.
+ *
+ * Nine consecutive bad bytes laid out contiguously all land in
+ * one codeword and it is lost; interleaved they land one per
+ * codeword and the block comes back whole. Asked here of the
+ * kernel's own codec, the one that decodes the volume.
+ */
+ beamfs_rsbench_burst(m);
+
+ /*
+ * Throughput in KiB/s, computed here rather than left to the
+ * caller: integer division in a shell script is where units go
+ * wrong.
+ */
+ if (enc_ns)
+ seq_printf(m, "ENCODE_KIB_PER_SEC=%llu\n",
+ div64_u64(bytes * 1000000ULL, enc_ns * 1024ULL / 1000ULL));
+ if (dec_ns)
+ seq_printf(m, "DECODE_KIB_PER_SEC=%llu\n",
+ div64_u64(bytes * 1000000ULL, dec_ns * 1024ULL / 1000ULL));
+
+ return 0;
+}
+
+DEFINE_SHOW_ATTRIBUTE(beamfs_rsbench);
+
+/*
+ * beamfs_debugfs_init -- create /sys/kernel/debug/beamfs.
+ *
+ * Module-wide rather than per-superblock: the codec is a property of
+ * the build, not of a mounted volume, and the number it produces does
+ * not change with what is mounted.
+ */
+void beamfs_debugfs_init(void)
+{
+ beamfs_debugfs_root = debugfs_create_dir("beamfs", NULL);
+ if (IS_ERR(beamfs_debugfs_root)) {
+ beamfs_debugfs_root = NULL;
+ return;
+ }
+
+ debugfs_create_file("rs_bench", 0444, beamfs_debugfs_root, NULL,
+ &beamfs_rsbench_fops);
+ debugfs_create_u32("rs_bench_iters", 0644, beamfs_debugfs_root,
+ &beamfs_rsbench_iters);
+}
+
+void beamfs_debugfs_exit(void)
+{
+ debugfs_remove_recursive(beamfs_debugfs_root);
+ beamfs_debugfs_root = NULL;
+}
diff --git a/fs/beamfs/scrub.c b/fs/beamfs/scrub.c
new file mode 100644
index 000000000000..41193283413f
--- /dev/null
+++ b/fs/beamfs/scrub.c
@@ -0,0 +1,1454 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * beamfs -- background scrubber
+ *
+ * Author: Aurelien DESBRIERES <aurelien@hackers.camp>
+ *
+ * beamfs corrects on read. A block nobody reads therefore accumulates
+ * upsets until it passes the eight-symbol correction radius and becomes
+ * unrecoverable, with nothing having reported the drift on the way.
+ * Cold data is the case this filesystem exists for -- an archive
+ * written once and read years later -- so waiting for a reader is
+ * waiting for the failure.
+ *
+ * The scrubber steps through the inode table and, for each regular
+ * file nobody is writing, follows the file's own pointers and decodes
+ * every data block; an indirect block is checked against its parity,
+ * where the volume keeps one, before its pointers are followed. At
+ * most one block named by the error-budget wear map is checked per
+ * step as well.
+ *
+ * A corrected block is written back, whole, under the buffer lock:
+ * each upset is correctable, their sum is not. It is written only
+ * while the owner still maps the block at the same place, asked under
+ * i_alloc_mutex, only if the buffer still holds the bytes the decode
+ * started from, and never on a read-only or frozen volume. A block
+ * with an uncorrectable codeword is not written and is retried on a
+ * later pass. Blocks reached through the wear map are checked and
+ * reported, never written: nothing says whose they are.
+ *
+ * Rate rather than priority. A thread that read every block on the
+ * device at RT priority would starve the workload it is meant to
+ * protect; a bounded interval gives the guarantee that matters -- a
+ * full sweep completes in a knowable time -- without that cost. The
+ * thread sleeps the interval after each file block it checks and after
+ * each inode-table slot it steps past, except at the end of a pass,
+ * where it sleeps a second. The interval is settable through sysfs, so
+ * an operator sizes the sweep against the expected upset rate;
+ * beamfs_scrub_pace then adjusts it once per pass, halving it after a
+ * pass that corrected anything and adding an eighth of the value set
+ * back after a quiet one, never below 1 ms and never above the value
+ * set.
+ */
+
+#include <linux/fs.h>
+#include <linux/kthread.h>
+#include <linux/delay.h>
+#include <linux/buffer_head.h>
+#include <linux/slab.h>
+#include <linux/sysfs.h>
+#include <linux/kobject.h>
+#include "beamfs.h"
+#include <linux/bio.h>
+#include <linux/math64.h>
+
+#define BEAMFS_SCRUB_DEFAULT_INTERVAL_MS 100
+
+/*
+ * The pace a volume starts at, in milliseconds between blocks.
+ *
+ * Per-volume it is settable through sysfs once mounted, which is no
+ * help for a volume something else mounts: xfstests makes and mounts
+ * its scratch device itself, and the scrubber is running on it before
+ * anything outside the kernel can say otherwise. Worse, on a machine
+ * whose root filesystem is beamfs, a sweep runs under every measurement
+ * taken there -- up to ten blocks a second at the default pace, and
+ * faster after any pass that needed a correction.
+ *
+ * As a parameter it is settable before the mount rather than after:
+ * beamfs.scrub_interval_ms= on the kernel command line, or through
+ * /sys/module/beamfs/parameters/ for the mounts still to come. 0 parks
+ * the scrubber, which is a measurement decision and not a safe
+ * default: correction on read is unaffected, but nothing then goes
+ * looking for the block nobody reads.
+ */
+static unsigned int scrub_interval_ms = BEAMFS_SCRUB_DEFAULT_INTERVAL_MS;
+module_param(scrub_interval_ms, uint, 0644);
+MODULE_PARM_DESC(scrub_interval_ms,
+ "milliseconds between blocks for newly mounted volumes (0 parks the scrubber)");
+
+/*
+ * Read @phys from the medium into @buf, around the block device's cache.
+ *
+ * The confirming read of an uncorrectable subblock asks whether the
+ * damage is on the medium. It used to ask by clearing the uptodate bit
+ * of the shared buffer and reading into it again: whatever the buffer
+ * held that the medium did not -- a writer's change not yet written,
+ * a block that had become another file's -- was replaced by what the
+ * disk held, in a buffer other paths read and may still write back. A
+ * private page and a bio ask the same question and change nothing
+ * anyone else sees.
+ */
+static int beamfs_scrub_read_medium(struct super_block *sb, u64 phys, u8 *buf)
+{
+ struct page *page;
+ struct bio *bio;
+ int ret;
+
+ page = alloc_page(GFP_NOFS);
+ if (!page)
+ return -ENOMEM;
+ bio = bio_alloc(sb->s_bdev, 1, REQ_OP_READ | REQ_SYNC, GFP_NOFS);
+ bio->bi_iter.bi_sector = phys << (sb->s_blocksize_bits - SECTOR_SHIFT);
+ __bio_add_page(bio, page, BEAMFS_BLOCK_SIZE, 0);
+ ret = submit_bio_wait(bio);
+ bio_put(bio);
+ if (!ret)
+ memcpy(buf, page_address(page), BEAMFS_BLOCK_SIZE);
+ __free_page(page);
+ return ret;
+}
+
+/*
+ * Decode what the medium holds for @phys into @r2, in the layout the
+ * first decode settled on. @buf is a scratch page.
+ */
+static noinline_for_stack void beamfs_scrub_confirm(struct super_block *sb,
+ u64 phys, u8 *buf,
+ bool woven, int *r2)
+{
+ u8 p2[BEAMFS_DATA_INLINE_SUBBLOCKS * (BEAMFS_RS_PARITY / 2)];
+ unsigned int k;
+ int mr;
+
+ mr = beamfs_scrub_read_medium(sb, phys, buf);
+ if (mr) {
+ /*
+ * No page: nothing confirmed, nothing reported. The
+ * medium refusing the read is damage.
+ */
+ for (k = 0; k < BEAMFS_DATA_INLINE_SUBBLOCKS; k++)
+ r2[k] = mr == -ENOMEM ? 0 : -1;
+ return;
+ }
+ /*
+ * The confirming read takes the layout the first decode settled
+ * on. Reading it the other way would confirm damage that is not
+ * there, which is the whole reason this second read exists.
+ */
+ if (woven)
+ beamfs_rs_decode_woven(buf + BEAMFS_CAPSULE_DATA_OFF,
+ buf + BEAMFS_CAPSULE_PARITY_OFF,
+ BEAMFS_RS_PARITY,
+ BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_DATA_INLINE_SUBBLOCKS,
+ r2, "sweep-confirm");
+ else
+ beamfs_rs_decode_region(buf, BEAMFS_SUBBLOCK_TOTAL,
+ buf + BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_SUBBLOCK_TOTAL,
+ BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_DATA_INLINE_SUBBLOCKS,
+ r2, p2, BEAMFS_RS_PARITY / 2,
+ "sweep-confirm");
+}
+
+/* The first logical block under each root: single, double, triple. */
+static u64 beamfs_scrub_root_base(unsigned int top)
+{
+ if (top == 1)
+ return BEAMFS_DIRECT_BLOCKS;
+ if (top == 2)
+ return BEAMFS_MAX_IBLOCK_INDIRECT;
+ return BEAMFS_MAX_IBLOCK_DINDIRECT;
+}
+
+/*
+ * Is @blk still the block at @depth on the path to logical block @base
+ * of @inode, in the tree whose root is at depth @top?
+ *
+ * The walk copies an indirect block's pointers and descends into them
+ * one at a time, sleeping the pace interval between leaves; a child
+ * freed and allocated again in the meantime is another file's block,
+ * and its "pointers" are whatever that file put there. Asked under
+ * i_alloc_mutex, from the root the inode holds now, reading the raw
+ * pointers: an identity check, not a verify.
+ */
+static bool beamfs_scrub_node_current(struct inode *inode, unsigned int top,
+ unsigned int depth, u64 base, u64 blk)
+{
+ struct beamfs_inode_info *fi = BEAMFS_I(inode);
+ struct super_block *sb = inode->i_sb;
+ u64 off = base - beamfs_scrub_root_base(top);
+ u64 node, span = 1;
+ unsigned int d;
+
+ lockdep_assert_held(&fi->i_alloc_mutex);
+ if (top == 1)
+ node = le64_to_cpu(fi->i_indirect);
+ else if (top == 2)
+ node = le64_to_cpu(fi->i_dindirect);
+ else
+ node = le64_to_cpu(fi->i_tindirect);
+
+ for (d = 1; d < top; d++)
+ span *= BEAMFS_INDIRECT_PTRS;
+
+ for (d = top; d > depth && node; d--) {
+ struct buffer_head *bh;
+ u64 slot = div64_u64(off, span);
+
+ if (slot >= BEAMFS_INDIRECT_PTRS)
+ return false;
+ off -= slot * span;
+ span = div64_u64(span, BEAMFS_INDIRECT_PTRS);
+ bh = sb_bread(sb, node);
+ if (!bh)
+ return false;
+ lock_buffer(bh);
+ node = le64_to_cpu(((__le64 *)bh->b_data)[slot]);
+ unlock_buffer(bh);
+ brelse(bh);
+ }
+ return node == blk;
+}
+
+/*
+ * Is @phys still logical block @iblock of @inode?
+ *
+ * Asked under i_alloc_mutex, which every path that frees one of the
+ * inode's blocks or installs a new one holds: truncate through both
+ * callers of beamfs_inline_free_blocks_from, eviction around
+ * beamfs_free_data_blocks, allocation throughout.
+ *
+ * A reserved inode is answered from its own pointers. Its blocks are
+ * placed by mkfs and never change hands: iget marks it immutable and
+ * the allocator skips it. The canary at inode 2 sits just below the
+ * data region, where lookup_phys refuses any file's pointer as
+ * corrupted: asked there, the sweep skipped the canary it used to
+ * check and logged "corrupted direct pointer ino=2" on every volume,
+ * every pass.
+ */
+static bool beamfs_scrub_leaf_current(struct inode *inode, u64 iblock,
+ u64 phys)
+{
+ u64 now = 0;
+
+ lockdep_assert_held(&BEAMFS_I(inode)->i_alloc_mutex);
+ if (beamfs_ino_is_reserved(inode->i_ino))
+ return iblock < BEAMFS_DIRECT_BLOCKS &&
+ le64_to_cpu(BEAMFS_I(inode)->i_direct[iblock]) == phys;
+ return beamfs_inline_lookup_phys(inode, iblock, &now) == 0 &&
+ now == phys;
+}
+
+/*
+ * Read @phys and copy the whole block into @orig. The buffer is
+ * returned held, or an ERR_PTR when it cannot be read.
+ */
+static struct buffer_head *beamfs_scrub_read_copy(struct super_block *sb,
+ u64 phys, u8 *orig)
+{
+ struct buffer_head *bh = sb_bread(sb, phys);
+
+ if (!bh)
+ return ERR_PTR(-EIO);
+ /*
+ * Decode into staging rather than in place: the
+ * buffer_head is shared through the block device page
+ * cache, and a scrub has no business modifying what a
+ * concurrent reader sees.
+ */
+ /*
+ * Under the buffer lock, or the snapshot is of two blocks.
+ *
+ * Writers hold lock_buffer across memcpy plus
+ * rs_encode_region -- the payload changes, then the
+ * sixteen parity fields catch up. Copying without the
+ * lock catches that halfway and hands the decoder a
+ * payload from after the write with parity from before,
+ * which is uncorrectable by construction.
+ *
+ * That is what the sweep kept reporting: block 1110
+ * failing all sixteen codewords while fsck called the
+ * same volume clean, and blocks 1062 and 1631 failing
+ * only the subblocks that held data. Twenty such reports
+ * appeared the moment new data blocks started being
+ * encoded on allocation, because there were simply more
+ * writes to collide with.
+ *
+ * The lock is held for a 4096-byte copy and nothing
+ * else. The decode runs afterwards, on the copy.
+ */
+ lock_buffer(bh);
+ /*
+ * The whole block, parity included.
+ *
+ * decode_rs8 corrects data and parity in place -- it
+ * writes to both buffers it is given. Copying only the
+ * payload and pointing the decoder at bh->b_data for the
+ * parity means every read rewrites the shared buffer's
+ * parity bytes. A block read often enough degrades until
+ * it will not decode at all, which is what the sweep
+ * kept reporting on blocks fsck called clean, and why
+ * the count grew as the volume got busier.
+ *
+ * The data path has done this correctly all along:
+ * "Decode RS(255,239) subblocks into a private scratch
+ * buffer, never into bh->b_data."
+ */
+ memcpy(orig, (u8 *)bh->b_data, BEAMFS_BLOCK_SIZE);
+ unlock_buffer(bh);
+ return bh;
+}
+
+/*
+ * The snapshot beamfs_scrub_check_block decodes: @phys copied into
+ * @orig, under @owner's allocation mutex when there is an owner, and
+ * only while @owner still names @phys at @iblock (-ESTALE
+ * otherwise). The lock and the unlock are unconditional on each path,
+ * so that neither a reader nor a checker has to pair two "if (owner)".
+ */
+static struct buffer_head *beamfs_scrub_snapshot(struct super_block *sb,
+ u64 phys, struct inode *owner,
+ u64 iblock, u8 *orig)
+{
+ struct beamfs_inode_info *fi;
+ struct buffer_head *bh;
+
+ if (!owner)
+ return beamfs_scrub_read_copy(sb, phys, orig);
+ fi = BEAMFS_I(owner);
+ mutex_lock(&fi->i_alloc_mutex);
+ if (beamfs_scrub_leaf_current(owner, iblock, phys))
+ bh = beamfs_scrub_read_copy(sb, phys, orig);
+ else
+ bh = ERR_PTR(-ESTALE);
+ mutex_unlock(&fi->i_alloc_mutex);
+ return bh;
+}
+
+/*
+ * beamfs_scrub_check_block -- decode one block, report what it found,
+ * and repair it while it is still what the caller took it for.
+ *
+ * @corrected receives the number of subblocks that needed correction.
+ * @owner and @iblock name the file and the logical block @phys is
+ * believed to be. Returns 0 if the block decodes, -EUCLEAN if any
+ * subblock exceeded the correction radius on the medium, -ESTALE if
+ * @owner no longer maps @iblock to @phys, or a negative errno on read
+ * failure.
+ *
+ * Without an owner -- the wear step, which visits whatever is
+ * allocated -- the block is checked and reported and never written:
+ * nothing says what it is.
+ *
+ * It used to work on the physical block alone, and that was wrong. The
+ * sweep walks a file's leaves from a copy of its pointers taken
+ * seconds earlier, and a leaf freed and allocated again as another
+ * file's indirect block was decoded as a data block of the first file:
+ * a nearly empty pointer array is within eight bytes of a zero
+ * codeword, so it was "corrected" to zero and written back. Measured
+ * on generic/083: 17 of 17 write-backs and uncorrectable verdicts of
+ * the sweep over 8 trials were on blocks that had changed hands and
+ * held pointers; one of them, indirect block 15011, took 15012 and its
+ * 17 children with it. Another trial rewrote a data block of another
+ * file over its 16 subblocks. The owner is now asked, under its
+ * allocation mutex, before the block is read and again before it is
+ * written, and the write happens only if the buffer still holds
+ * exactly the bytes that were decoded.
+ */
+int beamfs_scrub_check_block(struct super_block *sb, u64 phys,
+ unsigned int *corrected,
+ struct inode *owner, u64 iblock)
+{
+ struct buffer_head *bh = NULL;
+ /*
+ * Off the stack: submit-checklist allows a frame 512 bytes, and
+ * with these three on it the frame was 656. The allocation is
+ * small and GFP_NOFS; a sweep that cannot get it skips the block
+ * and comes back to it.
+ */
+ struct beamfs_scrub_scratch {
+ int results[BEAMFS_DATA_INLINE_SUBBLOCKS];
+ int r2[BEAMFS_DATA_INLINE_SUBBLOCKS];
+ u8 positions[BEAMFS_DATA_INLINE_SUBBLOCKS * (BEAMFS_RS_PARITY / 2)];
+ } *sc;
+ int *results, *r2;
+ u8 *positions;
+ u8 *staging, *orig;
+ unsigned int i, n_corrected = 0;
+ int ret = 0, dret;
+ /*
+ * Which layout the first decode settled on, so the confirming
+ * read takes the same one.
+ */
+ bool woven = false;
+ bool confirmed = false, repairable = true, wrote = false;
+
+ *corrected = 0;
+
+ sc = kzalloc_obj(*sc, GFP_NOFS);
+ if (!sc)
+ return -ENOMEM;
+ results = sc->results;
+ r2 = sc->r2;
+ positions = sc->positions;
+ /*
+ * Both pages before any mutex: the pool waits when it is empty,
+ * and nothing may wait on it holding the owner's allocation
+ * mutex. orig keeps the bytes the decode started from, to tell
+ * at write-back whether anybody wrote the buffer since.
+ */
+ staging = beamfs_scratch_get(sb);
+ orig = beamfs_scratch_get(sb);
+ if (!staging || !orig) {
+ ret = -ENOMEM;
+ goto out_free;
+ }
+
+ /*
+ * Still the owner's block, at the place the walk took it from?
+ * Asked under the mutex every free of the owner's blocks holds,
+ * and the snapshot is taken inside it: the block cannot change
+ * hands between the answer and the copy.
+ */
+ bh = beamfs_scrub_snapshot(sb, phys, owner, iblock, orig);
+ if (IS_ERR(bh)) {
+ ret = PTR_ERR(bh);
+ bh = NULL;
+ goto out_free;
+ }
+ memcpy(staging, orig, BEAMFS_BLOCK_SIZE);
+
+ /*
+ * Which layout, when the sweep does not know what it is holding.
+ *
+ * The callers with an owner know: the children of a level-one
+ * indirect block and an inode's i_direct are data blocks, and on
+ * an interleaved volume they are capsules. The wear step walks
+ * the wear map and visits whatever is allocated -- a capsule, an
+ * indirect block, a directory block -- with nothing to say
+ * which.
+ *
+ * So: try the layout the volume uses for data, and fall back to
+ * the alternating one. A block does not decode cleanly under
+ * both by accident -- sixteen codewords of sixteen parity bytes
+ * each agreeing on the wrong symbols is not something that
+ * happens -- so whichever succeeds is the one it was written in.
+ *
+ * Without this the sweep reported "block 17725 subblock 6/16
+ * uncorrectable" on a block that was intact, which is worse than
+ * missing damage: it condemns what it was meant to protect.
+ */
+ if (BEAMFS_SB(sb)->s_feat_incompat &
+ BEAMFS_FEATURE_INCOMPAT_RS_INTERLEAVE) {
+ woven = true;
+ ret = beamfs_rs_decode_woven(staging + BEAMFS_CAPSULE_DATA_OFF,
+ staging + BEAMFS_CAPSULE_PARITY_OFF,
+ BEAMFS_RS_PARITY,
+ BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_DATA_INLINE_SUBBLOCKS,
+ results, "sweep");
+ if (ret != -EUCLEAN)
+ goto decoded;
+
+ /*
+ * Not a capsule: an indirect or directory block. From
+ * the snapshot, not from the buffer, which is not
+ * locked now and may have been written since.
+ */
+ woven = false;
+ memcpy(staging, orig, BEAMFS_BLOCK_SIZE);
+ }
+
+ ret = beamfs_rs_decode_region(staging, BEAMFS_SUBBLOCK_TOTAL,
+ staging + BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_SUBBLOCK_TOTAL,
+ BEAMFS_SUBBLOCK_DATA,
+ BEAMFS_DATA_INLINE_SUBBLOCKS,
+ results, positions,
+ BEAMFS_RS_PARITY / 2,
+ "sweep");
+decoded:
+ dret = ret;
+ ret = 0;
+ if (dret < 0 && dret != -EUCLEAN) {
+ ret = dret;
+ goto out_brelse;
+ }
+
+ for (i = 0; i < BEAMFS_DATA_INLINE_SUBBLOCKS; i++) {
+ if (results[i] > 0) {
+ n_corrected++;
+ beamfs_log_rs_event_flagged(sb,
+ phys,
+ positions + (size_t)i * (BEAMFS_RS_PARITY / 2),
+ (unsigned int)results[i],
+ BEAMFS_SUBBLOCK_DATA,
+ beamfs_rs_event_subblock_bits(i));
+ /*
+ * The sweep is where the budget earns its keep: it
+ * visits blocks nobody reads, which are the ones
+ * that drift unobserved.
+ */
+ beamfs_budget_record(sb, phys,
+ (unsigned int)results[i]);
+ } else if (results[i] < 0) {
+ /*
+ * Confirm by re-reading before crying wolf. The
+ * sweep reads a block via sb_bread on the block
+ * device while the owning inode may still be
+ * flushing, and can catch data updated with parity
+ * not yet written, or the reverse -- uncorrectable
+ * on a block that is sound once the flush lands.
+ * Every first-boot report of this kind decoded
+ * clean offline, byte-identical on disk. A real
+ * defect survives a re-read; a half-flushed block
+ * does not.
+ *
+ * Once per block, into a private page: see
+ * beamfs_scrub_read_medium. A block with an
+ * uncorrectable subblock is not written back
+ * this pass; the next sweep retries from a
+ * fresh read.
+ */
+ repairable = false;
+ if (!confirmed) {
+ confirmed = true;
+ beamfs_scrub_confirm(sb, phys, staging,
+ woven, r2);
+ }
+ if (r2[i] >= 0)
+ continue;
+ /*
+ * Say which block and which codeword. An
+ * uncorrectable subblock is the event this
+ * filesystem exists to report, and a line that
+ * names neither is an alarm nobody can follow.
+ */
+ pr_err_ratelimited("beamfs: sweep: block %llu subblock %u/%u uncorrectable\n",
+ (unsigned long long)phys, i,
+ BEAMFS_DATA_INLINE_SUBBLOCKS);
+ /*
+ * Past the radius. Journalled with the same shape
+ * the read path uses, so a forensic pass cannot tell
+ * whether a reader or the scrubber found it -- which
+ * is correct: the damage is the same either way.
+ */
+ beamfs_log_rs_event_flagged(sb,
+ phys,
+ NULL, 0, BEAMFS_SUBBLOCK_DATA,
+ beamfs_rs_event_subblock_bits(i));
+ ret = -EUCLEAN;
+ }
+ }
+
+ /*
+ * Write the repair back, or it was not a repair.
+ *
+ * The decoder puts corrected data in staging and corrected parity
+ * straight into the buffer; without this the buffer was released
+ * and both were discarded. The block on disk stayed damaged, the
+ * next sweep found the same errors, and the counter climbed
+ * forever -- measured: 120 corrections of one block over 119
+ * sweeps, none of which fixed anything.
+ *
+ * That is the failure this filesystem exists to prevent. Each
+ * upset is individually correctable; what kills the data is their
+ * sum. A scrubber that detects without repairing lets them
+ * accumulate to the ninth, and the ninth is unrecoverable.
+ *
+ * Written under the buffer lock so a concurrent reader sees the
+ * block whole, and only when something was corrected: an
+ * untouched block must not be dirtied, or a quiet volume would
+ * rewrite itself endlessly and wear the medium for nothing.
+ *
+ * And only to the block the owner still names at @iblock, only
+ * if the buffer still holds the bytes the decode started from --
+ * a writer that came in between is right and the scrubber is
+ * not -- and never on a volume that is read-only or frozen.
+ */
+ if (n_corrected && repairable && owner && !sb_rdonly(sb) &&
+ sb_start_write_trylock(sb)) {
+ mutex_lock(&BEAMFS_I(owner)->i_alloc_mutex);
+ if (beamfs_scrub_leaf_current(owner, iblock, phys)) {
+ lock_buffer(bh);
+ if (!memcmp(bh->b_data, orig, BEAMFS_BLOCK_SIZE)) {
+ /*
+ * The whole block. staging holds the
+ * corrected codewords in their on-disk
+ * layout now -- data and parity both, at
+ * a 255-byte stride -- because that is
+ * what decode_rs8 produces and what the
+ * block has to go back as.
+ */
+ memcpy((u8 *)bh->b_data, staging,
+ BEAMFS_BLOCK_SIZE);
+ set_buffer_uptodate(bh);
+ wrote = true;
+ }
+ unlock_buffer(bh);
+ if (wrote)
+ mark_buffer_dirty(bh);
+ }
+ mutex_unlock(&BEAMFS_I(owner)->i_alloc_mutex);
+ sb_end_write(sb);
+ }
+
+ *corrected = n_corrected;
+out_brelse:
+ brelse(bh);
+out_free:
+ beamfs_scratch_put(sb, orig);
+ beamfs_scratch_put(sb, staging);
+ kfree(sc);
+ return ret;
+}
+
+/*
+ * Walk one inode's data blocks, decoding each.
+ *
+ * The bitmap says a block is in use; it does not say what it holds.
+ * Sweeping it directly meant handing indirect blocks and directory
+ * blocks to the RS decoder, which has no parity to find in them, and
+ * journalling every one as uncorrectable -- 24 false entries in the
+ * first minutes of the first run, in the same journal that exists to
+ * be forensic evidence. A scrubber that cries wolf is worse than none.
+ *
+ * Following the inode's own pointers is what the read path does, so
+ * the scrubber sees exactly the blocks a reader would decode and
+ * nothing else. Indirect blocks are not decoded as data: each is
+ * checked against its own parity, when the volume keeps an
+ * indirect-parity region, before its pointers are copied and followed.
+ *
+ * Returns the number of data blocks visited.
+ */
+static u64 beamfs_scrub_walk_level(struct super_block *sb, u64 blk,
+ unsigned int depth,
+ struct beamfs_sb_info *sbi,
+ struct inode *inode, unsigned int top,
+ u64 base)
+{
+ struct beamfs_inode_info *fi = BEAMFS_I(inode);
+ u64 nptrs = BEAMFS_BLOCK_SIZE / sizeof(__le64);
+ struct buffer_head *ibh;
+ __le64 *ptrs;
+ u64 visited = 0;
+ u64 span = 1;
+ u64 j;
+ unsigned int d;
+ int bad;
+
+ if (!blk || !beamfs_block_is_allocated(sb, blk))
+ return 0;
+
+ /* Logical blocks under one pointer of this block. */
+ for (d = 1; d < depth; d++)
+ span *= nptrs;
+
+ /*
+ * Copy the pointers and let the buffer go, for the same reason
+ * the inode is snapshotted above: the loop sleeps for the pace
+ * interval after every block, and a pointer into a block-device
+ * buffer does not survive that reliably. Reading ptrs[j] after a
+ * recycle gives whatever now occupies the page, and the sweep
+ * then reads and releases blocks at random -- which is how brelse
+ * came to be called on an already-free buffer.
+ *
+ * One page copied per indirect block visited, against a sweep
+ * that walks at one block per interval. Allocated before the
+ * mutex below, which the allocator must not be waited on under.
+ */
+ ptrs = kmalloc(BEAMFS_BLOCK_SIZE, GFP_NOFS);
+ if (!ptrs)
+ return 0;
+
+ /*
+ * Still this file's block, at this place in its tree?
+ *
+ * @blk comes from a copy of its parent taken when the walk
+ * started on the parent, which may be minutes ago; a block
+ * freed and allocated again since is another file's, and its
+ * verify below reports that file's state as this one's damage
+ * -- or its pointers are walked as this file's leaves. Asked
+ * under i_alloc_mutex, and the verify and the copy are done
+ * inside it, so the answer holds for both.
+ */
+ mutex_lock(&fi->i_alloc_mutex);
+ if (!beamfs_scrub_node_current(inode, top, depth, base, blk)) {
+ mutex_unlock(&fi->i_alloc_mutex);
+ kfree(ptrs);
+ return 0;
+ }
+ ibh = sb_bread(sb, blk);
+ if (!ibh) {
+ mutex_unlock(&fi->i_alloc_mutex);
+ kfree(ptrs);
+ return 0;
+ }
+
+ /*
+ * Check the indirect block itself before reading it.
+ *
+ * The sweep walked every data block an inode owns and never
+ * looked at the blocks holding the pointers, which carry their
+ * own parity scheme -- ind_parity, not the sixteen RS codewords
+ * a data block uses. A corrupted pointer array is worse than a
+ * corrupted data block: it loses everything below it.
+ */
+ bad = beamfs_ind_parity_verify_medium(sb, ibh);
+ if (!bad) {
+ /*
+ * The copy under the buffer lock, for the reason
+ * beamfs_scrub_check_block gives about data blocks and
+ * that was never applied to the blocks holding the
+ * pointers.
+ *
+ * Every site in file_inline.c that installs a pointer
+ * holds lock_buffer across the store. Copying without it
+ * catches the block halfway, and the walk below then
+ * descends into slots holding neither the old pointer
+ * nor the new one.
+ *
+ * The verify above stays outside the lock on purpose. It
+ * reads the parity region through beamfs_bread and takes
+ * a scratch page from a pool of thirty-two, and holding
+ * a buffer lock across that is the shape super.c
+ * documents at the scratch pool: three tasks stalled 191
+ * seconds in __bread_gfp under beamfs_ind_parity_verify,
+ * the machine at 330% CPU, never returning. A verify
+ * that reads a half-written block reports a block that
+ * is sound as uncorrectable, which is noise in dmesg; a
+ * walk over a half-written copy loses pointers, which is
+ * not. The lock goes where the damage is.
+ */
+ lock_buffer(ibh);
+ memcpy(ptrs, ibh->b_data, BEAMFS_BLOCK_SIZE);
+ unlock_buffer(ibh);
+ }
+ mutex_unlock(&fi->i_alloc_mutex);
+ brelse(ibh);
+ if (bad) {
+ pr_err_ratelimited("beamfs: sweep: indirect block %llu fails its parity\n",
+ (unsigned long long)blk);
+ sbi->s_scrub_uncorrectable++;
+ kfree(ptrs);
+ return 0;
+ }
+
+ for (j = 0; j < nptrs && !kthread_should_stop() && inode->i_nlink;
+ j++) {
+ u64 child = le64_to_cpu(ptrs[j]);
+ unsigned int corrected = 0;
+ int ret;
+
+ if (!child)
+ continue;
+
+ if (depth > 1) {
+ visited += beamfs_scrub_walk_level(sb, child,
+ depth - 1, sbi,
+ inode, top,
+ base + j * span);
+ continue;
+ }
+
+ if (!beamfs_block_is_allocated(sb, child))
+ continue;
+
+ /*
+ * The copy is as old as this walk, and the leaf is not
+ * trusted for what it says: check_block asks the owner
+ * whether logical block @base + j is still @child, and
+ * skips it with -ESTALE when it is not.
+ */
+ ret = beamfs_scrub_check_block(sb, child, &corrected,
+ inode, base + j);
+ if (ret == -ESTALE)
+ continue;
+ if (ret == -EUCLEAN)
+ pr_err_ratelimited("beamfs: sweep: block %llu is a leaf under indirect %llu (depth %u, slot %llu)\n",
+ (unsigned long long)child,
+ (unsigned long long)blk, depth,
+ (unsigned long long)j);
+ visited++;
+ sbi->s_scrub_blocks++;
+ if (corrected)
+ sbi->s_scrub_corrected++;
+ if (ret == -EUCLEAN)
+ sbi->s_scrub_uncorrectable++;
+
+ msleep_interruptible(READ_ONCE(sbi->s_scrub_interval_ms));
+ }
+
+ kfree(ptrs);
+ return visited;
+}
+
+static void beamfs_scrub_one_inode(struct super_block *sb, unsigned long ino)
+{
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+ struct beamfs_inode *raw;
+ struct buffer_head *bh;
+ unsigned long inodes_per_block, block, offset;
+ struct beamfs_inode snap;
+ unsigned int i;
+ umode_t mode;
+
+ /*
+ * Skip an inode somebody is writing.
+ *
+ * The sweep reads the inode table and the data blocks straight
+ * off the block device, in parallel with the writeback that is
+ * updating them. It can see a block with its payload written and
+ * its parity not yet, or the reverse, and report uncorrectable on
+ * something no reader would ever see in that state. Every such
+ * report checked out clean once the writer was done: fsck passed
+ * the image, the offline decoder found nothing, and the bytes on
+ * disk were identical to the ones that had just been rejected.
+ *
+ * Re-reading the block confirms the ones that settle between two
+ * reads, but a file being appended to continuously -- a system log
+ * during a test run -- is never settled, and both reads land in
+ * the same window.
+ *
+ * ilookup returns the inode only if it is already in cache; it
+ * never reads from disk, so it cannot recurse into the filesystem.
+ * An inode nobody has open is not being written and is safe to
+ * sweep. One that is dirty or under writeback is skipped and comes
+ * round again on the next pass, which is what a scrubber should do
+ * with a moving target.
+ */
+ {
+ struct inode *vi = ilookup(sb, ino);
+
+ if (vi) {
+ unsigned long st = inode_state_read_once(vi);
+ bool busy = (st & (I_DIRTY_ALL | I_SYNC |
+ I_NEW | I_FREEING | I_WILL_FREE)) ||
+ mapping_writably_mapped(vi->i_mapping);
+
+ iput(vi);
+ if (busy)
+ return;
+ }
+ }
+
+ inodes_per_block = BEAMFS_BLOCK_SIZE / sizeof(struct beamfs_inode);
+ block = le64_to_cpu(sbi->s_beamfs_sb->s_inode_table_blk)
+ + (ino - 1) / inodes_per_block;
+ offset = (ino - 1) % inodes_per_block;
+
+ bh = sb_bread(sb, block);
+ if (!bh)
+ return;
+ raw = (struct beamfs_inode *)bh->b_data + offset;
+
+ /*
+ * Take a copy of what the sweep needs, then let the buffer go.
+ *
+ * The loops below sleep for the pace interval between blocks --
+ * hundreds of milliseconds, sometimes seconds -- and reading
+ * raw->i_direct[i] after each nap means holding a pointer into a
+ * block-device buffer across all of it. The buffer can be
+ * recycled in that window, and then the pointers read out of it
+ * are whatever now occupies the page: brelse on a buffer that was
+ * already free, from beamfs_scrub_check_block, WARNING at
+ * fs/buffer.c:1141.
+ *
+ * Ninety-six bytes of pointers, copied once. The sweep works from
+ * a snapshot, which is what it wants anyway -- a file being
+ * rewritten underneath it is not something the scrubber has to
+ * follow.
+ */
+ memcpy(&snap, raw, sizeof(snap));
+ brelse(bh);
+ bh = NULL;
+ raw = &snap;
+ mode = le16_to_cpu(snap.i_mode);
+
+ /*
+ * Regular files only. A directory block has its own layout, and a
+ * fast symlink keeps its target in the pointer array as raw bytes
+ * -- feeding either to the decoder is the same mistake as sweeping
+ * the bitmap.
+ */
+ if (!S_ISREG(mode))
+ return;
+
+ {
+ /*
+ * The inode, held from here to the end of the walk.
+ *
+ * The check above says nobody is writing the file
+ * now. The walk takes seconds to minutes, and a file
+ * unlinked in the meantime was evicted, its blocks
+ * freed and handed to other files, while the walk
+ * still took them for its leaves: that is how the
+ * sweep came to "correct" other files' indirect
+ * blocks to zero. A reference keeps eviction, and so
+ * those frees, until iput; truncate frees under
+ * i_alloc_mutex, under which every block of the walk
+ * is checked. The walk stops at its next block once
+ * the file is unlinked, so the frees wait one pace
+ * interval at most. beamfs_iget refuses, with
+ * -ESTALE, a slot freed since the snapshot above.
+ *
+ * The tree walked is the one in memory, under the same
+ * mutex: the snapshot says what the inode was when it
+ * last reached the medium, not what its writers have
+ * made of it since.
+ */
+ struct inode *inode;
+ struct beamfs_inode_info *fi;
+ __le64 direct[BEAMFS_DIRECT_BLOCKS];
+ u64 roots[3];
+
+ inode = beamfs_iget(sb, ino);
+ if (IS_ERR(inode))
+ return;
+ if (!S_ISREG(inode->i_mode) || !inode->i_nlink) {
+ iput(inode);
+ return;
+ }
+ fi = BEAMFS_I(inode);
+ mutex_lock(&fi->i_alloc_mutex);
+ memcpy(direct, fi->i_direct, sizeof(direct));
+ roots[0] = le64_to_cpu(fi->i_indirect);
+ roots[1] = le64_to_cpu(fi->i_dindirect);
+ roots[2] = le64_to_cpu(fi->i_tindirect);
+ mutex_unlock(&fi->i_alloc_mutex);
+
+ for (i = 0; i < BEAMFS_DIRECT_BLOCKS &&
+ !kthread_should_stop() && inode->i_nlink; i++) {
+ u64 blk = le64_to_cpu(direct[i]);
+ unsigned int corrected = 0;
+ int ret;
+
+ if (!blk || !beamfs_block_is_allocated(sb, blk))
+ continue;
+
+ ret = beamfs_scrub_check_block(sb, blk, &corrected,
+ inode, i);
+ if (ret == -ESTALE)
+ continue;
+ sbi->s_scrub_blocks++;
+ if (corrected)
+ sbi->s_scrub_corrected++;
+ if (ret == -EUCLEAN)
+ sbi->s_scrub_uncorrectable++;
+
+ msleep_interruptible(READ_ONCE(sbi->s_scrub_interval_ms));
+ }
+
+ for (i = 0; i < 3; i++)
+ beamfs_scrub_walk_level(sb, roots[i], i + 1, sbi,
+ inode, i + 1,
+ beamfs_scrub_root_base(i + 1));
+ iput(inode);
+ }
+}
+
+/*
+ * Set the pace from what the sweep just found.
+ *
+ * One rule, applied once per sweep: corrections mean go faster, silence
+ * means drift back. Halve the interval on any correction, and add an
+ * eighth back when there were none.
+ *
+ * Halving is deliberate. A rise in flux is abrupt -- a beam turns on,
+ * an aircraft reaches altitude -- and a scrubber that eased into it
+ * would spend the interesting minutes still at its idle rate. Recovery
+ * is gradual for the opposite reason: nothing is lost by staying alert
+ * a while after the last correction, and a volume that oscillates
+ * around the threshold should settle rather than thrash.
+ *
+ * Bounded below at 1 ms, because faster is a busy loop rather than a
+ * scrub, and above by whatever the operator set, because they know
+ * what the volume is for and this only ever tightens their number.
+ *
+ * The rate is a response to measured corrections, not a prediction. It
+ * follows the environment; it does not model it.
+ */
+static void beamfs_scrub_pace(struct beamfs_sb_info *sbi)
+{
+ unsigned int cur = READ_ONCE(sbi->s_scrub_interval_ms);
+ unsigned int base = sbi->s_scrub_base_ms;
+ u64 found;
+
+ if (!cur || !base)
+ return;
+
+ found = sbi->s_scrub_corrected - sbi->s_scrub_last_corrected;
+ sbi->s_scrub_last_corrected = sbi->s_scrub_corrected;
+
+ if (found) {
+ cur = max_t(unsigned int, 1u, cur / 2);
+ } else if (cur < base) {
+ cur += max_t(unsigned int, 1u, base / 8);
+ cur = min(cur, base);
+ }
+
+ WRITE_ONCE(sbi->s_scrub_interval_ms, cur);
+}
+
+/*
+ * What the pacing is doing, and why.
+ *
+ * A mechanism that changes its own rate has to be able to account for
+ * it: an operator seeing the scrubber consume more bandwidth than they
+ * configured needs to find the reason here rather than guess. The
+ * ratio to base is the useful number -- 1 means quiet, 32 means the
+ * volume has been correcting for a while.
+ */
+static ssize_t scrub_pace_show(struct beamfs_sb_info *sbi, char *buf)
+{
+ unsigned int cur = READ_ONCE(sbi->s_scrub_interval_ms);
+ unsigned int base = sbi->s_scrub_base_ms;
+
+ return sysfs_emit(buf,
+ "interval_ms %u\nbase_ms %u\nfactor %u\ncorrected %llu\nwear_visits %llu\n",
+ cur, base,
+ (cur && base > cur) ? base / cur : 1,
+ sbi->s_scrub_corrected,
+ sbi->s_wear_visits);
+}
+
+/*
+ * Visit one worn block, if the budget knows of any.
+ *
+ * The sequential sweep answers "has anything changed"; this answers
+ * "where is the margin thin". They are different questions and the
+ * pacing above cannot serve the second: raising the rate for the whole
+ * volume to watch three blocks doubles the cost of 262051 that are
+ * untouched, which is what the measurement showed it doing.
+ *
+ * Called once per sweep step, so a worn block is seen every few
+ * hundred milliseconds while a fresh one waits for the sweep to reach
+ * it -- minutes or hours apart on a real volume. The ratio comes out of
+ * the geometry rather than a tuning knob: there is one wear visit per
+ * ordinary visit, and the number of worn blocks is small by definition.
+ * If it stops being small, the volume has a bigger problem than
+ * scheduling.
+ *
+ * Threshold at half the correctable symbols. Below that a block has
+ * consumed margin but retains most of it; at four of eight it is closer
+ * to the edge than to the start, and that is the point where seeing it
+ * often begins to matter.
+ */
+#define BEAMFS_WEAR_THRESHOLD (BEAMFS_RS_PARITY / 4)
+
+static void beamfs_scrub_wear_step(struct super_block *sb,
+ struct beamfs_sb_info *sbi)
+{
+ unsigned int corrected = 0;
+ u64 phys;
+
+ if (!sbi->s_budget_len)
+ return;
+
+ phys = beamfs_budget_next_worn(sb, sbi->s_wear_cursor,
+ BEAMFS_WEAR_THRESHOLD);
+ if (!phys) {
+ /* Round again from the start of the data region. */
+ sbi->s_wear_cursor = 0;
+ return;
+ }
+
+ sbi->s_wear_cursor = phys + 1;
+ sbi->s_wear_visits++;
+
+ if (!beamfs_block_is_allocated(sb, phys))
+ return;
+
+ /*
+ * Checked and reported, never written: the wear map names a
+ * physical block, and nothing says what it holds now or whose it
+ * is. A worn block is repaired when the sweep reaches the file
+ * that owns it, which asks the owner first.
+ */
+ if (beamfs_scrub_check_block(sb, phys, &corrected, NULL, 0) == -EUCLEAN)
+ sbi->s_scrub_uncorrectable++;
+ sbi->s_scrub_blocks++;
+ if (corrected)
+ sbi->s_scrub_corrected++;
+}
+
+static int beamfs_scrub_thread(void *data)
+{
+ struct super_block *sb = data;
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+
+ while (!kthread_should_stop()) {
+ unsigned long ino;
+
+ if (!READ_ONCE(sbi->s_scrub_interval_ms)) {
+ set_current_state(TASK_INTERRUPTIBLE);
+ if (!kthread_should_stop())
+ schedule_timeout(HZ);
+ __set_current_state(TASK_RUNNING);
+ continue;
+ }
+
+ ino = (unsigned long)sbi->s_scrub_cursor + 1;
+
+ /*
+ * s_inode_bitmap marks inodes that are FREE: setup_bitmap
+ * sets the bit when i_mode reads zero. An allocated inode
+ * is one whose bit is clear, which is the opposite of the
+ * obvious reading and why the first version swept nothing
+ * -- it visited only empty slots and found no data blocks
+ * on any of them.
+ */
+ if (sbi->s_inode_bitmap &&
+ !test_bit(ino, sbi->s_inode_bitmap))
+ beamfs_scrub_one_inode(sb, ino);
+
+ /*
+ * One worn block per step, alongside the sweep. The two
+ * run at the same cadence but cover sets of wildly
+ * different size, which is what gives worn blocks their
+ * attention without slowing the sweep.
+ */
+ beamfs_scrub_wear_step(sb, sbi);
+
+ sbi->s_scrub_cursor++;
+ if (sbi->s_scrub_cursor >= sbi->s_ninodes) {
+ sbi->s_scrub_cursor = 0;
+ sbi->s_scrub_passes++;
+ beamfs_scrub_pace(sbi);
+ /*
+ * Re-anchor once per sweep, so the wall-clock
+ * reference is at most one sweep old rather than as
+ * old as the mount. A volume left mounted for six
+ * months would otherwise date its events by a
+ * reading taken six months earlier.
+ *
+ * Once per sweep rather than on a timer: a sweep is
+ * already the unit in which this thread thinks, and
+ * a superblock write per sweep is nothing against
+ * the reads the sweep just did.
+ */
+ beamfs_clock_anchor(sb);
+ beamfs_alert_check_rate(sb);
+ /*
+ * A pass over an idle volume costs one inode-table
+ * read per inode and nothing else. Pause between
+ * sweeps so an empty filesystem does not spin.
+ */
+ msleep_interruptible(1000);
+ } else {
+ msleep_interruptible(READ_ONCE(sbi->s_scrub_interval_ms));
+ }
+ }
+ return 0;
+}
+
+/* ------------------------------------------------------------------ */
+/* sysfs */
+/* ------------------------------------------------------------------ */
+
+struct beamfs_scrub_attr {
+ struct attribute attr;
+ ssize_t (*show)(struct beamfs_sb_info *sbi, char *buf);
+ ssize_t (*store)(struct beamfs_sb_info *sbi, const char *buf,
+ size_t len);
+};
+
+static ssize_t interval_show(struct beamfs_sb_info *sbi, char *buf)
+{
+ return sysfs_emit(buf, "%u\n", READ_ONCE(sbi->s_scrub_interval_ms));
+}
+
+static ssize_t interval_store(struct beamfs_sb_info *sbi, const char *buf,
+ size_t len)
+{
+ unsigned int v;
+
+ if (kstrtouint(buf, 10, &v))
+ return -EINVAL;
+ /*
+ * Zero parks the thread. There is no upper clamp: an operator who
+ * wants one block a minute on a nearly idle archive is making a
+ * reasonable choice, and one who wants a tight sweep before a
+ * mission is making another.
+ */
+ /*
+ * Writing the interval sets the ceiling as well as the current
+ * pace: an operator asking for a rate is stating what the volume
+ * should cost when nothing is wrong, and the pacing only ever
+ * tightens from there.
+ */
+ sbi->s_scrub_base_ms = v;
+ WRITE_ONCE(sbi->s_scrub_interval_ms, v);
+ return len;
+}
+
+static ssize_t cursor_show(struct beamfs_sb_info *sbi, char *buf)
+{
+ return sysfs_emit(buf, "%llu\n", sbi->s_scrub_cursor);
+}
+
+static ssize_t passes_show(struct beamfs_sb_info *sbi, char *buf)
+{
+ return sysfs_emit(buf, "%llu\n", sbi->s_scrub_passes);
+}
+
+static ssize_t blocks_show(struct beamfs_sb_info *sbi, char *buf)
+{
+ return sysfs_emit(buf, "%llu\n", sbi->s_scrub_blocks);
+}
+
+static ssize_t corrected_show(struct beamfs_sb_info *sbi, char *buf)
+{
+ return sysfs_emit(buf, "%llu\n", sbi->s_scrub_corrected);
+}
+
+static ssize_t uncorrectable_show(struct beamfs_sb_info *sbi, char *buf)
+{
+ return sysfs_emit(buf, "%llu\n", sbi->s_scrub_uncorrectable);
+}
+
+/*
+ * error_budget -- how much correction capacity the volume has left.
+ *
+ * One line per wear level: the number of blocks whose worst subblock
+ * has needed that many corrections. Level 0 is untouched, level
+ * BEAMFS_ERROR_BUDGET_MAX is blocks with no margin at all -- the next
+ * upset in the wrong subblock takes them out.
+ *
+ * Reading walks the whole region, 244 MiB on a 931 GiB volume, so this
+ * is a deliberate cost paid on request rather than a counter kept in
+ * memory. A counter would have to be rebuilt at mount and would drift
+ * against the medium in between; the medium is the record.
+ */
+static ssize_t error_budget_show(struct beamfs_sb_info *sbi, char *buf)
+{
+ u64 hist[BEAMFS_ERROR_BUDGET_MAX + 1];
+ struct super_block *sb = sbi->s_sb;
+ unsigned int i;
+ int len = 0;
+
+ if (!sb)
+ return sysfs_emit(buf, "unavailable\n");
+
+ if (!sbi->s_budget_blk)
+ return sysfs_emit(buf, "disabled\n");
+
+ beamfs_budget_histogram(sb, hist);
+
+ for (i = 0; i <= BEAMFS_ERROR_BUDGET_MAX; i++)
+ len += sysfs_emit_at(buf, len, "%u %llu\n", i, hist[i]);
+
+ return len;
+}
+
+static ssize_t alert_rate_limit_show(struct beamfs_sb_info *sbi, char *buf)
+{
+ return sysfs_emit(buf, "%u\n", sbi->s_alert_rate_limit);
+}
+
+/*
+ * How many blocks may be corrected in one sweep before it is worth
+ * telling somebody.
+ *
+ * Zero disables the check and is the default: the right number depends
+ * entirely on where the machine sits, and a value invented here would
+ * either cry wolf on a spacecraft or stay silent in a hospital.
+ */
+static ssize_t alert_rate_limit_store(struct beamfs_sb_info *sbi,
+ const char *buf, size_t len)
+{
+ u32 v;
+
+ if (kstrtouint(buf, 10, &v))
+ return -EINVAL;
+ sbi->s_alert_rate_limit = v;
+ return len;
+}
+
+/*
+ * clock_anchor -- what the two clocks read at the same instant.
+ *
+ * Three fields: the monotonic stamp, the wall time it corresponded to,
+ * and what that wall time was worth. A reader converting a journal
+ * entry to a date needs all three, and dropping the third turns a
+ * qualified statement into an unqualified one.
+ */
+static ssize_t clock_anchor_show(struct beamfs_sb_info *sbi, char *buf)
+{
+ static const char * const q[] = {
+ "unknown", "rtc", "ntp", "hardware"
+ };
+ u32 i = sbi->s_anchor_quality;
+
+ if (i >= ARRAY_SIZE(q))
+ i = 0;
+
+ return sysfs_emit(buf, "mono %llu\nreal %llu\nquality %s\n",
+ sbi->s_anchor_mono, sbi->s_anchor_real, q[i]);
+}
+
+#define BEAMFS_SCRUB_RO(_name) \
+ static struct beamfs_scrub_attr beamfs_scrub_attr_##_name = { \
+ .attr = { .name = __stringify(_name), .mode = 0444 }, \
+ .show = _name##_show, \
+ }
+
+#define BEAMFS_SCRUB_RW(_name) \
+ static struct beamfs_scrub_attr beamfs_scrub_attr_##_name = { \
+ .attr = { .name = __stringify(_name), .mode = 0644 }, \
+ .show = _name##_show, .store = _name##_store, \
+ }
+
+BEAMFS_SCRUB_RW(interval);
+BEAMFS_SCRUB_RO(cursor);
+BEAMFS_SCRUB_RO(passes);
+BEAMFS_SCRUB_RO(blocks);
+BEAMFS_SCRUB_RO(corrected);
+BEAMFS_SCRUB_RO(uncorrectable);
+BEAMFS_SCRUB_RO(error_budget);
+BEAMFS_SCRUB_RO(clock_anchor);
+BEAMFS_SCRUB_RO(scrub_pace);
+BEAMFS_SCRUB_RW(alert_rate_limit);
+
+static struct attribute *beamfs_scrub_attrs[] = {
+ &beamfs_scrub_attr_interval.attr,
+ &beamfs_scrub_attr_cursor.attr,
+ &beamfs_scrub_attr_passes.attr,
+ &beamfs_scrub_attr_blocks.attr,
+ &beamfs_scrub_attr_corrected.attr,
+ &beamfs_scrub_attr_uncorrectable.attr,
+ &beamfs_scrub_attr_error_budget.attr,
+ &beamfs_scrub_attr_clock_anchor.attr,
+ &beamfs_scrub_attr_scrub_pace.attr,
+ &beamfs_scrub_attr_alert_rate_limit.attr,
+ NULL,
+};
+ATTRIBUTE_GROUPS(beamfs_scrub);
+
+static ssize_t beamfs_scrub_attr_show(struct kobject *kobj,
+ struct attribute *attr, char *buf)
+{
+ struct beamfs_sb_info *sbi =
+ container_of(kobj, struct beamfs_sb_info, s_kobj);
+ struct beamfs_scrub_attr *a =
+ container_of(attr, struct beamfs_scrub_attr, attr);
+
+ return a->show ? a->show(sbi, buf) : -EIO;
+}
+
+static ssize_t beamfs_scrub_attr_store(struct kobject *kobj,
+ struct attribute *attr,
+ const char *buf, size_t len)
+{
+ struct beamfs_sb_info *sbi =
+ container_of(kobj, struct beamfs_sb_info, s_kobj);
+ struct beamfs_scrub_attr *a =
+ container_of(attr, struct beamfs_scrub_attr, attr);
+
+ return a->store ? a->store(sbi, buf, len) : -EIO;
+}
+
+static const struct sysfs_ops beamfs_scrub_sysfs_ops = {
+ .show = beamfs_scrub_attr_show,
+ .store = beamfs_scrub_attr_store,
+};
+
+static void beamfs_scrub_release(struct kobject *kobj)
+{
+ struct beamfs_sb_info *sbi =
+ container_of(kobj, struct beamfs_sb_info, s_kobj);
+
+ complete(&sbi->s_kobj_unregister);
+}
+
+static const struct kobj_type beamfs_scrub_ktype = {
+ .default_groups = beamfs_scrub_groups,
+ .sysfs_ops = &beamfs_scrub_sysfs_ops,
+ .release = beamfs_scrub_release,
+};
+
+static struct kset *beamfs_kset;
+
+int beamfs_scrub_init(struct super_block *sb)
+{
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+ int ret;
+
+ sbi->s_scrub_interval_ms = READ_ONCE(scrub_interval_ms);
+ sbi->s_scrub_base_ms = READ_ONCE(scrub_interval_ms);
+ sbi->s_scrub_last_corrected = 0;
+ sbi->s_wear_cursor = 0;
+ sbi->s_wear_visits = 0;
+ sbi->s_scrub_cursor = 0;
+ sbi->s_scrub_passes = 0;
+ sbi->s_scrub_blocks = 0;
+ sbi->s_scrub_corrected = 0;
+ sbi->s_scrub_uncorrectable = 0;
+ init_completion(&sbi->s_kobj_unregister);
+
+ if (!beamfs_kset) {
+ beamfs_kset = kset_create_and_add("beamfs", NULL, fs_kobj);
+ if (!beamfs_kset)
+ return -ENOMEM;
+ }
+
+ sbi->s_kobj.kset = beamfs_kset;
+ ret = kobject_init_and_add(&sbi->s_kobj, &beamfs_scrub_ktype,
+ NULL, "%s", sb->s_id);
+ if (ret) {
+ kobject_put(&sbi->s_kobj);
+ wait_for_completion(&sbi->s_kobj_unregister);
+ return ret;
+ }
+
+ sbi->s_scrub_thread = kthread_run(beamfs_scrub_thread, sb,
+ "beamfs-scrub/%s", sb->s_id);
+ if (IS_ERR(sbi->s_scrub_thread)) {
+ ret = PTR_ERR(sbi->s_scrub_thread);
+ sbi->s_scrub_thread = NULL;
+ kobject_del(&sbi->s_kobj);
+ kobject_put(&sbi->s_kobj);
+ wait_for_completion(&sbi->s_kobj_unregister);
+ return ret;
+ }
+
+ return 0;
+}
+
+/*
+ * Stop the sweep. Safe to call more than once.
+ *
+ * The sweep holds a reference on the inode it walks, and put_super runs
+ * after generic_shutdown_super has evicted the volume's inodes: an
+ * inode still held then is busy after unmount, and its iput later
+ * touches a superblock that is gone. beamfs_kill_sb calls this before
+ * kill_block_super; put_super calls beamfs_scrub_exit, which finds the
+ * thread already stopped and takes down the sysfs directory.
+ */
+void beamfs_scrub_stop(struct super_block *sb)
+{
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+
+ if (sbi && sbi->s_scrub_thread) {
+ kthread_stop(sbi->s_scrub_thread);
+ sbi->s_scrub_thread = NULL;
+ }
+}
+
+void beamfs_scrub_exit(struct super_block *sb)
+{
+ struct beamfs_sb_info *sbi = BEAMFS_SB(sb);
+
+ if (!sbi)
+ return;
+
+ beamfs_scrub_stop(sb);
+ if (sbi->s_kobj.state_initialized) {
+ kobject_del(&sbi->s_kobj);
+ kobject_put(&sbi->s_kobj);
+ wait_for_completion(&sbi->s_kobj_unregister);
+ }
+}
--
2.55.0
next prev parent reply other threads:[~2026-10-19 4:18 UTC|newest]
Thread overview: 19+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-19 4:17 [RFC PATCH 00/18] beamfs: Reed-Solomon protected filesystem Aurelien DESBRIERES
2026-10-19 4:17 ` [RFC PATCH 01/18] beamfs: add on-disk format definitions Aurelien DESBRIERES
2026-10-19 4:17 ` [RFC PATCH 02/18] beamfs: add in-core header and tracepoints Aurelien DESBRIERES
2026-10-19 4:17 ` [RFC PATCH 03/18] beamfs: add CRC32 and Reed-Solomon layer Aurelien DESBRIERES
2026-10-19 4:17 ` [RFC PATCH 04/18] beamfs: add error budget, alerts and clock Aurelien DESBRIERES
2026-10-19 4:17 ` [RFC PATCH 05/18] beamfs: add indirect parity, tree checker Aurelien DESBRIERES
2026-10-19 4:17 ` [RFC PATCH 06/18] beamfs: add block and inode allocator Aurelien DESBRIERES
2026-10-19 4:17 ` [RFC PATCH 07/18] beamfs: add superblock operations Aurelien DESBRIERES
2026-10-19 4:17 ` [RFC PATCH 08/18] beamfs: add inode and directory operations Aurelien DESBRIERES
2026-10-19 4:17 ` [RFC PATCH 09/18] beamfs: add namespace operations Aurelien DESBRIERES
2026-10-19 4:17 ` [RFC PATCH 10/18] beamfs: add iomap file operations Aurelien DESBRIERES
2026-10-19 4:17 ` [RFC PATCH 11/18] beamfs: inline data path 1/4, block map Aurelien DESBRIERES
2026-10-19 4:17 ` [RFC PATCH 12/18] beamfs: inline data path 2/4, allocation Aurelien DESBRIERES
2026-10-19 4:17 ` [RFC PATCH 13/18] beamfs: inline data path 3/4, read side Aurelien DESBRIERES
2026-10-19 4:17 ` [RFC PATCH 14/18] beamfs: inline data path 4/4, write side Aurelien DESBRIERES
2026-10-19 4:17 ` Aurelien DESBRIERES [this message]
2026-10-19 4:17 ` [RFC PATCH 16/18] Documentation: filesystems: add beamfs Aurelien DESBRIERES
2026-10-19 4:17 ` [RFC PATCH 17/18] beamfs: add Kconfig and Makefile Aurelien DESBRIERES
2026-10-19 4:17 ` [RFC PATCH 18/18] MAINTAINERS: add beamfs Aurelien DESBRIERES
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261019041728.327785-16-aurelien@hackers.camp \
--to=aurelien@hackers.camp \
--cc=aurelien.desbrieres@gmail.com \
--cc=brauner@kernel.org \
--cc=corbet@lwn.net \
--cc=jack@suse.cz \
--cc=linux-api@vger.kernel.org \
--cc=linux-doc@vger.kernel.org \
--cc=linux-fsdevel@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=rdunlap@infradead.org \
--cc=skhan@linuxfoundation.org \
--cc=viro@zeniv.linux.org.uk \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®