Two loose ends from the mount-by-name commit, both closed. A slot is a device number, so supporting more than one snapshot is a small array rather than a redesign: SCHFS_SNAP_DEV through SCHFS_SNAP_DEV + SCHFS_SNAP_SLOTS - 1, each with its own volume and its own scratch pages, allocated on first use. Four of them -- "one at a time" is an odd thing to have to explain, and the cost is two pages per slot. Not many, because each one is a device number and those are a small fixed space. The leak mattered more. umount removed the mount table entry and left the slot marked in use, so a snapshot could be unmounted and then never mounted again until reboot. sys_umount2 releases it now, keyed off the device it already has in hand. Verified in the running system: two snapshots mounted at once, both readable, both unmounted, and a third mount afterwards succeeds and reads correctly -- which is the case that failed before. schfs-check passes; bootcheck 108 [ok]. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
148 lines
5.7 KiB
C
148 lines
5.7 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.
|
|
// Mounted snapshots. Same block device, an older tree root, read-only.
|
|
//
|
|
// Each 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 views alias in the inode cache and hand
|
|
// a reader whichever was cached first. So a slot is a device: SCHFS_SNAP_DEV
|
|
// through SCHFS_SNAP_DEV + SCHFS_SNAP_SLOTS - 1.
|
|
//
|
|
// Four rather than one because "one at a time" is an odd thing to explain and
|
|
// the cost is two scratch pages per slot, allocated on first use. Four rather
|
|
// than many because each is a device number and they are a fixed, small space.
|
|
#define SCHFS_SNAP_SLOTS 4
|
|
#define SCHFS_SNAP_DEV 66
|
|
#define SCHFS_IS_SNAP_DEV(d) \
|
|
((d) >= SCHFS_SNAP_DEV && (d) < SCHFS_SNAP_DEV + SCHFS_SNAP_SLOTS)
|
|
|
|
#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
|