// 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. // // One filesystem block, which is also one kalloc page: this said 2048 while // the comment above it said one page, so half of every bounce buffer went // unused and every read did twice the work it needed to. // // The chunk size is not a copy-size tuning knob. Each trip through the loop // takes the volume sleeplock and builds a fresh B-tree cursor to find the // extent for that offset, so this is really "how many tree descents per byte // read". Measured on an 834 KB file: 417 descents at 2048, 209 at 4096. // // It must not exceed a page, because the buffer is a single kalloc(). #define BOUNCE SCHFS_BLKSIZE 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, };