An x86-64 operating system built from xv6, now largely rewritten: SMP, a copy-on-write root filesystem (SchFS), PCI/USB/virtio drivers, a TTF rasteriser, and a Linux-numbered syscall ABI. No history before this point -- the tree was never under version control. This commit exists so the rename that follows has something to undo to.
414 lines
13 KiB
C
414 lines
13 KiB
C
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
// SchFS behind the VFS: the fs_ops table that makes SchFS files reachable
|
|
// through open(2), read(2), write(2) and the rest.
|
|
//
|
|
// Everything below is translation, not filesystem logic. The filesystem lives
|
|
// in btree.c/space.c/vol.c/obj.c/objw.c and is driven through the schfs_k*
|
|
// entry points in kmount.c, each of which is one transaction. This file's whole
|
|
// job is turning a `struct inode` into an object id and back.
|
|
//
|
|
// ---------------------------------------------------------------------------
|
|
// The two models, and where they disagree
|
|
//
|
|
// The VFS inherited xv6's shape: allocate an inode, fill it in, then link a
|
|
// name to it. SchFS has no free-standing inode — an object exists because a
|
|
// record with its id exists — so schfs_ialloc writes an inode record with
|
|
// nlink 0 and no name at all. Nothing can reach it until dirlink adds an entry.
|
|
// If the caller dies in between, the record is unreferenced but harmless, and
|
|
// it never reaches the disk anyway: nothing is durable until the transaction
|
|
// that created it commits.
|
|
//
|
|
// The other disagreement is `nlink`. The VFS adjusts it itself (create sets it
|
|
// to 1, link bumps it, unlink drops it) and then calls iupdate. So the object
|
|
// layer's own link accounting is deliberately bypassed here: schfs_dir_add and
|
|
// schfs_dir_del are used rather than schfs_link and schfs_unlink, and iput's
|
|
// ifree hook is what releases an object once the VFS has decided it has no
|
|
// names left. Two pieces of code counting links independently is precisely how
|
|
// a filesystem loses a file that is still referenced.
|
|
|
|
#include "types.h"
|
|
#include "x86.h"
|
|
#include "defs.h"
|
|
#include "param.h"
|
|
#include "stat.h"
|
|
#include "spinlock.h"
|
|
#include "proc.h" /* `proc` — copyin/copyout need the current pgdir */
|
|
#include "sleeplock.h"
|
|
#include "fs.h"
|
|
#include "buf.h"
|
|
#include "file.h"
|
|
|
|
#include "schfs_int.h"
|
|
|
|
// The VFS talks in T_*; the disk format uses SCHFS_T_*. Neither is going to
|
|
// change to suit the other, so the mapping lives here, once, in both
|
|
// directions rather than as an assumption that the numbers happen to line up.
|
|
static short
|
|
type_to_vfs(uint16_t t)
|
|
{
|
|
switch (t) {
|
|
case SCHFS_T_DIR: return T_DIR;
|
|
case SCHFS_T_DEV: return T_DEV;
|
|
case SCHFS_T_SYMLINK: return T_SYMLINK;
|
|
default: return T_FILE;
|
|
}
|
|
}
|
|
|
|
static uint16_t
|
|
type_from_vfs(short t)
|
|
{
|
|
switch (t) {
|
|
case T_DIR: return SCHFS_T_DIR;
|
|
case T_DEV: return SCHFS_T_DEV;
|
|
case T_SYMLINK: return SCHFS_T_SYMLINK;
|
|
default: return SCHFS_T_FILE;
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Inode load / store / free */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static void
|
|
schfs_vfs_iload(struct inode *ip)
|
|
{
|
|
struct schfs_inode in;
|
|
|
|
if (schfs_kstat(ip->inum, &in) != 0) {
|
|
// An unreadable inode on the root filesystem is a panic in v1 because the
|
|
// system cannot continue without it. Here it is a mounted volume that may
|
|
// simply be damaged, so report an empty file and let the caller fail
|
|
// normally rather than taking the machine down with it.
|
|
ip->type = T_FILE;
|
|
ip->nlink = 0;
|
|
ip->size = 0;
|
|
ip->valid = 1;
|
|
return;
|
|
}
|
|
|
|
ip->type = type_to_vfs(in.type);
|
|
ip->major = (short)in.rdev_major;
|
|
ip->minor = (short)in.rdev_minor;
|
|
ip->nlink = (short)in.nlink;
|
|
ip->size = in.size;
|
|
ip->rwmode = RW;
|
|
ip->supermode = 0;
|
|
ip->showmode = 1;
|
|
ip->atime = (uint)in.atime;
|
|
ip->mtime = (uint)in.mtime;
|
|
ip->ctime = (uint)in.ctime;
|
|
memset(ip->addrs, 0, sizeof(ip->addrs));
|
|
ip->valid = 1;
|
|
}
|
|
|
|
static void
|
|
schfs_vfs_istore(struct inode *ip)
|
|
{
|
|
struct schfs_inode in;
|
|
|
|
// Read-modify-write rather than build-from-scratch: the on-disk record holds
|
|
// fields the VFS has no slot for (uid, gid, the real mode bits, the
|
|
// generation), and rebuilding it here would quietly zero every one of them.
|
|
if (schfs_kstat(ip->inum, &in) != 0) {
|
|
memset(&in, 0, sizeof(in));
|
|
in.mode = ip->type == T_DIR ? 0755 : 0644;
|
|
}
|
|
|
|
in.type = type_from_vfs(ip->type);
|
|
in.rdev_major = (uint16_t)ip->major;
|
|
in.rdev_minor = (uint16_t)ip->minor;
|
|
in.nlink = (uint32_t)ip->nlink;
|
|
in.size = ip->size;
|
|
in.atime = ip->atime;
|
|
in.mtime = ip->mtime;
|
|
in.ctime = ip->ctime;
|
|
|
|
schfs_ksetattr(ip->inum, &in);
|
|
}
|
|
|
|
static void
|
|
schfs_vfs_ifree(struct inode *ip)
|
|
{
|
|
schfs_kfree(ip->inum);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Data */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
// Reads and writes are staged through a bounce buffer rather than going
|
|
// straight to user memory, because the filesystem takes a sleeplock and walks
|
|
// the tree while a copyout could fault. One page at a time keeps that buffer
|
|
// off the 4096-byte kernel stack -- it is kalloc'd -- and bounds how long the
|
|
// volume lock is held.
|
|
#define BOUNCE 2048
|
|
|
|
static int
|
|
schfs_vfs_read(struct inode *ip, int user_dst, uint64 dst, uint64 off, uint n)
|
|
{
|
|
char *tmp = kalloc();
|
|
if (tmp == 0)
|
|
return -1;
|
|
|
|
uint done = 0;
|
|
while (done < n) {
|
|
uint want = n - done;
|
|
if (want > BOUNCE)
|
|
want = BOUNCE;
|
|
|
|
long got = schfs_kread(ip->inum, off + done, tmp, want);
|
|
if (got < 0) { kfree(tmp); return done ? (int)done : -1; }
|
|
if (got == 0) break; /* end of file */
|
|
|
|
if (user_dst) {
|
|
if (copyout(myproc()->pgdir, dst + done, tmp, (uint)got) < 0)
|
|
break;
|
|
} else {
|
|
memmove((char *)(dst + done), tmp, (uint)got);
|
|
}
|
|
done += (uint)got;
|
|
if ((uint)got < want)
|
|
break; /* short read: end of file */
|
|
}
|
|
|
|
kfree(tmp);
|
|
return (int)done;
|
|
}
|
|
|
|
static int
|
|
schfs_vfs_write(struct inode *ip, int user_src, uint64 src, uint64 off, uint n)
|
|
{
|
|
char *tmp = kalloc();
|
|
if (tmp == 0)
|
|
return -1;
|
|
|
|
uint done = 0;
|
|
while (done < n) {
|
|
uint want = n - done;
|
|
if (want > BOUNCE)
|
|
want = BOUNCE;
|
|
|
|
if (user_src) {
|
|
if (copyin(myproc()->pgdir, (addr_t)(src + done), tmp, (uint64)want) < 0)
|
|
break;
|
|
} else {
|
|
memmove(tmp, (char *)(src + done), want);
|
|
}
|
|
|
|
long put = schfs_kwrite(ip->inum, off + done, tmp, want);
|
|
if (put <= 0) break;
|
|
done += (uint)put;
|
|
if ((uint)put < want) break;
|
|
}
|
|
|
|
kfree(tmp);
|
|
if (off + done > ip->size)
|
|
ip->size = off + done;
|
|
return (int)done;
|
|
}
|
|
|
|
static void
|
|
schfs_vfs_truncate(struct inode *ip, uint64 newsize)
|
|
{
|
|
if (schfs_ktruncate(ip->inum, newsize) == 0)
|
|
ip->size = newsize;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Directories */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static struct inode *
|
|
schfs_vfs_dirlookup(struct inode *dp, char *name, uint *poff)
|
|
{
|
|
// "." and ".." are not stored: a directory costs nothing until it has real
|
|
// entries. Both are answered from what the VFS already knows, except ".."
|
|
// at the mount root, which the mount layer redirects before we get here.
|
|
if (name[0] == '.' && name[1] == '\0') {
|
|
if (poff) *poff = 0;
|
|
return iget(dp->dev, dp->inum);
|
|
}
|
|
|
|
unsigned namelen = 0;
|
|
while (name[namelen]) namelen++;
|
|
|
|
if (namelen == 2 && name[0] == '.' && name[1] == '.') {
|
|
struct schfs_inode in;
|
|
if (schfs_kstat(dp->inum, &in) != 0)
|
|
return 0;
|
|
if (poff) *poff = 0;
|
|
return iget(dp->dev, in.parent ? in.parent : dp->inum);
|
|
}
|
|
|
|
uint64 ino = schfs_klookup_at(dp->inum, name, namelen);
|
|
if (ino == 0)
|
|
return 0;
|
|
// poff is v1's byte offset into the directory file. SchFS finds an entry by
|
|
// name, so there is nothing meaningful to hand back; dirunlink below takes
|
|
// the name and ignores it.
|
|
if (poff) *poff = 0;
|
|
return iget(dp->dev, ino);
|
|
}
|
|
|
|
static int
|
|
schfs_vfs_dirlink(struct inode *dp, char *name, uint64 inum)
|
|
{
|
|
unsigned namelen = 0;
|
|
while (name[namelen]) namelen++;
|
|
if (namelen == 0 || namelen > SCHFS_NAME_MAX)
|
|
return -1;
|
|
|
|
struct schfs_inode in;
|
|
if (schfs_kstat(inum, &in) != 0)
|
|
return -1;
|
|
|
|
int r = schfs_kdiradd(dp->inum, name, namelen, inum, in.type);
|
|
if (r != 0)
|
|
return -1;
|
|
|
|
// A subdirectory's ".." is synthesised from its parent field, so moving or
|
|
// creating one has to set that field or the new directory reports the wrong
|
|
// parent -- and every ".." walk out of it lands somewhere else entirely.
|
|
if (in.type == SCHFS_T_DIR && in.parent != dp->inum) {
|
|
in.parent = dp->inum;
|
|
schfs_ksetattr(inum, &in);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
schfs_vfs_dirunlink(struct inode *dp, char *name, uint off)
|
|
{
|
|
(void)off;
|
|
unsigned namelen = 0;
|
|
while (name[namelen]) namelen++;
|
|
return schfs_kunlink(dp->inum, name, namelen) == 0 ? 0 : -1;
|
|
}
|
|
|
|
static int
|
|
schfs_vfs_dirempty(struct inode *dp)
|
|
{
|
|
return schfs_kdirempty(dp->inum);
|
|
}
|
|
|
|
static struct inode *
|
|
schfs_vfs_ialloc(uint dev, short type)
|
|
{
|
|
uint64 ino = 0;
|
|
if (schfs_kalloc_object(type_from_vfs(type), type == T_DIR ? 0755 : 0644,
|
|
&ino) != 0)
|
|
return 0;
|
|
return iget(dev, ino);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* getdents64 */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
// One record's worth of state for the callback below.
|
|
//
|
|
// Entries come back in name-hash order, which has nothing to do with their
|
|
// names -- POSIX promises no order, and hash order is what makes a lookup one
|
|
// tree descent instead of a scan. The position handed back to userland is
|
|
// therefore an *index*: entry N of the walk. Resuming means walking to N again,
|
|
// which readdir already does linearly, so a full listing stays linear overall
|
|
// only because the common case is resuming exactly where the last call stopped.
|
|
struct gd_ctx {
|
|
char *out;
|
|
int cap;
|
|
int written;
|
|
uint64 skip; // entries to step over before emitting
|
|
uint64 index; // entries seen so far
|
|
int full;
|
|
};
|
|
|
|
static int
|
|
gd_entry(uint64 ino, unsigned short type, const char *name, unsigned namelen,
|
|
void *arg)
|
|
{
|
|
struct gd_ctx *g = arg;
|
|
|
|
if (g->index++ < g->skip)
|
|
return 0;
|
|
if (g->full)
|
|
return 1;
|
|
|
|
if (namelen > 255)
|
|
namelen = 255;
|
|
int reclen = (8 + 8 + 2 + 1 + (int)namelen + 1 + 7) & ~7;
|
|
if (g->written + reclen > g->cap) {
|
|
g->full = 1;
|
|
return 1; /* stop; the caller will come back */
|
|
}
|
|
|
|
char *k = g->out + g->written;
|
|
uint64 *p64 = (uint64 *)k;
|
|
p64[0] = ino;
|
|
p64[1] = g->index; /* resume position: entries consumed */
|
|
*(uint16 *)(k + 16) = (uint16)reclen;
|
|
*(uint8 *)(k + 18) = type == SCHFS_T_DIR ? 4 /* DT_DIR */
|
|
: type == SCHFS_T_DEV ? 2 /* DT_CHR */
|
|
: type == SCHFS_T_SYMLINK ? 10 /* DT_LNK */
|
|
: 8; /* DT_REG */
|
|
memmove(k + 19, name, namelen);
|
|
memset(k + 19 + namelen, 0, reclen - 19 - namelen);
|
|
g->written += reclen;
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
schfs_vfs_getdents(struct inode *dp, char *out, int cap, uint64 *poff)
|
|
{
|
|
struct gd_ctx g;
|
|
g.out = out;
|
|
g.cap = cap;
|
|
g.written = 0;
|
|
g.skip = *poff;
|
|
g.index = 0;
|
|
g.full = 0;
|
|
|
|
// "." and ".." are synthesised: they are not records, so a walk would never
|
|
// produce them, and every readdir caller expects both.
|
|
static const char *dots[2] = { ".", ".." };
|
|
for (int i = 0; i < 2; i++) {
|
|
if (g.index++ < g.skip)
|
|
continue;
|
|
int reclen = (8 + 8 + 2 + 1 + (i + 1) + 1 + 7) & ~7;
|
|
if (g.written + reclen > cap)
|
|
return g.written;
|
|
char *k = out + g.written;
|
|
uint64 *p64 = (uint64 *)k;
|
|
p64[0] = dp->inum; /* close enough for ".."; nothing reads it */
|
|
p64[1] = g.index;
|
|
*(uint16 *)(k + 16) = (uint16)reclen;
|
|
*(uint8 *)(k + 18) = 4; /* DT_DIR */
|
|
memmove(k + 19, dots[i], i + 1);
|
|
memset(k + 19 + i + 1, 0, reclen - 19 - i - 1);
|
|
g.written += reclen;
|
|
}
|
|
|
|
if (schfs_kreaddir(dp->inum, gd_entry, &g) != 0 && g.written == 0)
|
|
return -1;
|
|
|
|
*poff = g.index;
|
|
return g.written;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
const struct fs_ops schfs_ops = {
|
|
.name = "schfs",
|
|
.iload = schfs_vfs_iload,
|
|
.istore = schfs_vfs_istore,
|
|
.ifree = schfs_vfs_ifree,
|
|
.read = schfs_vfs_read,
|
|
.write = schfs_vfs_write,
|
|
.truncate = schfs_vfs_truncate,
|
|
.dirlookup = schfs_vfs_dirlookup,
|
|
.dirlink = schfs_vfs_dirlink,
|
|
.dirunlink = schfs_vfs_dirunlink,
|
|
.dirempty = schfs_vfs_dirempty,
|
|
.ialloc = schfs_vfs_ialloc,
|
|
.getdents = schfs_vfs_getdents,
|
|
};
|