Transaction groups commit from a schfsd thread every 5 seconds, on sync and fsync, on unmount and shutdown, and early when freed-but-pending blocks crowd the free ones -- the contract ext4 and ZFS have. A crash still lands on the last group whole, and loses at most those seconds. Committing before every write(2) returned cost two cache flushes and an uberblock a call: 4 KB writes go from 2.8 to 6.6 MB/s on AHCI, and from 66 KB/s to 3.9 MB/s on QEMU's IDE disk. Filling the disk made the root volume unmountable, on the last commit as well: a tree update that ran out of blocks halfway was published by the next commit. 1% of the volume (1 to 64 MiB) is now a metadata reserve, as btrfs and ZFS keep one; data stops at it with ENOSPC as a short write, new names at half of it, and deletes may use the rest. A write that runs out part way records what it wrote in the inode rather than leaving blocks past an end of file nothing moved. Deleting a large file goes in 4 MiB steps from the end with a commit allowed between them, as ext4 restarts its transaction through a long truncate. Deleting 80 MB on a full disk ran out of room halfway and leaked the rest. The VFS's dirunlink called the object layer's whole unlink, which freed the object with its last name, so a file removed while open lost its data under the reader. It removes the name only now, and the object is freed on the last iput. An object still open when the machine went down is found by the mount's space scan and freed, like ext4's orphan list. The allocation bitmap has a second level of pointer pages, so the mount ceiling is 64 TiB instead of 64 GiB, and the freed-this-group bitmap has pages only where frees happened: a 1 TiB volume takes 32 MiB. A freed block written after the newest snapshot is reused at once (ZFS's birth-time rule, so bfree carries the block's generation); only older ones are kept while a snapshot exists. Deleting one snapshot no longer stops retaining for the others, which let their blocks be handed out again. Checked under QEMU: crash after sync, after 8 s and within 5 s; fill to ENOSPC, rm, remount; unlink while open; an orphan freed at the next mount; a 1 TiB volume. schfs-check passes, with a new test for the snapshot rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
554 lines
20 KiB
C
554 lines
20 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
//
|
|
// SchFS space manager: which blocks are in use.
|
|
//
|
|
// There is no free-space map on disk, and that is the interesting decision
|
|
// here. The committed tree already records exactly which blocks are live —
|
|
// every tree node is named by its parent's pointer, every data block by an
|
|
// extent record — so the map is *derived* at mount by walking the tree once,
|
|
// held in RAM, and never written back.
|
|
//
|
|
// The alternative, persisting it, buys a fast mount and costs the one problem
|
|
// copy-on-write is otherwise free of: the map has to be updated atomically with
|
|
// the tree it describes, and writing the map needs blocks, which changes the
|
|
// map. ZFS spends real complexity on that recursion and btrfs resolves it with
|
|
// a fixpoint loop. Deriving the map instead cannot disagree with the filesystem
|
|
// no matter what a crash interrupts, because it is computed from it — there is
|
|
// no second source of truth to get out of step.
|
|
//
|
|
// What it costs is a metadata scan at mount, proportional to the number of tree
|
|
// blocks rather than to the size of the volume. Nothing is written, so it is
|
|
// also safe on a read-only mount. If mount time ever becomes the thing that
|
|
// hurts, the map can be cached on disk later purely as a hint, validated
|
|
// against the uberblock's generation and discarded when it does not match — an
|
|
// addition, not a format change.
|
|
//
|
|
// ---------------------------------------------------------------------------
|
|
// Deferred frees
|
|
//
|
|
// A block released during the transaction group being built is still reachable
|
|
// from the last *committed* root, which is where a crash lands. Handing it
|
|
// straight back would let the filesystem overwrite the very copy it is supposed
|
|
// to fall back to. So frees go into a second bitmap and only rejoin the free
|
|
// map when the commit that supersedes them lands. One transaction of delay, no
|
|
// more: once group N is published, group N-1's tree is no longer anybody's
|
|
// fallback.
|
|
|
|
#include "schfs_int.h"
|
|
|
|
#ifdef SCHFS_HOSTED
|
|
#include <string.h>
|
|
#else
|
|
#include "types.h"
|
|
#include "defs.h"
|
|
#endif
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Bit vectors, paged */
|
|
/* */
|
|
/* The bitmap has to be indexable, and the kernel hands out memory a */
|
|
/* page at a time with no promise that two pages are adjacent. So it */
|
|
/* is pages of bits found through pages of pointers -- see struct */
|
|
/* schfs_bits -- and a missing page reads as zeros. See */
|
|
/* docs/architecture/schfs.md for the resulting limits. */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static uint64_t *
|
|
bits_page(const struct schfs_bits *v, uint64_t pg)
|
|
{
|
|
uint64_t **d = v->dir[pg / SCHFS_SPACE_PTRS_PER_PAGE];
|
|
return d ? d[pg % SCHFS_SPACE_PTRS_PER_PAGE] : 0;
|
|
}
|
|
|
|
// The page for `pg`, taken from `palloc` (zeroed) if it is not there yet.
|
|
// 0 when there is no memory for it.
|
|
static uint64_t *
|
|
bits_page_get(struct schfs_bits *v, uint64_t pg, void *(*palloc)(void))
|
|
{
|
|
uint64_t ***d = &v->dir[pg / SCHFS_SPACE_PTRS_PER_PAGE];
|
|
if (!*d) {
|
|
if (!palloc || !(*d = (uint64_t **)palloc()))
|
|
return 0;
|
|
memset(*d, 0, SCHFS_BLKSIZE);
|
|
}
|
|
uint64_t **slot = &(*d)[pg % SCHFS_SPACE_PTRS_PER_PAGE];
|
|
if (!*slot) {
|
|
if (!(*slot = (uint64_t *)palloc()))
|
|
return 0;
|
|
memset(*slot, 0, SCHFS_BLKSIZE);
|
|
}
|
|
return *slot;
|
|
}
|
|
|
|
// Give every page back, pointer pages included.
|
|
static void
|
|
bits_free(struct schfs_bits *v, void (*pfree)(void *))
|
|
{
|
|
for (int i = 0; i < SCHFS_SPACE_DIRS; i++) {
|
|
if (!v->dir[i])
|
|
continue;
|
|
if (pfree) {
|
|
for (int j = 0; j < (int)SCHFS_SPACE_PTRS_PER_PAGE; j++)
|
|
if (v->dir[i][j])
|
|
pfree(v->dir[i][j]);
|
|
pfree(v->dir[i]);
|
|
}
|
|
v->dir[i] = 0;
|
|
}
|
|
}
|
|
|
|
static int
|
|
bit_test(const struct schfs_bits *v, uint64_t b)
|
|
{
|
|
uint64_t *pg = bits_page(v, b / SCHFS_SPACE_BITS_PER_PAGE);
|
|
uint64_t off = b % SCHFS_SPACE_BITS_PER_PAGE;
|
|
return pg ? (int)((pg[off / 64] >> (off % 64)) & 1) : 0;
|
|
}
|
|
|
|
// Only on a page that exists: the inuse map has all of its pages from init,
|
|
// and the pend map takes one in schfs_space_free before it sets anything.
|
|
static void
|
|
bit_set(const struct schfs_bits *v, uint64_t b)
|
|
{
|
|
uint64_t *pg = bits_page(v, b / SCHFS_SPACE_BITS_PER_PAGE);
|
|
uint64_t off = b % SCHFS_SPACE_BITS_PER_PAGE;
|
|
pg[off / 64] |= 1ULL << (off % 64);
|
|
}
|
|
|
|
// The whole 64-block word containing b, so a scan can skip a fully used region
|
|
// sixty-four blocks at a time instead of one. On a nearly full volume that is
|
|
// the difference between an allocation costing microseconds and milliseconds.
|
|
static uint64_t
|
|
bit_word(const struct schfs_bits *v, uint64_t b)
|
|
{
|
|
uint64_t *pg = bits_page(v, b / SCHFS_SPACE_BITS_PER_PAGE);
|
|
uint64_t off = b % SCHFS_SPACE_BITS_PER_PAGE;
|
|
return pg ? pg[off / 64] : 0;
|
|
}
|
|
|
|
static int
|
|
popcount64(uint64_t v)
|
|
{
|
|
int n = 0;
|
|
while (v) { v &= v - 1; n++; }
|
|
return n;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Setup */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
// Bring an already-sized map back to "empty volume": every block free except
|
|
// the ones that are never allocatable. Split out of schfs_space_init because
|
|
// mounting may have to try more than one uberblock, and each candidate root
|
|
// needs a scan starting from a clean map -- while the pages themselves were
|
|
// taken once and must not be taken again.
|
|
void
|
|
schfs_space_reset(struct schfs_space *s, uint64_t nblocks)
|
|
{
|
|
for (uint64_t i = 0; i < s->npages; i++)
|
|
memset(bits_page(&s->inuse, i), 0, SCHFS_BLKSIZE);
|
|
bits_free(&s->pend, s->pfree);
|
|
|
|
s->nblocks = nblocks;
|
|
s->free_blocks = nblocks - SCHFS_FIRST_BLOCK;
|
|
s->pending = 0;
|
|
// 1% of the volume, at least 1 MB and at most 64 MB. A tree update touches
|
|
// a path of at most SCHFS_MAX_LEVEL nodes per record, and a group rewrites
|
|
// a node once however often it changes, so this is generous; btrfs keeps
|
|
// between 16 MB and 512 MB for the same job.
|
|
s->reserve = nblocks / 100;
|
|
if (s->reserve < 256)
|
|
s->reserve = 256;
|
|
if (s->reserve > 16384)
|
|
s->reserve = 16384;
|
|
s->hint = SCHFS_FIRST_BLOCK;
|
|
// Cleared, not preserved: schfs_space_scan sets RETAIN_ALL when it finds a
|
|
// snapshot, so carrying one candidate root's answer into the next root's
|
|
// scan would pin blocks for a snapshot that tree may not even have.
|
|
s->flags = 0;
|
|
s->snap_gen = 0;
|
|
|
|
// The boot block and the uberblock ring are never allocatable, and neither
|
|
// is anything past the end of the volume: the last bitmap page covers more
|
|
// blocks than exist, and a stray allocation out there would be written to a
|
|
// sector the device does not have.
|
|
for (uint64_t b = 0; b < SCHFS_FIRST_BLOCK; b++)
|
|
bit_set(&s->inuse, b);
|
|
for (uint64_t b = nblocks; b < s->npages * SCHFS_SPACE_BITS_PER_PAGE; b++)
|
|
bit_set(&s->inuse, b);
|
|
}
|
|
|
|
void
|
|
schfs_space_release(struct schfs_space *s)
|
|
{
|
|
bits_free(&s->inuse, s->pfree);
|
|
bits_free(&s->pend, s->pfree);
|
|
// Zeroed rather than merely emptied, so calling this twice is harmless and
|
|
// a released map cannot be mistaken for a sized one.
|
|
memset(s, 0, sizeof(*s));
|
|
}
|
|
|
|
int
|
|
schfs_space_init(struct schfs_space *s, uint64_t nblocks,
|
|
void *(*palloc)(void), void (*pfree)(void *))
|
|
{
|
|
memset(s, 0, sizeof(*s));
|
|
s->palloc = palloc;
|
|
s->pfree = pfree;
|
|
|
|
if (nblocks <= SCHFS_FIRST_BLOCK)
|
|
return -3;
|
|
if (nblocks > SCHFS_MAX_BLOCKS)
|
|
return -4; /* larger than this build can map */
|
|
|
|
uint64_t npages = (nblocks + SCHFS_SPACE_BITS_PER_PAGE - 1)
|
|
/ SCHFS_SPACE_BITS_PER_PAGE;
|
|
|
|
// Every inuse page now; pend pages as frees need them. A failure part way
|
|
// leaves what was taken reachable from the map, so release finds it.
|
|
for (uint64_t i = 0; i < npages; i++) {
|
|
if (!bits_page_get(&s->inuse, i, palloc)) {
|
|
schfs_space_release(s);
|
|
return -5;
|
|
}
|
|
s->npages = i + 1;
|
|
}
|
|
|
|
schfs_space_reset(s, nblocks);
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
schfs_space_mark(struct schfs_space *s, uint64_t blk, uint64_t n)
|
|
{
|
|
for (uint64_t i = 0; i < n; i++) {
|
|
uint64_t b = blk + i;
|
|
if (b < SCHFS_FIRST_BLOCK || b >= s->nblocks)
|
|
continue;
|
|
if (bit_test(&s->inuse, b))
|
|
continue; /* already counted; extents may be shared */
|
|
bit_set(&s->inuse, b);
|
|
s->free_blocks--;
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Allocation */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
uint64_t
|
|
schfs_space_alloc_run(struct schfs_space *s, uint64_t n)
|
|
{
|
|
if (n == 0 || n > s->free_blocks)
|
|
return 0;
|
|
|
|
// Two sweeps: from the rotating hint to the end of the volume, then from
|
|
// the start back to it. Starting every search at block 0 would make the
|
|
// front of the volume the only place anything is ever written and turn
|
|
// allocation into a linear scan of everything already used.
|
|
for (int pass = 0; pass < 2; pass++) {
|
|
uint64_t lo = pass == 0 ? s->hint : SCHFS_FIRST_BLOCK;
|
|
uint64_t hi = pass == 0 ? s->nblocks : s->hint;
|
|
uint64_t run = 0, first = 0;
|
|
|
|
for (uint64_t b = lo; b < hi; ) {
|
|
if (b % 64 == 0 && bit_word(&s->inuse, b) == ~0ULL) {
|
|
run = 0;
|
|
b += 64;
|
|
continue;
|
|
}
|
|
if (bit_test(&s->inuse, b)) {
|
|
run = 0;
|
|
b++;
|
|
continue;
|
|
}
|
|
if (run == 0)
|
|
first = b;
|
|
run++;
|
|
b++;
|
|
if (run == n) {
|
|
for (uint64_t i = 0; i < n; i++)
|
|
bit_set(&s->inuse, first + i);
|
|
s->free_blocks -= n;
|
|
s->hint = first + n < s->nblocks ? first + n : SCHFS_FIRST_BLOCK;
|
|
return first;
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
uint64_t
|
|
schfs_space_alloc(struct schfs_space *s)
|
|
{
|
|
return schfs_space_alloc_run(s, 1);
|
|
}
|
|
|
|
void
|
|
schfs_space_free(struct schfs_space *s, uint64_t blk, uint64_t gen)
|
|
{
|
|
// A snapshot may still be holding this block, and if so it must stay.
|
|
//
|
|
// Copy-on-write frees the old block whenever one is rewritten, and that is
|
|
// exactly the block a snapshot is holding. Freeing it would hand it out
|
|
// again and overwrite the data the snapshot exists to keep -- silently,
|
|
// with the damage only visible later when the snapshot is read.
|
|
//
|
|
// ZFS's rule says which: a snapshot names the tree committed before the
|
|
// group it was created in, so it can hold only blocks written before that
|
|
// group. A block born in or after the newest snapshot's group is in none
|
|
// of them and goes back to the free map as usual; only the older ones are
|
|
// kept, until a mount derives the map from what the snapshots still reach.
|
|
// This used to keep everything while any snapshot existed, so a volume
|
|
// with one never reused a block it freed until it was remounted.
|
|
if ((s->flags & SCHFS_SPACE_RETAIN_ALL) && gen < s->snap_gen)
|
|
return;
|
|
|
|
if (blk < SCHFS_FIRST_BLOCK || blk >= s->nblocks)
|
|
return;
|
|
if (!bit_test(&s->inuse, blk))
|
|
return; /* never allocated, or freed twice */
|
|
if (bit_test(&s->pend, blk))
|
|
return; /* already queued this group */
|
|
// No page to note it in, and no memory for one: the block stays in use
|
|
// until the next mount derives the map again. Lost space for a while,
|
|
// never a block handed out twice.
|
|
if (!bits_page_get(&s->pend, blk / SCHFS_SPACE_BITS_PER_PAGE, s->palloc))
|
|
return;
|
|
bit_set(&s->pend, blk);
|
|
s->pending++;
|
|
}
|
|
|
|
void
|
|
schfs_space_commit(struct schfs_space *s)
|
|
{
|
|
uint64_t words = SCHFS_SPACE_BITS_PER_PAGE / 64;
|
|
for (uint64_t p = 0; p < s->npages; p++) {
|
|
uint64_t *pend = bits_page(&s->pend, p);
|
|
uint64_t *inuse = bits_page(&s->inuse, p);
|
|
if (!pend)
|
|
continue;
|
|
for (uint64_t w = 0; w < words; w++) {
|
|
if (pend[w] == 0)
|
|
continue;
|
|
s->free_blocks += popcount64(pend[w] & inuse[w]);
|
|
inuse[w] &= ~pend[w];
|
|
pend[w] = 0;
|
|
}
|
|
}
|
|
// The pages stay for the next group: a volume being written frees in the
|
|
// same places again, and taking and dropping them every commit is waste.
|
|
s->pending = 0;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Deriving the map from the committed tree */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
// Every extent record in a leaf names a run of data blocks. A range shared by
|
|
// several files (a reflink, a snapshot) appears more than once, which is why
|
|
// schfs_space_mark is idempotent rather than counting.
|
|
static void
|
|
mark_leaf_extents(struct schfs_space *s, const void *blk)
|
|
{
|
|
const struct schfs_item *it = SCHFS_ITEMS(blk);
|
|
int n = SCHFS_HDR(blk)->nritems;
|
|
|
|
for (int i = 0; i < n; i++) {
|
|
if (it[i].key.type != SCHFS_KEY_EXTENT)
|
|
continue;
|
|
if (it[i].size != sizeof(struct schfs_extent))
|
|
continue;
|
|
if ((int)it[i].offset + (int)it[i].size > SCHFS_BLKSIZE)
|
|
continue;
|
|
const struct schfs_extent *e =
|
|
(const struct schfs_extent *)((const char *)blk + it[i].offset);
|
|
schfs_space_mark(s, e->disk_block, e->nblocks);
|
|
}
|
|
|
|
// Shared runs, at the length they were *allocated*.
|
|
//
|
|
// Not redundant with the loop above, and the difference matters. A file
|
|
// that shares a run may since have been truncated inside it, so its extent
|
|
// record describes a shorter run than exists -- and the tail would be
|
|
// derived as free while the reference record still holds it. The allocator
|
|
// would then hand out a block that a later release frees a second time.
|
|
for (int i = 0; i < n; i++) {
|
|
if (it[i].key.type != SCHFS_KEY_EXTREF)
|
|
continue;
|
|
if (it[i].size != sizeof(struct schfs_extref))
|
|
continue;
|
|
if ((int)it[i].offset + (int)it[i].size > SCHFS_BLKSIZE)
|
|
continue;
|
|
const struct schfs_extref *x =
|
|
(const struct schfs_extref *)((const char *)blk + it[i].offset);
|
|
schfs_space_mark(s, it[i].key.offset, x->nblocks);
|
|
}
|
|
}
|
|
|
|
// Inodes with no links left, for the mount to free. See schfs_vol.orphans.
|
|
static void
|
|
collect_orphans(struct schfs_vol *v, const void *blk)
|
|
{
|
|
const struct schfs_item *it = SCHFS_ITEMS(blk);
|
|
int n = SCHFS_HDR(blk)->nritems;
|
|
|
|
for (int i = 0; i < n; i++) {
|
|
if (it[i].key.type != SCHFS_KEY_INODE)
|
|
continue;
|
|
if (it[i].size != sizeof(struct schfs_inode))
|
|
continue;
|
|
if ((int)it[i].offset + (int)it[i].size > SCHFS_BLKSIZE)
|
|
continue;
|
|
const struct schfs_inode *in =
|
|
(const struct schfs_inode *)((const char *)blk + it[i].offset);
|
|
if (in->nlink != 0 || it[i].key.objectid == v->root_objectid)
|
|
continue;
|
|
if (v->norphans >= SCHFS_ORPHANS_MAX) { v->orphans_more = 1; continue; }
|
|
v->orphans[v->norphans++] = it[i].key.objectid;
|
|
}
|
|
}
|
|
|
|
// Snapshot roots found while walking, so they can be walked in turn.
|
|
//
|
|
// Collected rather than descended into immediately: the walk keeps one block
|
|
// buffer and a path, and re-entering it from inside itself would need a second
|
|
// of each.
|
|
#define SNAP_ROOTS_MAX 64
|
|
|
|
struct snap_roots {
|
|
int n;
|
|
int overflow;
|
|
uint64_t newest; /* the largest `created` seen */
|
|
uint64_t nr[SNAP_ROOTS_MAX];
|
|
uint32_t crc[SNAP_ROOTS_MAX];
|
|
};
|
|
|
|
static void
|
|
collect_snap_roots(struct snap_roots *sr, const void *blk)
|
|
{
|
|
const struct schfs_item *it = SCHFS_ITEMS(blk);
|
|
int n = SCHFS_HDR(blk)->nritems;
|
|
|
|
for (int i = 0; i < n; i++) {
|
|
if (it[i].key.type != SCHFS_KEY_SNAP)
|
|
continue;
|
|
if (it[i].size != sizeof(struct schfs_snap))
|
|
continue;
|
|
if ((int)it[i].offset + (int)it[i].size > SCHFS_BLKSIZE)
|
|
continue;
|
|
const struct schfs_snap *sn =
|
|
(const struct schfs_snap *)((const char *)blk + it[i].offset);
|
|
if (sn->created > sr->newest)
|
|
sr->newest = sn->created;
|
|
if (sr->n >= SNAP_ROOTS_MAX) { sr->overflow = 1; continue; }
|
|
sr->nr[sr->n] = sn->root;
|
|
sr->crc[sr->n] = sn->root_crc;
|
|
sr->n++;
|
|
}
|
|
}
|
|
|
|
// Walk the whole tree, marking every block it reaches.
|
|
//
|
|
// Iterative, with one block buffer and a path of block numbers, for the same
|
|
// reason everything else here is: a recursive walk keeps a block per level and
|
|
// the kernel stack is one block in total. Climbing re-reads the parent, so an
|
|
// internal node is read once per child — mount-time cost that the buffer cache
|
|
// largely absorbs, in exchange for bounded memory.
|
|
// Walk one root, marking everything it reaches. Snapshot records encountered
|
|
// on the way are collected into `sr` when it is non-null.
|
|
static int
|
|
scan_root(struct schfs_vol *v, uint64_t root, uint32_t root_crc,
|
|
struct snap_roots *sr)
|
|
{
|
|
struct schfs_space *s = &v->space;
|
|
void *blk = v->ctx.a;
|
|
|
|
struct {
|
|
uint64_t nr;
|
|
uint32_t crc;
|
|
int idx; /* next child to descend into */
|
|
} path[SCHFS_MAX_LEVEL + 1];
|
|
|
|
int d = 0;
|
|
path[0].nr = root;
|
|
path[0].crc = root_crc;
|
|
path[0].idx = 0;
|
|
schfs_space_mark(s, root, 1);
|
|
|
|
for (;;) {
|
|
if (schfs_node_read(&v->dev, path[d].nr, path[d].crc, blk) != 0)
|
|
return -2;
|
|
|
|
if (SCHFS_HDR(blk)->level == 0) {
|
|
mark_leaf_extents(s, blk);
|
|
if (sr) {
|
|
collect_snap_roots(sr, blk);
|
|
collect_orphans(v, blk);
|
|
}
|
|
d--;
|
|
} else if (path[d].idx < SCHFS_HDR(blk)->nritems) {
|
|
struct schfs_kptr *kp = &SCHFS_KPTRS(blk)[path[d].idx];
|
|
path[d].idx++;
|
|
if (d + 1 > SCHFS_MAX_LEVEL)
|
|
return -2;
|
|
d++;
|
|
path[d].nr = kp->blocknr;
|
|
path[d].crc = kp->crc;
|
|
path[d].idx = 0;
|
|
schfs_space_mark(s, path[d].nr, 1);
|
|
continue;
|
|
} else {
|
|
d--; /* every child visited */
|
|
}
|
|
|
|
if (d < 0)
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// Walk the live tree, then every snapshot's tree.
|
|
//
|
|
// The snapshots have to be walked or the derivation is actively dangerous: a
|
|
// block reachable only from a snapshot would look free, be handed out, and
|
|
// overwrite the very data the snapshot exists to keep. Nothing would complain
|
|
// at the time -- the corruption surfaces later, when the snapshot is read.
|
|
//
|
|
// The live tree is walked first because that is where the snapshot records
|
|
// live; a snapshot's own tree may contain older snapshot records, which are
|
|
// deliberately not followed. They name roots the live tree no longer keeps,
|
|
// and a snapshot that was deleted should not be resurrected by an older
|
|
// snapshot's memory of it.
|
|
int
|
|
schfs_space_scan(struct schfs_vol *v)
|
|
{
|
|
struct snap_roots sr;
|
|
sr.n = 0;
|
|
sr.overflow = 0;
|
|
sr.newest = 0;
|
|
v->norphans = 0;
|
|
v->orphans_more = 0;
|
|
|
|
int r = scan_root(v, v->tree.root, v->tree.root_crc, &sr);
|
|
if (r != 0)
|
|
return r;
|
|
|
|
if (sr.n > 0 || sr.overflow) {
|
|
v->space.flags |= SCHFS_SPACE_RETAIN_ALL;
|
|
v->space.snap_gen = sr.newest;
|
|
}
|
|
|
|
for (int i = 0; i < sr.n; i++) {
|
|
r = scan_root(v, sr.nr[i], sr.crc[i], 0);
|
|
if (r != 0)
|
|
return r;
|
|
}
|
|
|
|
// Refusing is the only safe answer. Carrying on would mean deriving a free
|
|
// map that considers a live snapshot's blocks available.
|
|
if (sr.overflow)
|
|
return -2;
|
|
|
|
return 0;
|
|
}
|