`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>
139 lines
5.3 KiB
C
139 lines
5.3 KiB
C
struct file {
|
|
enum { FD_NONE, FD_PIPE, FD_INODE, FD_SOCKET } type;
|
|
int ref; // reference count
|
|
char readable;
|
|
char writable;
|
|
// O_NONBLOCK. A property of the open file description, not of the
|
|
// descriptor, which is why it lives here and not in myproc()->ofile_flags --
|
|
// dup() shares it and F_SETFL on either fd changes it for both, exactly as
|
|
// POSIX says.
|
|
char nonblock;
|
|
struct pipe *pipe;
|
|
struct inode *ip;
|
|
struct socket *sock;
|
|
uint64 off;
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Filesystem operations
|
|
//
|
|
// One table per filesystem, chosen by iget() from the device number and hung
|
|
// off the inode. Everything above this line -- the syscall layer, exec, the
|
|
// page cache -- calls the wrappers in fs.c and never learns which filesystem it
|
|
// is talking to.
|
|
//
|
|
// A vtable rather than a chain of `if (dev == SCHFS_DEV)` because there are two
|
|
// filesystems today and there will be more: the whole point of the exercise is
|
|
// that adding one is a new table, not another branch in fifteen functions.
|
|
//
|
|
// Every hook takes an inode that is already locked, except ialloc (which has no
|
|
// inode yet) and iload (which is what makes one valid in the first place).
|
|
// ---------------------------------------------------------------------------
|
|
|
|
struct stat;
|
|
|
|
struct fs_ops {
|
|
const char *name;
|
|
|
|
// Fill in the type/size/link fields of a freshly iget()'d inode.
|
|
void (*iload)(struct inode *);
|
|
// Write those fields back to disk.
|
|
void (*istore)(struct inode *);
|
|
// The last link is gone and nobody has it open: release the object.
|
|
void (*ifree)(struct inode *);
|
|
|
|
int (*read)(struct inode *, int user_dst, uint64 dst, uint64 off, uint n);
|
|
int (*write)(struct inode *, int user_src, uint64 src, uint64 off, uint n);
|
|
// Set the length. Growing is allowed only where the filesystem can represent
|
|
// the gap; v1 cannot and ignores it.
|
|
void (*truncate)(struct inode *, uint64 newsize);
|
|
|
|
// Directory operations. `poff` is an opaque cookie identifying where the
|
|
// entry lives, meaningful only to the same filesystem's dirunlink.
|
|
struct inode *(*dirlookup)(struct inode *dp, char *name, uint *poff);
|
|
int (*dirlink)(struct inode *dp, char *name, uint64 inum);
|
|
int (*dirunlink)(struct inode *dp, char *name, uint off);
|
|
int (*dirempty)(struct inode *dp);
|
|
|
|
// Create an object with no name yet. The caller sets nlink and calls
|
|
// dirlink; until it does, the object is unreachable.
|
|
struct inode *(*ialloc)(uint dev, short type);
|
|
|
|
// Read directory entries into a getdents64-shaped stream. `off` is an opaque
|
|
// position that starts at 0 and is echoed back; returns bytes written, 0 at
|
|
// the end of the directory, or negative on error.
|
|
int (*getdents)(struct inode *dp, char *out, int cap, uint64 *off);
|
|
};
|
|
|
|
// In-memory inode.
|
|
//
|
|
// `inum` and `size` are 64-bit because the filesystem underneath may be: SchFS
|
|
// object ids and file sizes are both uint64 on disk, and a 32-bit copy here
|
|
// would silently truncate them into a different file. The v1 filesystem uses a
|
|
// small fraction of the range and is unaffected.
|
|
struct inode {
|
|
uint dev;
|
|
uint64 inum;
|
|
int ref;
|
|
struct sleeplock lock;
|
|
int valid;
|
|
const struct fs_ops *ops; // chosen by iget from dev; never null once held
|
|
|
|
short type;
|
|
short major;
|
|
short minor;
|
|
short nlink;
|
|
uint64 size;
|
|
|
|
uint rwmode;
|
|
uint supermode;
|
|
uint showmode;
|
|
uint atime;
|
|
uint mtime;
|
|
uint ctime;
|
|
uint addrs[NDIRECT+3];
|
|
};
|
|
#define I_VALID 0x2
|
|
|
|
// Device number that means "the mounted SchFS volume". Real disks are 0 and 1,
|
|
// and this is deliberately far from them so a stray value never looks like a
|
|
// SchFS inode.
|
|
#define SCHFS_DEV 64
|
|
|
|
// Device number that means "the synthetic /proc". Like SCHFS_DEV, it is
|
|
// deliberately nowhere near a real disk's 0 or 1.
|
|
// A mounted snapshot. Same block device, a different tree root, read-only.
|
|
// It needs its own device number because an inode is identified by (dev, inum)
|
|
// and a snapshot's object ids are the *same numbers* as the live tree's -- one
|
|
// number would make the two views alias in the inode cache and hand a reader
|
|
// whichever was cached first.
|
|
#define SCHFS_SNAP_DEV 66
|
|
|
|
#define PROCFS_DEV 65
|
|
|
|
// table mapping major device number to
|
|
// device functions
|
|
struct devsw {
|
|
int (*read)(struct inode*, uint, char*, int);
|
|
int (*write)(struct inode*, uint, char*, int);
|
|
// Optional. Returns 0 when a read would block, which is the only thing
|
|
// fileread needs to know to honour O_NONBLOCK without every device driver
|
|
// growing a flags argument. A device that omits this is treated as always
|
|
// ready, which is the right answer for a disk and for /dev/zero.
|
|
int (*ready)(struct inode*);
|
|
// Optional. The last descriptor for this open file is going away. A device
|
|
// that hands a program exclusive-feeling control of the hardware needs this
|
|
// to put the hardware back -- /dev/fb0 uses it to give the text console its
|
|
// screen back when whatever was drawing on it exits.
|
|
void (*close)(struct inode*);
|
|
// Optional. A descriptor has just been opened on this device. /dev/input
|
|
// uses it to discard events that arrived while nobody was listening -- see
|
|
// inputopen().
|
|
void (*open)(struct inode*);
|
|
};
|
|
|
|
extern struct devsw devsw[];
|
|
|
|
#define CONSOLE 1
|
|
// Major number of /dev/input/eventN. The minor is the device index.
|
|
#define INPUT_MAJOR 7
|