schfs: mount a snapshot by name, read-only
`mount -t schfs @golden /mnt` mounts a snapshot. The source argument
finally means something: sys_mount ignored it outright, because there was
one volume and nothing to choose between. `@name` rather than a path
because a snapshot is not a device -- there is no node in /dev for it and
there should not be, since it lives inside the volume already mounted.
A second volume rather than a parameter threaded through all 23 schfs_k*
entry points. Only the read side can ever address it, so the write path
keeps talking to the live volume and needed no change; the four read
entry points grew _dev variants and pick the volume from the device the
inode already carries. One fs_ops table serves both, because every entry
takes an inode and dispatches on it -- a second table would be the same
pointers under a different name.
It needs its own device number: a snapshot's object ids are the *same
numbers* as the live tree's, so one device would make the two views alias
in the inode cache and hand a reader whichever was cached first.
Three things this turned up, none of them new but all of them reachable
for the first time:
- sys_mount refused with `dev == SCHFS_DEV`, meaning "do not mount SchFS
onto SchFS". A snapshot's mount point is necessarily on the live root,
so that refused the entire feature. The real condition is mounting a
device onto *itself*, which is `dev == fsdev`.
- createat allocates an inode and then panics if dirlink fails --
reasonable about a writable filesystem, a crash on a read-only one.
The refusal now happens before anything is allocated. Unlink and
rename already handled a refusal properly.
- a refused create reported ENOENT, so writing to a snapshot said "No
such file or directory" -- the sentence a shell prints for every
create failure. It reports EROFS now.
Verified in the running system against an image built with
--snapshot golden: the mount succeeds, ls lists the snapshot's tree,
/mnt2/etc/motd reads back through it, and a write is refused with
"Read-only file system".
One at a time, and no umount path for it yet: mount_del_by_root removes
the mount table entry, but ksnap_unmount is only called on the failure
paths. schfs-check passes; bootcheck 108 [ok].
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
36614b69c5
commit
3919f1446e
6 changed files with 345 additions and 18 deletions
|
|
@ -36,6 +36,7 @@
|
|||
#include "fs.h"
|
||||
#include "buf.h"
|
||||
|
||||
#include "file.h" /* SCHFS_DEV, SCHFS_SNAP_DEV */
|
||||
#include "schfs_int.h"
|
||||
|
||||
// A SchFS block is 4096 bytes and a buffer-cache block is BSIZE (1024), so each
|
||||
|
|
@ -52,6 +53,16 @@ static struct schfs_kdev kdev;
|
|||
static struct schfs_vol kvol;
|
||||
static int kmounted;
|
||||
|
||||
// A mounted snapshot: the same block device with an older tree root, read-only.
|
||||
//
|
||||
// A second volume rather than a parameter threaded through all 23 entry points,
|
||||
// because only the read side can ever address it -- a snapshot cannot be
|
||||
// written to, so every mutation keeps talking to kvol and needs no change.
|
||||
static struct schfs_vol ksnapvol;
|
||||
static int ksnap_mounted;
|
||||
static char *ksnapcurblk;
|
||||
static char *ksnapdatablk;
|
||||
|
||||
// Block buffers, allocated once at mount.
|
||||
//
|
||||
// Nothing in SchFS may put a block on the stack: a cursor needs a whole
|
||||
|
|
@ -250,6 +261,181 @@ schfs_kmount(uint dev)
|
|||
(int)kvol.generation);
|
||||
}
|
||||
|
||||
// Which volume a device number names. The VFS hands an inode to every fs_ops
|
||||
// call and an inode carries its device, so this is all the dispatch a
|
||||
// read-only second view needs.
|
||||
static struct schfs_vol *
|
||||
vol_for(uint dev)
|
||||
{
|
||||
return dev == SCHFS_SNAP_DEV ? &ksnapvol : &kvol;
|
||||
}
|
||||
|
||||
static char *
|
||||
curblk_for(uint dev)
|
||||
{
|
||||
return dev == SCHFS_SNAP_DEV ? ksnapcurblk : kcurblk;
|
||||
}
|
||||
|
||||
static char *
|
||||
datablk_for(uint dev)
|
||||
{
|
||||
return dev == SCHFS_SNAP_DEV ? ksnapdatablk : kdatablk;
|
||||
}
|
||||
|
||||
static int
|
||||
mounted_for(uint dev)
|
||||
{
|
||||
return dev == SCHFS_SNAP_DEV ? ksnap_mounted : kmounted;
|
||||
}
|
||||
|
||||
// Mount a snapshot by name, read-only.
|
||||
//
|
||||
// The snapshot's records live in the *live* tree, so the name is resolved
|
||||
// there; what comes back is a root, and the second volume is a copy of the
|
||||
// first with its tree pointed at it. Nothing else differs -- same device, same
|
||||
// node format, same checksums.
|
||||
int
|
||||
schfs_ksnap_mount(const char *name, unsigned namelen)
|
||||
{
|
||||
if (!kmounted)
|
||||
return -1;
|
||||
if (ksnap_mounted)
|
||||
return -2; /* one at a time, for now */
|
||||
|
||||
if (ksnapcurblk == 0) ksnapcurblk = kalloc();
|
||||
if (ksnapdatablk == 0) ksnapdatablk = kalloc();
|
||||
if (ksnapcurblk == 0 || ksnapdatablk == 0)
|
||||
return -3;
|
||||
|
||||
acquiresleep(&schfs_lock);
|
||||
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &kvol.tree, kcurblk);
|
||||
|
||||
struct schfs_key k;
|
||||
schfs_mkkey(&k, SCHFS_SNAP_OBJECTID, SCHFS_KEY_SNAP, 0);
|
||||
|
||||
int found = 0;
|
||||
struct schfs_snap sn;
|
||||
int r = schfs_cursor_seek(&c, &k);
|
||||
while (r == 0) {
|
||||
const struct schfs_key *got = schfs_cursor_key(&c);
|
||||
if (!got || got->objectid != SCHFS_SNAP_OBJECTID ||
|
||||
got->type != SCHFS_KEY_SNAP)
|
||||
break;
|
||||
|
||||
unsigned len;
|
||||
const struct schfs_snap *p = (const struct schfs_snap *)
|
||||
schfs_cursor_val(&c, &len);
|
||||
if (p && len == sizeof(*p)) {
|
||||
unsigned i = 0;
|
||||
while (i < namelen && i < SCHFS_SNAP_NAME_MAX && p->name[i] == name[i])
|
||||
i++;
|
||||
if (i == namelen &&
|
||||
(namelen == SCHFS_SNAP_NAME_MAX || p->name[namelen] == 0)) {
|
||||
sn = *p;
|
||||
found = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
r = schfs_cursor_next(&c);
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
releasesleep(&schfs_lock);
|
||||
return -4;
|
||||
}
|
||||
|
||||
// Same volume, older root. The device, the space map and the block callbacks
|
||||
// are all the live volume's -- a snapshot is not a separate filesystem, it is
|
||||
// a second way into this one.
|
||||
ksnapvol = kvol;
|
||||
ksnapvol.tree.root = sn.root;
|
||||
ksnapvol.tree.root_crc = sn.root_crc;
|
||||
ksnapvol.tree.root_gen = sn.root_generation;
|
||||
// Its own scratch, or a read through the snapshot would tread on a cursor
|
||||
// the live volume still has positioned.
|
||||
ksnapvol.ctx.a = ksnapcurblk;
|
||||
|
||||
ksnap_mounted = 1;
|
||||
releasesleep(&schfs_lock);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void
|
||||
schfs_ksnap_unmount(void)
|
||||
{
|
||||
acquiresleep(&schfs_lock);
|
||||
ksnap_mounted = 0;
|
||||
releasesleep(&schfs_lock);
|
||||
}
|
||||
|
||||
uint64
|
||||
schfs_ksnap_root(void)
|
||||
{
|
||||
return ksnap_mounted ? ksnapvol.root_objectid : 0;
|
||||
}
|
||||
|
||||
// The read path, addressable per device so a snapshot can be read through the
|
||||
// same VFS. Each is the existing call with the volume chosen rather than
|
||||
// assumed; the originals below stay as the live-volume spelling, because most
|
||||
// callers have no business knowing there is a choice.
|
||||
long
|
||||
schfs_kread_dev(uint dev, uint64_t ino, uint64_t off, void *buf, unsigned n)
|
||||
{
|
||||
if (!mounted_for(dev))
|
||||
return -1;
|
||||
acquiresleep(&schfs_lock);
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &vol_for(dev)->tree, curblk_for(dev));
|
||||
long r = schfs_file_read(vol_for(dev), &c, ino, off, buf, n, datablk_for(dev));
|
||||
releasesleep(&schfs_lock);
|
||||
return r;
|
||||
}
|
||||
|
||||
int
|
||||
schfs_kstat_dev(uint dev, uint64_t ino, struct schfs_inode *out)
|
||||
{
|
||||
if (!mounted_for(dev))
|
||||
return -1;
|
||||
acquiresleep(&schfs_lock);
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &vol_for(dev)->tree, curblk_for(dev));
|
||||
int r = schfs_inode_get(vol_for(dev), &c, ino, out);
|
||||
releasesleep(&schfs_lock);
|
||||
return r;
|
||||
}
|
||||
|
||||
uint64
|
||||
schfs_klookup_at_dev(uint dev, uint64_t dir, const char *name, unsigned len)
|
||||
{
|
||||
if (!mounted_for(dev))
|
||||
return 0;
|
||||
acquiresleep(&schfs_lock);
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &vol_for(dev)->tree, curblk_for(dev));
|
||||
uint64_t ino = 0;
|
||||
uint16_t type = 0;
|
||||
int r = schfs_dir_lookup(vol_for(dev), &c, dir, name, len, &ino, &type);
|
||||
releasesleep(&schfs_lock);
|
||||
return r == 0 ? ino : 0;
|
||||
}
|
||||
|
||||
int
|
||||
schfs_kreaddir_dev(uint dev, uint64_t dir,
|
||||
int (*fn)(uint64_t, uint16_t, const char *, unsigned, void *),
|
||||
void *arg)
|
||||
{
|
||||
if (!mounted_for(dev))
|
||||
return -1;
|
||||
acquiresleep(&schfs_lock);
|
||||
struct schfs_cursor c;
|
||||
schfs_cursor_init(&c, &vol_for(dev)->tree, curblk_for(dev));
|
||||
int r = schfs_dir_iterate(vol_for(dev), &c, dir, fn, arg);
|
||||
releasesleep(&schfs_lock);
|
||||
return r;
|
||||
}
|
||||
|
||||
int
|
||||
schfs_kmounted(void)
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue