Initial commit: Schunix as it stands before the SchOS rename

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.
This commit is contained in:
Nexoniarz 2026-08-09 13:47:01 +02:00
commit d0f7acf9eb
275 changed files with 51417 additions and 0 deletions

241
kernel/fs/exec.c Normal file
View file

@ -0,0 +1,241 @@
#include "types.h"
#include "traps.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "proc.h"
#include "defs.h"
#include "x86.h"
#include "elf.h"
#include "fcntl.h"
#include "errno.h"
// Binaries with entry points at 0x100000+ are musl/Linux-ABI.
// Schunix-native binaries (init) are linked at 0x4000.
#define LINUX_ABI_ENTRY_THRESHOLD 0x100000UL
extern int linux_syscall_map[];
extern int linux_syscall_map_size;
int
exec(char *path, char **argv)
{
char *s, *last;
int i, off;
uint64 argc, sz, sp, ustack[MAXARG+1];
struct elfhdr elf;
struct inode *ip;
struct proghdr ph;
pml4e_t *pgdir, *oldpgdir;
uint64 stack_slots[MAXARG + 32];
int slot;
uint64 max_sz = 0;
uint64 rand_va = 0;
begin_op();
if((ip = namei(path)) == 0){
end_op();
// Must be ENOENT, not a bare -1: execvp() relies on this exact errno to
// know it should try the next $PATH component rather than give up.
return -ENOENT;
}
ilock(ip);
pgdir = 0;
if(readi(ip, 0, (uint64)&elf, 0, sizeof(elf)) != sizeof(elf))
goto bad;
if(elf.magic != ELF_MAGIC)
goto bad;
if((pgdir = setupkvm()) == 0)
goto bad;
for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=elf.phentsize){
if(readi(ip, 0, (uint64)&ph, off, sizeof(ph)) != sizeof(ph))
goto bad;
if(ph.type != ELF_PROG_LOAD)
continue;
if(ph.memsz < ph.filesz)
goto bad;
if(ph.vaddr + ph.memsz < ph.vaddr)
goto bad;
uint64 start = ph.vaddr & ~0xFFF;
uint64 end = ph.vaddr + ph.memsz;
if(allocuvm(pgdir, start, end) == 0)
goto bad;
if(loaduvm(pgdir, (char*)ph.vaddr, ip, ph.off, ph.filesz) < 0)
goto bad;
if(end > max_sz)
max_sz = end;
}
iunlockput(ip);
end_op();
ip = 0;
sz = PGROUNDUP(max_sz);
if((sz = allocuvm(pgdir, sz, sz + 64*PGSIZE)) == 0)
goto bad;
clearpteu(pgdir, (char*)(sz - 64*PGSIZE));
sp = sz;
for(argc = 0; argv[argc]; argc++) {
if(argc >= MAXARG)
goto bad;
sp -= strlen(argv[argc]) + 1;
sp &= ~7;
if(copyout(pgdir, sp, argv[argc], strlen(argv[argc]) + 1) < 0)
goto bad;
ustack[argc] = sp;
}
// Push 16 pseudo-random bytes for AT_RANDOM.
{
uint8 randbuf[16];
uint rseed = (uint)(uintptr_t)myproc() ^ (uint)myproc()->pid;
for(int ri = 0; ri < 16; ri++){
rseed = rseed * 1664525u + 1013904223u;
randbuf[ri] = (uint8)(rseed >> 16);
}
sp -= 16; sp &= ~0xFUL;
rand_va = sp;
if(copyout(pgdir, rand_va, (char*)randbuf, 16) < 0) goto bad;
}
slot = 0;
stack_slots[slot++] = argc;
for(i = 0; i < argc; i++) {
stack_slots[slot++] = ustack[i];
}
stack_slots[slot++] = 0; // end of argv
stack_slots[slot++] = 0; // end of envp (no environment)
// Auxiliary vector — musl reads these for page size, clock, etc.
stack_slots[slot++] = 6; // AT_PAGESZ
stack_slots[slot++] = 4096;
stack_slots[slot++] = 17; // AT_CLKTCK
stack_slots[slot++] = 100;
stack_slots[slot++] = 23; // AT_SECURE = 0 (not setuid)
stack_slots[slot++] = 0;
stack_slots[slot++] = 11; // AT_UID
stack_slots[slot++] = 0;
stack_slots[slot++] = 12; // AT_EUID
stack_slots[slot++] = 0;
stack_slots[slot++] = 13; // AT_GID
stack_slots[slot++] = 0;
stack_slots[slot++] = 14; // AT_EGID
stack_slots[slot++] = 0;
stack_slots[slot++] = 16; // AT_HWCAP
stack_slots[slot++] = 0;
stack_slots[slot++] = 25; // AT_RANDOM
stack_slots[slot++] = rand_va;
stack_slots[slot++] = 0; // AT_NULL
stack_slots[slot++] = 0;
sp -= slot * sizeof(uint64);
sp &= ~0xF;
if(copyout(pgdir, sp, (char*)stack_slots, slot * sizeof(uint64)) < 0)
goto bad;
for(last=s=path; *s; s++)
if(*s == '/')
last = s+1;
safestrcpy(myproc()->name, last, sizeof(myproc()->name));
safestrcpy(myproc()->exe, path, sizeof(myproc()->exe));
for(int i = 0; i < NOFILE; i++){
if(myproc()->ofile[i] && (myproc()->ofile_flags[i] & FD_CLOEXEC)){
fileclose(myproc()->ofile[i]);
myproc()->ofile[i] = 0;
myproc()->ofile_flags[i] = 0;
}
}
// Auto-select syscall ABI: musl binaries have entry >= 0x100000;
// Schunix-native binaries (init) are linked at 0x4000.
if (elf.entry >= LINUX_ABI_ENTRY_THRESHOLD) {
myproc()->syscall_map = linux_syscall_map;
myproc()->map_size = linux_syscall_map_size;
} else {
myproc()->syscall_map = 0;
myproc()->map_size = 0;
}
// Detach shared segments before anything forgets which mappings they were.
//
// This has to happen while myproc()->pgdir and myproc()->vma[] still describe the
// *old* image: the memset below erases the bookkeeping, and freevm() at the
// end would then free a shared memory segment's pages as though they were
// ordinary heap. Doing it here rather than there is the difference between
// detaching a segment and corrupting whoever else has it mapped.
proc_release_shared(myproc());
oldpgdir = myproc()->pgdir;
myproc()->pgdir = pgdir;
myproc()->sz = sz;
// New address space: reset all per-process mmap/signal/TLS state.
// sig_handler/sig_restorer point into the *old* image — POSIX resets
// handled signals to default on exec, and keeping them would make the
// first delivered signal jump to a stale address in the new image.
// mmap_next likewise belongs to the old address space; left alone it
// could hand out mmap ranges overlapping the new image's layout.
memset(myproc()->vma, 0, sizeof(myproc()->vma));
memset(myproc()->sig_handler, 0, sizeof(myproc()->sig_handler));
memset(myproc()->sig_restorer, 0, sizeof(myproc()->sig_restorer));
myproc()->fs_base = 0;
myproc()->signal_mask = 0;
myproc()->pending_signals = 0;
myproc()->in_signal = 0;
myproc()->sigsusp_active = 0;
myproc()->mmap_next = 0;
myproc()->tf->rax = 0;
myproc()->tf->rbx = 0;
myproc()->tf->rdx = 0;
myproc()->tf->rbp = 0;
myproc()->tf->r8 = 0;
myproc()->tf->r9 = 0;
myproc()->tf->r10 = 0;
myproc()->tf->r11 = 0x200; // RFLAGS for sysretq: IF=1 (bit 9) so user gets timer IRQs
myproc()->tf->r12 = 0;
myproc()->tf->r13 = 0;
myproc()->tf->r14 = 0;
myproc()->tf->r15 = 0;
myproc()->tf->rdi = argc;
myproc()->tf->rsi = sp + 8;
myproc()->tf->rip = elf.entry;
myproc()->tf->rcx = elf.entry;
myproc()->tf->rsp = sp;
// syscall_entry saves user RSP at kstack+PGSIZE-16 (not in the trapframe).
// sysretq reads RSP from that slot, so update it to the new stack pointer.
// Without this, sysretq returns with the old parent's RSP, causing lksh to
// start with an invalid/wrong stack pointer.
*(uint64*)((char*)myproc()->kstack + PGSIZE - 16) = sp;
// Userspace is running now, so the splash has done its job and the display
// becomes a terminal. Only the first exec does anything; every later one
// finds the console already in charge.
console_takeover();
switchuvm(myproc());
freevm(oldpgdir);
return 0;
bad:
if(pgdir)
freevm(pgdir);
if(ip){
iunlockput(ip);
end_op();
}
return -1;
}

294
kernel/fs/file.c Normal file
View file

@ -0,0 +1,294 @@
#include "types.h"
#include "defs.h"
#include "stat.h"
#include "param.h"
#include "fs.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "file.h"
#include "posix.h"
#include "errno.h"
extern struct devsw devsw[];
struct {
struct spinlock lock;
struct file file[NFILE];
} ftable;
void
fileinit(void)
{
initlock(&ftable.lock, "ftable");
}
struct file*
filealloc(void)
{
struct file *f;
acquire(&ftable.lock);
for(f = ftable.file; f < ftable.file + NFILE; f++){
if(f->ref == 0){
f->ref = 1;
// Cleared here rather than left to the caller: every creator sets
// readable/writable/off, but only open() sets nonblock, so a struct file
// recycled from a non-blocking device would hand its flag to the next
// pipe or socket that happened to land in this slot.
f->nonblock = 0;
release(&ftable.lock);
return f;
}
}
release(&ftable.lock);
return 0;
}
struct file*
filedup(struct file *f)
{
acquire(&ftable.lock);
if(f->ref < 1)
panic("filedup");
f->ref++;
release(&ftable.lock);
return f;
}
void
fileclose(struct file *f)
{
struct file ff;
acquire(&ftable.lock);
if(f->ref < 1)
panic("fileclose");
if(--f->ref > 0){
release(&ftable.lock);
return;
}
ff = *f;
f->ref = 0;
f->type = FD_NONE;
release(&ftable.lock);
if(ff.type == FD_PIPE)
pipeclose(ff.pipe, ff.writable);
else if(ff.type == FD_SOCKET)
sockclose(ff.sock);
else if(ff.type == FD_INODE){
// Give a device a chance to put the hardware back before the inode goes.
// Done here rather than in exit() because a program that closes the
// framebuffer without exiting has finished with it just as thoroughly.
if(ff.ip && ff.ip->type == T_DEV && ff.ip->major >= 0 &&
ff.ip->major < NDEV && devsw[ff.ip->major].close)
devsw[ff.ip->major].close(ff.ip);
begin_op();
iput(ff.ip);
end_op();
}
}
int
filestat(struct file *f, struct stat *st)
{
if(f->type == FD_INODE){
ilock(f->ip);
stati(f->ip, st);
iunlock(f->ip);
return 0;
}
return -1;
}
int
fileread(struct file *f, char *addr, int n)
{
int r;
if(f->readable == 0)
return -1;
// O_NONBLOCK on a pipe or a socket. Reported here rather than inside
// piperead/sockread so that the blocking implementations stay unchanged:
// this is a check of the same condition their sleep loops wait on, made one
// level up. Writes still block -- fcntl documents that.
if(f->type == FD_PIPE){
if(f->nonblock && !(pipe_poll(f->pipe, f->readable, f->writable) & POLLIN))
return -EAGAIN;
return piperead(f->pipe, addr, n);
}
if(f->type == FD_SOCKET){
if(f->nonblock && !sockreadable(f->sock))
return -EAGAIN;
return sockread(f->sock, addr, n);
}
if(f->type == FD_INODE){
ilock(f->ip);
if(f->ip->type == 3){
if(f->ip->major < 0 || f->ip->major >= NDEV || !devsw[f->ip->major].read){
iunlock(f->ip);
return -1;
}
// O_NONBLOCK. Without this a game that opens an input device
// non-blocking and drains it once per frame stops dead on the first
// frame -- it blocks in read() waiting for a key, and the result looks
// exactly like a hung renderer rather than a blocking read.
if(f->nonblock && devsw[f->ip->major].ready &&
!devsw[f->ip->major].ready(f->ip)){
iunlock(f->ip);
return -EAGAIN;
}
r = devsw[f->ip->major].read(f->ip, f->off, addr, n);
if(r > 0)
f->off += r;
iunlock(f->ip);
return r;
}
if((f->ip->rwmode & 2) == 0){
cprintf("Schunix: permission denied (read)\n");
iunlock(f->ip);
return -1;
}
// The "recycle bin" check that used to sit here is gone.
//
// It made a file that still existed, still had a name and still had blocks
// refuse every read with a printf -- a state no POSIX program is prepared
// for. It was reachable only through `delete`, a NO_LINUX syscall from the
// third-party filesystem this file derives from, which the decision to keep
// only Linux numbering had already orphaned.
//
// The on-disk `showmode` field stays for now: removing it changes the inode
// layout, and this whole file is being replaced by SchFS. It is simply
// never consulted.
if((r = readi(f->ip, 0, (uint64)addr, f->off, n)) > 0)
f->off += r;
iunlock(f->ip);
return r;
}
panic("fileread");
}
int
filewrite(struct file *f, char *addr, int n)
{
int r;
if(f->writable == 0)
return -1;
if(f->type == FD_PIPE)
return pipewrite(f->pipe, addr, n);
if(f->type == FD_SOCKET)
return sockwrite(f->sock, addr, n);
if(f->type == FD_INODE){
ilock(f->ip);
if(f->ip->type == 3){
if(f->ip->major < 0 || f->ip->major >= NDEV || !devsw[f->ip->major].write){
iunlock(f->ip);
return -1;
}
r = devsw[f->ip->major].write(f->ip, f->off, addr, n);
if(r > 0)
f->off += r;
iunlock(f->ip);
return r;
}
if((f->ip->rwmode & 1) == 0){
cprintf("Schunix: permission denied (write)\n");
iunlock(f->ip);
return -1;
}
// Recycle-bin check removed here too; see the note in fileread().
iunlock(f->ip);
int max = ((LOGSIZE-1-1-2) / 2) * 512;
int i = 0;
while(i < n){
int n1 = n - i;
if(n1 > max)
n1 = max;
begin_op();
ilock(f->ip);
if ((r = writei(f->ip, 1, (uint64)(addr + i), f->off, n1)) > 0)
f->off += r;
iunlock(f->ip);
end_op();
if(r < 0)
break;
if(r != n1)
panic("short filewrite");
i += r;
}
return i == n ? n : -1;
}
panic("filewrite");
}
// poll(2) readiness for one open file. Returns a POLL* bitmask restricted
// to the requested events (plus error/hangup conditions, which POSIX says
// are always reported). Pipes and the console tty have real state; other
// inodes (regular files, block-ish devices) are always ready, per POSIX.
int
file_poll(struct file *f, int events)
{
if(f->type == FD_PIPE)
return pipe_poll(f->pipe, f->readable, f->writable) &
(events | POLLERR | POLLHUP);
if(f->type == FD_SOCKET){
int rev = 0;
if(sockreadable(f->sock))
rev |= POLLIN;
// A stream socket is writable whenever it has a peer. The buffer can
// still be full, in which case the write blocks -- same latitude poll()
// has always had, and the same answer a pipe gives.
if(sock_hup(f->sock))
rev |= POLLHUP;
else
rev |= POLLOUT;
return rev & (events | POLLERR | POLLHUP);
}
if(f->type == FD_INODE){
// An input device is only readable when an event is waiting. Reporting it
// always-ready -- which is what the generic answer below does -- turns a
// poll loop into a busy loop that blocks in read().
if(f->ip && f->ip->type == T_DEV && f->ip->major == INPUT_MAJOR){
int rev = 0;
if((events & POLLIN) && f->readable && input_ready(f->ip->minor))
rev |= POLLIN;
if(events & POLLOUT)
rev |= POLLOUT;
return rev;
}
if(f->ip && f->ip->type == T_DEV && f->ip->major == CONSOLE){
int rev = 0;
if((events & POLLIN) && f->readable && console_input_ready())
rev |= POLLIN;
if((events & POLLOUT) && f->writable)
rev |= POLLOUT;
return rev;
}
return events & (POLLIN | POLLOUT);
}
return POLLNVAL;
}

1839
kernel/fs/fs.c Normal file

File diff suppressed because it is too large Load diff

255
kernel/fs/log.c Normal file
View file

@ -0,0 +1,255 @@
#include "types.h"
#include "defs.h"
#include "param.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "buf.h"
// Simple logging that allows concurrent FS system calls.
//
// A log transaction contains the updates of multiple FS system
// calls. The logging system only commits when there are
// no FS system calls active. Thus there is never
// any reasoning required about whether a commit might
// write an uncommitted system call's updates to disk.
//
// A system call should call begin_op()/end_op() to mark
// its start and end. Usually begin_op() just increments
// the count of in-progress FS system calls and returns.
// But if it thinks the log is close to running out, it
// sleeps until the last outstanding end_op() commits.
//
// The log is a physical re-do log containing disk blocks.
// The on-disk log format:
// header block, containing block #s for block A, B, C, ...
// block A
// block B
// block C
// ...
// Log appends are synchronous.
// Contents of the header block, used for both the on-disk header block
// and to keep track in memory of logged block# before commit.
struct logheader {
int n;
int block[LOGSIZE];
};
struct log {
struct spinlock lock;
int start;
int size;
int outstanding; // how many FS sys calls are executing.
int committing; // in commit(), please wait.
int dev;
struct logheader lh;
};
struct log log;
static void recover_from_log(void);
static void commit();
void
initlog(int dev)
{
if (sizeof(struct logheader) >= BSIZE)
panic("initlog: too big logheader");
struct superblock sb;
initlock(&log.lock, "log");
readsb(dev, &sb);
log.start = sb.logstart;
log.size = sb.nlog;
log.dev = dev;
recover_from_log();
}
// Copy committed blocks from log to their home location
static void
install_trans(void)
{
int tail;
for (tail = 0; tail < log.lh.n; tail++) {
struct buf *lbuf = bread(log.dev, log.start+tail+1); // read log block
struct buf *dbuf = bread(log.dev, log.lh.block[tail]); // read dst
memmove(dbuf->data, lbuf->data, BSIZE); // copy block to dst
bwrite(dbuf); // write dst to disk
brelse(lbuf);
brelse(dbuf);
}
}
// Read the log header from disk into the in-memory log header
static void
read_head(void)
{
struct buf *buf = bread(log.dev, log.start);
struct logheader *lh = (struct logheader *) (buf->data);
int i;
log.lh.n = lh->n;
for (i = 0; i < log.lh.n; i++) {
log.lh.block[i] = lh->block[i];
}
brelse(buf);
}
// Write in-memory log header to disk.
// This is the true point at which the
// current transaction commits.
static void
write_head(void)
{
struct buf *buf = bread(log.dev, log.start);
struct logheader *hb = (struct logheader *) (buf->data);
int i;
hb->n = log.lh.n;
for (i = 0; i < log.lh.n; i++) {
hb->block[i] = log.lh.block[i];
}
bwrite(buf);
brelse(buf);
}
static void
recover_from_log(void)
{
read_head();
install_trans(); // if committed, copy from log to disk
log.lh.n = 0;
write_head(); // clear the log
}
// called at the start of each FS system call.
void
begin_op(void)
{
// Nothing to do: the root filesystem is SchFS, which does not use this log.
//
// This is not a shortcut. The log exists to make a multi-block v1 update
// atomic; SchFS gets that from copy-on-write instead -- every schfs_k* call
// that changes anything is one transaction and is durable by the time it
// returns. So there is nothing here for begin_op to protect.
//
// It also cannot do anything: initlog() ran inside fsinit(), which no longer
// runs at all, so the lock below and everything it guards are uninitialised.
// Taking them is where the first SchFS-root boot stopped -- after the mount,
// before init ever ran, looking for all the world like a hang.
//
// The rest of this file is dead with it and goes when the v1 format does.
// begin_op/end_op stay as no-ops meanwhile because sys_fs.c calls them in
// dozens of places, and emptying those out is a separate change.
return;
#if 0
acquire(&log.lock);
while(1){
if(log.committing){
sleep(&log, &log.lock);
} else if(log.lh.n + (log.outstanding+1)*MAXOPBLOCKS > LOGSIZE){
// this op might exhaust log space; wait for commit.
sleep(&log, &log.lock);
} else {
log.outstanding += 1;
release(&log.lock);
break;
}
}
#endif
}
// called at the end of each FS system call.
// commits if this was the last outstanding operation.
void
end_op(void)
{
return; // see begin_op()
#if 0
int do_commit = 0;
acquire(&log.lock);
log.outstanding -= 1;
if(log.committing)
panic("log.committing");
if(log.outstanding == 0){
do_commit = 1;
log.committing = 1;
} else {
// begin_op() may be waiting for log space.
wakeup(&log);
}
release(&log.lock);
if(do_commit){
// call commit w/o holding locks, since not allowed
// to sleep with locks.
commit();
acquire(&log.lock);
log.committing = 0;
wakeup(&log);
release(&log.lock);
}
#endif
}
// Copy modified blocks from cache to log.
static void
write_log(void)
{
int tail;
for (tail = 0; tail < log.lh.n; tail++) {
struct buf *to = bread(log.dev, log.start+tail+1); // log block
struct buf *from = bread(log.dev, log.lh.block[tail]); // cache block
memmove(to->data, from->data, BSIZE);
bwrite(to); // write the log
brelse(from);
brelse(to);
}
}
static void
commit()
{
if (log.lh.n > 0) {
write_log(); // Write modified blocks from cache to log
write_head(); // Write header to disk -- the real commit
install_trans(); // Now install writes to home locations
log.lh.n = 0;
write_head(); // Erase the transaction from the log
}
}
// Caller has modified b->data and is done with the buffer.
// Record the block number and pin in the cache with B_DIRTY.
// commit()/write_log() will do the disk write.
//
// log_write() replaces bwrite(); a typical use is:
// bp = bread(...)
// modify bp->data[]
// log_write(bp)
// brelse(bp)
void
log_write(struct buf *b)
{
int i;
if (log.lh.n >= LOGSIZE || log.lh.n >= log.size - 1)
panic("too big a transaction");
if (log.outstanding < 1)
panic("log_write outside of trans");
acquire(&log.lock);
for (i = 0; i < log.lh.n; i++) {
if (log.lh.block[i] == b->blockno) // log absorbtion
break;
}
log.lh.block[i] = b->blockno;
if (i == log.lh.n)
log.lh.n++;
b->flags |= B_DIRTY; // prevent eviction
release(&log.lock);
}

166
kernel/fs/pipe.c Normal file
View file

@ -0,0 +1,166 @@
#include "types.h"
#include "defs.h"
#include "param.h"
#include "mmu.h"
#include "proc.h"
#include "fs.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "file.h"
#include "posix.h"
#include "errno.h"
#define PIPESIZE 512
struct pipe {
struct spinlock lock;
char data[PIPESIZE];
uint nread; // number of bytes read
uint nwrite; // number of bytes written
int readopen; // read fd is still open
int writeopen; // write fd is still open
};
int
pipealloc(struct file **f0, struct file **f1)
{
struct pipe *p;
p = 0;
*f0 = *f1 = 0;
if((*f0 = filealloc()) == 0 || (*f1 = filealloc()) == 0)
goto bad;
if((p = (struct pipe*)kalloc()) == 0)
goto bad;
p->readopen = 1;
p->writeopen = 1;
p->nwrite = 0;
p->nread = 0;
initlock(&p->lock, "pipe");
(*f0)->type = FD_PIPE;
(*f0)->readable = 1;
(*f0)->writable = 0;
(*f0)->pipe = p;
(*f1)->type = FD_PIPE;
(*f1)->readable = 0;
(*f1)->writable = 1;
(*f1)->pipe = p;
return 0;
//PAGEBREAK: 20
bad:
if(p)
kfree((char*)p);
if(*f0)
fileclose(*f0);
if(*f1)
fileclose(*f1);
return -1;
}
void
pipeclose(struct pipe *p, int writable)
{
acquire(&p->lock);
if(writable){
p->writeopen = 0;
wakeup(&p->nread);
} else {
p->readopen = 0;
wakeup(&p->nwrite);
}
if(p->readopen == 0 && p->writeopen == 0){
release(&p->lock);
kfree((char*)p);
} else
release(&p->lock);
}
//PAGEBREAK: 40
int
pipewrite(struct pipe *p, char *addr, int n)
{
int i;
acquire(&p->lock);
// Writing to a pipe nobody is reading is SIGPIPE and EPIPE, in that order.
//
// This returned a bare -1 with no signal and no errno, so `yes | head -2`
// came back with whatever errno happened to be lying around -- "Invalid
// argument" -- instead of quietly ending the way every shell pipeline is
// built to. Checked up front as well as in the blocking loop below: the
// common case is that head has already exited by the time the next write
// arrives, and the pipe is nowhere near full.
if(p->readopen == 0){
release(&p->lock);
sigsend(myproc()->pid, SIGPIPE);
return -EPIPE;
}
for(i = 0; i < n; i++){
while(p->nwrite == p->nread + PIPESIZE){ //DOC: pipewrite-full
if(p->readopen == 0){
release(&p->lock);
sigsend(myproc()->pid, SIGPIPE);
return i > 0 ? i : -EPIPE; // a short write is still a write
}
if(myproc()->killed){
release(&p->lock);
return -1;
}
wakeup(&p->nread);
sleep(&p->nwrite, &p->lock); //DOC: pipewrite-sleep
}
p->data[p->nwrite++ % PIPESIZE] = addr[i];
}
wakeup(&p->nread); //DOC: pipewrite-wakeup1
release(&p->lock);
return n;
}
int
piperead(struct pipe *p, char *addr, int n)
{
int i;
acquire(&p->lock);
while(p->nread == p->nwrite && p->writeopen){ //DOC: pipe-empty
if(myproc()->killed){
release(&p->lock);
return -1;
}
sleep(&p->nread, &p->lock); //DOC: piperead-sleep
}
for(i = 0; i < n; i++){ //DOC: piperead-copy
if(p->nread == p->nwrite)
break;
addr[i] = p->data[p->nread++ % PIPESIZE];
}
wakeup(&p->nwrite); //DOC: piperead-wakeup
release(&p->lock);
return i;
}
// Readiness for poll(2)/select(2). Reports state, doesn't block.
int
pipe_poll(struct pipe *p, int readable, int writable)
{
int rev = 0;
acquire(&p->lock);
if(readable){
if(p->nread != p->nwrite)
rev |= POLLIN;
if(p->writeopen == 0)
rev |= POLLHUP;
}
if(writable){
if(p->readopen == 0)
rev |= POLLERR;
else if(p->nwrite < p->nread + PIPESIZE)
rev |= POLLOUT;
}
release(&p->lock);
return rev;
}

524
kernel/fs/procfs.c Normal file
View file

@ -0,0 +1,524 @@
// /proc as a real filesystem.
//
// It used to be a handful of character devices: init ran `mknod("/proc/uptime",
// 6, 2)` and friends, so every file was a real inode on the root filesystem
// with a major/minor pair behind it. That works for a fixed set of files and
// cannot work at all for the thing /proc is mostly *for* -- one directory per
// process. You cannot mknod a directory for a process that does not exist yet
// and will not exist in a minute, which is why `ps` and `top` have never
// worked: they call dirtree_flagread("/proc") and get a list with no pids in
// it.
//
// So this is a synthetic filesystem behind the same `struct fs_ops` vtable
// SchFS uses. Nothing is stored: every directory listing is generated from the
// process table at the moment it is asked for, and every file's contents are
// generated when it is read.
//
// The global files (meminfo, uptime, pci, ...) keep their existing generators
// in kernel/dev/procfs.c; this layer only decides what exists and hands the
// reads over. That split is deliberate -- the generators are about content and
// this file is about namespace, and they change for different reasons.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "proc.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "file.h"
#include "stat.h"
#include "uaccess.h"
#include "errno.h"
// ---------------------------------------------------------------------------
// Inode numbering
//
// An inode number has to survive being handed out, stored in a struct inode,
// and given back later with no other context -- so it encodes the whole
// identity of the object.
//
// 1 the root, /proc
// PROCFS_GLOBAL | id a global file, id being the PROC_* minor that
// kernel/dev/procfs.c already uses
// pid << 8 the directory /proc/<pid>
// (pid << 8) | fileid a file inside it
//
// pid is bounded by NPROC and file ids by PF_MAX, so the ranges cannot
// collide. Shifting by 8 rather than packing tightly keeps the numbers
// readable in a debugger, which matters more than density here.
// ---------------------------------------------------------------------------
// Size of the scratch buffer every generator renders into. Must stay equal to
// PROC_BUFSZ in kernel/dev/procfs.c: the SchFS and PCI generators there write
// up to PROC_BUFSZ regardless of the cap they are handed, so a smaller value
// here would be a buffer overrun rather than a truncation.
#define PROCFS_GENBUF 2048
#define PROCFS_ROOT 1ULL
#define PROCFS_GLOBAL 0x10000ULL
#define PF_DIR 0 // the directory itself
#define PF_STAT 1
#define PF_STATUS 2
#define PF_CMDLINE 3
#define PF_STATM 4
#define PF_EXE 5
#define PF_MAX 6
#define INO_PID(ino) ((int)((ino) >> 8))
#define INO_FILE(ino) ((int)((ino) & 0xFF))
#define INO_IS_GLOBAL(ino) (((ino) & PROCFS_GLOBAL) != 0)
// The global files, by the name they appear under and the PROC_* id that
// kernel/dev/procfs.c generates them from.
struct global_file { const char *name; int id; };
static const struct global_file globals[] = {
{ "meminfo", 1 },
{ "uptime", 2 },
{ "cpuinfo", 3 },
{ "version", 4 },
{ "stat", 5 },
{ "loadavg", 6 },
{ "mounts", 7 },
{ "filesystems", 8 },
{ "schfs", 9 },
{ "sysrq", 10 },
{ "pci", 11 },
{ "interrupts", 12 },
{ "devices", 13 },
};
#define NGLOBALS ((int)(sizeof(globals) / sizeof(globals[0])))
// Per-process files. `statm` and `status` are here because ps reads them; a
// missing file is not an error to it, but a present one is cheaper than the
// fallback paths it takes without them.
static const struct global_file pidfiles[] = {
{ "stat", PF_STAT },
{ "status", PF_STATUS },
{ "cmdline", PF_CMDLINE },
{ "statm", PF_STATM },
{ "exe", PF_EXE },
};
#define NPIDFILES ((int)(sizeof(pidfiles) / sizeof(pidfiles[0])))
// ---------------------------------------------------------------------------
// Content
// ---------------------------------------------------------------------------
// A tiny append-only formatter. procfs files are small and generated whole,
// so there is no reason for anything cleverer.
struct sb { char *p; int n, cap; };
static void sbputc(struct sb *s, char c)
{ if (s->n < s->cap - 1) s->p[s->n++] = c; }
static void sbputs(struct sb *s, const char *t)
{ while (*t) sbputc(s, *t++); }
static void sbputi(struct sb *s, int64 v)
{
char tmp[24];
int i = 0, neg = 0;
uint64 u;
if (v < 0) { neg = 1; u = (uint64)(-v); } else u = (uint64)v;
do { tmp[i++] = '0' + (int)(u % 10); u /= 10; } while (u);
if (neg) sbputc(s, '-');
while (i--) sbputc(s, tmp[i]);
}
// Linux's /proc/<pid>/stat, as far as anything here reads it.
//
// The field order is not negotiable: ps counts fields by position, so a
// missing one silently shifts everything after it. Fields Schunix does not
// track are zero, which is what Linux itself reports for things a given
// process has never done.
static char
state_char(int st)
{
switch (st) {
case RUNNING: return 'R';
case RUNNABLE: return 'R';
case SLEEPING: return 'S';
case ZOMBIE: return 'Z';
case EMBRYO: return 'R';
default: return '?';
}
}
static void
gen_pid_stat(struct sb *s, const struct procinfo *pi)
{
int i;
sbputi(s, pi->pid);
sbputs(s, " (");
sbputs(s, pi->name);
sbputs(s, ") ");
sbputc(s, state_char(pi->state));
sbputc(s, ' ');
sbputi(s, pi->ppid); // 4 ppid
sbputc(s, ' ');
sbputi(s, pi->pgid); // 5 pgrp
sbputc(s, ' ');
sbputi(s, pi->sid); // 6 session
sbputs(s, " 0 -1 0"); // 7 tty_nr, 8 tpgid, 9 flags
// 10..17: minflt cminflt majflt cmajflt utime stime cutime cstime.
// No per-process accounting yet, so these are honestly zero rather than
// invented -- a fabricated CPU time is worse than none, because top would
// draw a plausible graph of a number nobody measured.
for (i = 0; i < 8; i++)
sbputs(s, " 0");
sbputs(s, " 20 0 1 0 0 "); // priority nice num_threads itrealvalue starttime
sbputi(s, (int64)pi->sz); // 23 vsize, in bytes
sbputc(s, ' ');
sbputi(s, (int64)(pi->sz / PGSIZE)); // 24 rss, in pages
sbputs(s, " 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n");
}
static void
gen_pid_status(struct sb *s, const struct procinfo *pi)
{
sbputs(s, "Name:\t"); sbputs(s, pi->name); sbputc(s, '\n');
sbputs(s, "State:\t"); sbputc(s, state_char(pi->state));
sbputs(s, "\nTgid:\t"); sbputi(s, pi->pid);
sbputs(s, "\nPid:\t"); sbputi(s, pi->pid);
sbputs(s, "\nPPid:\t"); sbputi(s, pi->ppid);
sbputs(s, "\nUid:\t"); sbputi(s, pi->uid);
sbputc(s, '\t'); sbputi(s, pi->uid);
sbputc(s, '\t'); sbputi(s, pi->uid);
sbputc(s, '\t'); sbputi(s, pi->uid);
sbputs(s, "\nVmSize:\t"); sbputi(s, (int64)(pi->sz / 1024));
sbputs(s, " kB\nThreads:\t1\n");
}
static void
gen_pid_statm(struct sb *s, const struct procinfo *pi)
{
int64 pages = (int64)(pi->sz / PGSIZE);
sbputi(s, pages); sbputc(s, ' ');
sbputi(s, pages); sbputs(s, " 0 0 0 0 0\n");
}
// argv is not kept after exec, so the best honest answer is the process name.
// Linux separates arguments with NUL and ps turns those into spaces; one
// NUL-terminated string is a valid degenerate case of that.
static void
gen_pid_cmdline(struct sb *s, const struct procinfo *pi)
{
sbputs(s, pi->name);
sbputc(s, '\0');
}
// Generate the whole of one file into `buf`. Returns its length.
static int
gen_file(uint64 ino, char *buf, int cap)
{
struct sb s = { buf, 0, cap };
struct procinfo pi;
if (INO_IS_GLOBAL(ino))
return proc_gen_global((int)(ino & 0xFF), buf, cap);
if (proc_snapshot_pid(INO_PID(ino), &pi) < 0)
return 0; // it exited between lookup and read
switch (INO_FILE(ino)) {
case PF_STAT: gen_pid_stat(&s, &pi); break;
case PF_STATUS: gen_pid_status(&s, &pi); break;
case PF_CMDLINE: gen_pid_cmdline(&s, &pi); break;
case PF_STATM: gen_pid_statm(&s, &pi); break;
case PF_EXE: sbputs(&s, pi.exe[0] ? pi.exe : pi.name); break;
default: return 0;
}
return s.n;
}
// ---------------------------------------------------------------------------
// fs_ops
// ---------------------------------------------------------------------------
static void
procfs_iload(struct inode *ip)
{
uint64 ino = ip->inum;
ip->valid = 1;
ip->nlink = 1;
ip->major = ip->minor = 0;
// Read-only, with one exception: /proc/sysrq is the deliberate write target
// (`echo c > /proc/sysrq` to crash on purpose). Marking everything 2 made
// filewrite() refuse it with "permission denied", which the USB suite
// caught immediately -- three of its checks drive the disk through sysrq.
ip->rwmode = (INO_IS_GLOBAL(ino) && (ino & 0xFF) == 10) ? 3 : 2;
ip->showmode = 1;
ip->atime = ip->mtime = ip->ctime = 0;
if (ino == PROCFS_ROOT || (!INO_IS_GLOBAL(ino) && INO_FILE(ino) == PF_DIR)) {
ip->type = T_DIR;
ip->size = 0;
ip->nlink = 2;
return;
}
ip->type = T_FILE;
// Size is reported as zero.
//
// A synthetic file has no length until it is generated, and generating it
// here -- on every stat(), for every process -- to fill in a number almost
// nobody reads would be absurd. Linux does exactly the same thing, which is
// why `ls -l /proc` shows a column of zeroes, and why every tool that reads
// /proc reads until EOF instead of trusting st_size.
ip->size = 0;
}
static void procfs_istore(struct inode *ip) { (void)ip; }
static void procfs_ifree(struct inode *ip) { (void)ip; }
static void procfs_truncate(struct inode *ip, uint64 n) { (void)ip; (void)n; }
static int
procfs_read(struct inode *ip, int user_dst, uint64 dst, uint64 off, uint n)
{
char *buf;
int len, r;
if (ip->type == T_DIR)
return -EISDIR;
// The 2048-byte scratch used to be a local. KSTACKSIZE is 4096, so that was
// half the kernel stack in one array, on a path already several frames deep
// -- a generator with any locals of its own, or one more frame below it,
// runs off the end. /proc/schfs is what finally did it: the canary at the
// bottom of the stack came back zeroed. Nothing about SchFS caused this; it
// only pushed an existing overrun over the edge.
//
// A whole page for a 2 KB buffer is not wasteful here -- kalloc's unit is a
// page either way, and this is not a hot path.
if ((buf = kalloc()) == 0)
return -ENOMEM;
len = gen_file(ip->inum, buf, PROCFS_GENBUF);
if (off >= (uint64)len) {
kfree(buf);
return 0;
}
if (off + n > (uint64)len)
n = (uint)((uint64)len - off);
r = (int)n;
if (user_dst) {
if (copy_to_user(dst, buf + off, n) < 0)
r = -EFAULT;
} else {
memmove((char *)dst, buf + off, n);
}
kfree(buf);
return r;
}
static int
procfs_write(struct inode *ip, int user_src, uint64 src, uint64 off, uint n)
{
// Only /proc/sysrq accepts writes, and its handler lives with the other
// generators. Everything else is read-only, and says so.
if (INO_IS_GLOBAL(ip->inum) && (ip->inum & 0xFF) == 10)
return proc_write_sysrq(user_src, src, n);
(void)off;
return -EROFS;
}
// Decimal string -> pid, or -1 if it is not all digits. This is also what
// makes a name like "meminfo" fall through to the global table.
static int
name_to_pid(const char *name)
{
int v = 0;
if (!name || !*name)
return -1;
while (*name) {
if (*name < '0' || *name > '9')
return -1;
v = v * 10 + (*name++ - '0');
if (v > 1 << 20)
return -1;
}
return v;
}
static struct inode *
procfs_dirlookup(struct inode *dp, char *name, uint *poff)
{
struct procinfo pi;
int i, pid;
if (poff)
*poff = 0;
if (namecmp(name, ".") == 0)
return iget(dp->dev, dp->inum);
if (namecmp(name, "..") == 0)
return iget(dp->dev, PROCFS_ROOT); // /proc/<pid>/.. is /proc
// Inside /proc/<pid>/
if (dp->inum != PROCFS_ROOT) {
for (i = 0; i < NPIDFILES; i++)
if (namecmp(name, (char *)pidfiles[i].name) == 0)
return iget(dp->dev, (dp->inum & ~0xFFULL) | pidfiles[i].id);
return 0;
}
// In /proc itself: a number is a process, a word is a global file.
pid = name_to_pid(name);
if (pid > 0) {
if (proc_snapshot_pid(pid, &pi) < 0)
return 0;
return iget(dp->dev, ((uint64)pid << 8) | PF_DIR);
}
for (i = 0; i < NGLOBALS; i++)
if (namecmp(name, (char *)globals[i].name) == 0)
return iget(dp->dev, PROCFS_GLOBAL | globals[i].id);
return 0;
}
// Nothing in /proc can be created, removed or linked. Saying so with an error
// is better than a silent no-op: a program that thinks it wrote a file and
// finds it missing later has a much harder problem to debug.
static int procfs_dirlink(struct inode *dp, char *name, uint64 inum)
{ (void)dp; (void)name; (void)inum; return -EROFS; }
static int procfs_dirunlink(struct inode *dp, char *name, uint off)
{ (void)dp; (void)name; (void)off; return -EROFS; }
static int procfs_dirempty(struct inode *dp) { (void)dp; return 0; }
static struct inode *procfs_ialloc(uint dev, short type)
{ (void)dev; (void)type; return 0; }
// One getdents64 record.
struct linux_dirent64 {
uint64 d_ino;
int64 d_off;
ushort d_reclen;
uchar d_type;
char d_name[];
};
#define DT_DIR_ 4
#define DT_REG_ 8
static int
emit(char *out, int cap, int used, uint64 ino, int type, const char *name)
{
struct linux_dirent64 *de;
int nlen = 0, reclen;
while (name[nlen]) nlen++;
reclen = (int)sizeof(*de) + nlen + 1;
reclen = (reclen + 7) & ~7; // 8-byte aligned, as getdents64 requires
if (used + reclen > cap)
return -1;
de = (struct linux_dirent64 *)(out + used);
de->d_ino = ino;
de->d_off = used + reclen;
de->d_reclen = (ushort)reclen;
de->d_type = (uchar)type;
memmove(de->d_name, name, nlen);
de->d_name[nlen] = 0;
return reclen;
}
static void
utoa(uint v, char *buf)
{
char t[12];
int i = 0, j = 0;
do { t[i++] = '0' + (v % 10); v /= 10; } while (v);
while (i) buf[j++] = t[--i];
buf[j] = 0;
}
// The listing is generated fresh each time, and *off is an index into it.
//
// A process that exits between two getdents calls therefore shifts everything
// after it up by one, and one entry can be missed. That is a real race and it
// is the same one Linux has -- /proc has no stable ordering to offer, and
// anything that wants a consistent snapshot has to re-read.
static int
procfs_getdents(struct inode *dp, char *out, int cap, uint64 *off)
{
struct procinfo pi;
char name[16];
int used = 0, idx = 0, i, r;
uint64 want = *off;
#define EMIT(ino, type, nm) do { \
if (idx++ >= (int)want) { \
r = emit(out, cap, used, (ino), (type), (nm)); \
if (r < 0) goto full; \
used += r; (*off)++; \
} \
} while (0)
if (dp->inum == PROCFS_ROOT) {
EMIT(PROCFS_ROOT, DT_DIR_, ".");
EMIT(PROCFS_ROOT, DT_DIR_, "..");
for (i = 0; i < NGLOBALS; i++)
EMIT(PROCFS_GLOBAL | globals[i].id, DT_REG_, globals[i].name);
for (i = 0; proc_snapshot(i, &pi) == 0; i++) {
utoa((uint)pi.pid, name);
EMIT(((uint64)pi.pid << 8) | PF_DIR, DT_DIR_, name);
}
} else {
EMIT(dp->inum, DT_DIR_, ".");
EMIT(PROCFS_ROOT, DT_DIR_, "..");
for (i = 0; i < NPIDFILES; i++)
EMIT((dp->inum & ~0xFFULL) | pidfiles[i].id, DT_REG_, pidfiles[i].name);
}
full:
#undef EMIT
return used;
}
const struct fs_ops procfs_ops = {
.name = "procfs",
.iload = procfs_iload,
.istore = procfs_istore,
.ifree = procfs_ifree,
.read = procfs_read,
.write = procfs_write,
.truncate = procfs_truncate,
.dirlookup = procfs_dirlookup,
.dirlink = procfs_dirlink,
.dirunlink = procfs_dirunlink,
.dirempty = procfs_dirempty,
.ialloc = procfs_ialloc,
.getdents = procfs_getdents,
};
// Graft this filesystem over /proc.
//
// Called by init once it has made the directory. The mknod'd files that used
// to live there are simply covered: path resolution stops at the mount point,
// so they are unreachable and harmless. Removing them from init is a separate,
// later cleanup -- doing both at once would make a boot failure ambiguous
// between "the mount is wrong" and "the nodes are missing".
void
procfs_mount(void)
{
struct inode *dp = namei("/proc");
if (dp == 0) {
cprintf("procfs: /proc does not exist; not mounted\n");
return;
}
if (mount_add(dp->dev, dp->inum, PROCFS_DEV, PROCFS_ROOT) < 0)
cprintf("procfs: /proc already has something mounted on it\n");
iput(dp);
}

1181
kernel/fs/schfs/btree.c Normal file

File diff suppressed because it is too large Load diff

215
kernel/fs/schfs/btree.h Normal file
View file

@ -0,0 +1,215 @@
// SchFS copy-on-write B-tree.
//
// This is the load-bearing piece of the whole filesystem: inodes, directory
// entries, extent maps and indexes are all just records in this one tree, so a
// subtle bug here is a bug in everything. It is therefore written as portable C
// against an abstract block device, and exercised on the host by
// tools/schfs_test.c before it is ever run inside the kernel.
//
// Copy-on-write means no block that is part of the *committed* tree is ever
// modified. An update copies the path from the leaf back up to the root, and
// the caller then publishes the new root. A crash therefore lands on either the
// old tree or the new one, never a mixture — which is why there is no journal
// and no fsck.
//
// "Committed" is the operative word: a block first written during the
// transaction group now being built is not reachable from any published root,
// so it may be rewritten in place. See schfs_btree.generation.
#ifndef SCHFS_BTREE_H
#define SCHFS_BTREE_H
#include "schfs.h"
// Block device the tree lives on. The kernel supplies one backed by the buffer
// cache; the test harness supplies one backed by a plain file or by memory.
struct schfs_bdev {
void *ctx;
int (*bread)(void *ctx, uint64_t blk, void *buf);
int (*bwrite)(void *ctx, uint64_t blk, const void *buf);
// Allocate a block for copy-on-write. Must never return a block that is
// reachable from the last committed root.
uint64_t (*balloc)(void *ctx);
// Release a block that the new tree no longer references. Freeing may only
// take effect at commit, since the old tree must stay readable until then.
void (*bfree)(void *ctx, uint64_t blk);
// Optional: allocate n physically contiguous blocks, returning the first,
// or 0 if that cannot be satisfied. This is what lets a file become one
// extent rather than one per block, and it has to be a separate entry point
// because an allocator with a free list will happily return scattered
// blocks from balloc. NULL means unsupported; callers then fall back to
// allocating singly and accept the extra extent records.
uint64_t (*balloc_run)(void *ctx, uint64_t n);
};
struct schfs_btree {
struct schfs_bdev *dev;
uint64_t root; // block number of the current root
uint32_t root_crc;
uint64_t root_gen; // generation stamped in the root block itself
// The transaction group being built. Every block written by a mutation is
// stamped with this, and a block already carrying it is rewritten in place
// rather than copied — it cannot be part of a committed tree. The volume
// layer advances it on every commit; nothing else may touch it.
uint64_t generation;
};
// ---------------------------------------------------------------------------
// Scratch buffers
//
// Nothing in this file may put a block on the stack. A SchFS block is 4096
// bytes and so is the entire kernel stack (KSTACKSIZE), so even one such local
// overflows it — the kstack canary caught exactly that on the first boot of the
// read path. Mutations need three blocks live at once (the node being edited,
// the sibling a split produces, and a repacking buffer), so callers hand in
// three and the tree borrows them.
//
// The three must not alias, and their contents do not survive across calls.
// ---------------------------------------------------------------------------
struct schfs_ctx {
void *a; // node currently being edited
void *b; // sibling produced by a split
void *c; // repacking scratch for leaves
};
// ---------------------------------------------------------------------------
// Cursor
//
// The read path is driven through a cursor for the same reason: a cursor holds
// exactly one block buffer, supplied by the caller, and remembers the path as
// block numbers, re-reading a parent on the way back up. Walking a tree of any
// depth therefore costs one page rather than one page per level.
// ---------------------------------------------------------------------------
struct schfs_path_ent {
uint64_t blocknr;
uint32_t crc;
int index; // internal: child descended into; leaf: item
};
struct schfs_cursor {
struct schfs_btree *t;
void *blk; // caller-owned, SCHFS_BLKSIZE bytes
int depth; // path[depth] is the leaf
int valid; // 0 once the walk has run off either end
struct schfs_path_ent path[SCHFS_MAX_LEVEL + 1];
};
// `blk` must point at SCHFS_BLKSIZE bytes that outlive the cursor.
void schfs_cursor_init(struct schfs_cursor *c, struct schfs_btree *t, void *blk);
// Position at the first key >= start, or at the very first key if start is
// NULL. Returns 0 if positioned, -1 if the tree holds no such key, -2 on
// corruption.
int schfs_cursor_seek(struct schfs_cursor *c, const struct schfs_key *start);
// Step. Return 0, -1 at the end of the tree, -2 on corruption.
int schfs_cursor_next(struct schfs_cursor *c);
int schfs_cursor_prev(struct schfs_cursor *c);
// Current record. Both pointers are invalidated by the next seek/next/prev.
// Return NULL when the cursor is not positioned.
const struct schfs_key *schfs_cursor_key(struct schfs_cursor *c);
const void *schfs_cursor_val(struct schfs_cursor *c, unsigned *len);
// Ordering over keys: (objectid, type, offset). Returns <0, 0, >0.
int schfs_key_cmp(const struct schfs_key *a, const struct schfs_key *b);
uint32_t schfs_crc32c(uint32_t crc, const void *buf, unsigned len);
// ---------------------------------------------------------------------------
// Node accessors
//
// Plain views over the on-disk layout in schfs.h. Exposed because the space
// manager walks the tree by block number rather than by key, and a second,
// slightly different copy of these three lines is exactly the sort of thing
// that ends up disagreeing with the first.
// ---------------------------------------------------------------------------
#define SCHFS_HDR(b) ((struct schfs_node_hdr *)(b))
#define SCHFS_KPTRS(b) ((struct schfs_kptr *) \
((char *)(b) + sizeof(struct schfs_node_hdr)))
#define SCHFS_ITEMS(b) ((struct schfs_item *) \
((char *)(b) + sizeof(struct schfs_node_hdr)))
// Read one tree block into `out` and validate it completely: magic, the block's
// self-pointer, its checksum, and — when want_crc is non-zero — that the
// checksum is the one its parent recorded. Returns 0 or -2.
int schfs_node_read(struct schfs_bdev *dev, uint64_t blk, uint32_t want_crc,
void *out);
// Create an empty tree (a single empty leaf) and set t->root.
int schfs_btree_create(struct schfs_btree *t, struct schfs_bdev *dev,
struct schfs_ctx *x);
// Look a key up. On success copies at most *len bytes of the value into buf and
// stores the true size in *len. Returns 0 on success, -1 if absent, -2 on a
// corrupt or unreadable block. Uses x->a as its block buffer.
int schfs_btree_lookup(struct schfs_btree *t, struct schfs_ctx *x,
const struct schfs_key *k, void *buf, unsigned *len);
// Same, through a cursor the caller already has positioned somewhere else.
int schfs_btree_lookup_cur(struct schfs_cursor *c, const struct schfs_key *k,
void *buf, unsigned *len);
// Greatest key <= k, which is how an extent covering a file offset is found:
// extents are keyed by the file offset they start at, so the record governing
// byte N is the last one at or before (ino, EXTENT, N). Leaves the cursor
// positioned on it. Returns 0, -1 if every key in the tree is greater, -2 on
// corruption.
int schfs_btree_lookup_le(struct schfs_cursor *c, const struct schfs_key *k);
// Insert or replace. Copies the path, so t->root changes on success.
// Returns 0, -1 if the value is too large to store inline, -2 on I/O or
// corruption, -3 if the device is out of blocks.
int schfs_btree_insert(struct schfs_btree *t, struct schfs_ctx *x,
const struct schfs_key *k, const void *val, unsigned len);
// Remove a key. Emptied nodes are freed and the root is collapsed when it drops
// to a single child, so a tree that is emptied returns to one block.
// Returns 0, -1 if absent, -2 on corruption, -3 out of space.
int schfs_btree_delete(struct schfs_btree *t, struct schfs_ctx *x,
const struct schfs_key *k);
// Walk records from `start` (inclusive) onward. This is what a directory read
// uses: every entry of directory D is a key in [(D,DIRENT,0), (D,DIRENT,~0)],
// so listing a directory costs the size of that directory rather than the size
// of the filesystem. fn returns non-zero to stop.
//
// WARNING: the `val` pointer handed to fn points *into the cursor's block
// buffer*. Any other tree operation the callback performs may overwrite it —
// including one on a different cursor, if the two were given the same buffer.
// A callback that looks anything up must copy what it needs first. The key is
// already copied out for you; the value is not, because it can be a kilobyte
// and most callbacks consume it immediately.
int schfs_btree_range(struct schfs_cursor *c, const struct schfs_key *start,
int (*fn)(const struct schfs_key *k,
const void *val, unsigned len, void *arg),
void *arg);
// ---------------------------------------------------------------------------
// Host-only facilities
//
// These recurse and keep a block per level, which is fine in a tool and fatal
// in the kernel. Compiling them out of the kernel makes that a link error
// rather than a comment nobody reads.
// ---------------------------------------------------------------------------
#ifdef SCHFS_HOSTED
// Walk every record in key order, calling fn for each. A non-zero return from
// fn stops the walk and is passed back.
int schfs_btree_iterate(struct schfs_btree *t,
int (*fn)(const struct schfs_key *k,
const void *val, unsigned len, void *arg),
void *arg);
// Full structural check: magic, self-pointer, checksum, level agreement, key
// ordering within and across nodes, that values lie inside their block, and
// that every child's first key is exactly the key its parent files it under.
// Returns 0 if the tree is sound, non-zero on the first problem found.
int schfs_btree_check(struct schfs_btree *t);
#endif /* SCHFS_HOSTED */
#endif /* SCHFS_BTREE_H */

623
kernel/fs/schfs/kmount.c Normal file
View file

@ -0,0 +1,623 @@
// SPDX-License-Identifier: Apache-2.0
//
// SchFS in the kernel: block device, mount, and the transaction bracket.
//
// The filesystem code itself (btree.c, space.c, vol.c, obj.c, objw.c) is the
// same portable C the host tools compile, so nothing here re-implements the
// format. This file is only the three things that cannot be shared: a block
// device backed by the buffer cache, the boot-time mount, and the rule about
// when a group of changes becomes durable.
//
// The running root filesystem is still the v1 code in kernel/fs/fs.c, so a
// SchFS volume that fails to mount costs nothing — the system boots exactly as
// before and says so on the console.
//
// ---------------------------------------------------------------------------
// Transactions
//
// Every entry point below that changes anything is one transaction group: it
// takes the volume lock, does its work, and commits before returning. That
// makes an operation durable the moment it returns, and — because a commit is a
// single uberblock write that either lands or does not — indivisible against a
// crash. There is no journal to replay and nothing to undo, because none of the
// work is reachable until that one block is written.
//
// The cost is one uberblock write per operation. Batching several operations
// into a group would amortise it, and the structure here is deliberately shaped
// so that becomes a change to where commit is called rather than a redesign;
// but a filesystem that loses the last few operations on a crash is a much
// worse default than a slow one.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "buf.h"
#include "schfs_int.h"
// A SchFS block is 4096 bytes and a buffer-cache block is BSIZE (1024), so each
// filesystem block spans SCHFS_BPB consecutive buffers. Keeping the buffer
// cache at its own block size avoids disturbing the v1 filesystem, the log and
// the IDE driver, all of which assume BSIZE.
#define SCHFS_BPB (SCHFS_BLKSIZE / BSIZE)
struct schfs_kdev {
uint dev;
};
static struct schfs_kdev kdev;
static struct schfs_vol kvol;
static int kmounted;
// Block buffers, allocated once at mount.
//
// Nothing in SchFS may put a block on the stack: a cursor needs a whole
// SCHFS_BLKSIZE buffer and the kernel stack is one 4096-byte page (KSTACKSIZE),
// so a single such local overflows it. kalloc returns exactly one page, which
// is exactly one SchFS block, so each of these fits with nothing to spare.
//
// kcurblk and kdatablk serve the read paths; kctx* are the three the volume
// keeps for mutations. They are separate pages because a write path runs a
// cursor of its own inside the volume's buffers while the caller's cursor is
// still positioned in kcurblk.
static char *kcurblk;
static char *kdatablk;
static char *kctxa, *kctxb, *kctxc;
// Those buffers are shared by every caller, so every entry point below holds
// this lock for the whole operation. A sleeplock rather than a spinlock because
// bread() sleeps waiting on the disk.
//
// It also serialises transactions, which is not incidental: a group being built
// rewrites its own blocks in place, so two operations interleaving inside one
// group would each see the other's half-finished tree.
static struct sleeplock schfs_lock;
// Seconds, for inode timestamps. The kernel has no wall clock wired up, so this
// matches what fs.c does for the v1 filesystem rather than inventing a second,
// differently-wrong answer.
static uint64_t
know(void)
{
return (uint64_t)uptime();
}
// Start and finish one transaction. `changed` says whether anything was
// written, so a read does not pay for an uberblock write.
static void
ktx_begin(void)
{
acquiresleep(&schfs_lock);
}
static int
ktx_end(int changed, int r)
{
if (changed && r >= 0) {
int c = schfs_vol_commit(&kvol);
if (c != 0)
r = c;
}
releasesleep(&schfs_lock);
return r;
}
static int
kd_read(void *ctx, uint64_t blk, void *out)
{
struct schfs_kdev *d = ctx;
if (blk == 0)
return -1;
// The buffer cache addresses blocks with a uint, which caps the volume long
// before the on-disk format does. Refusing out here beats truncating and
// reading some unrelated sector, which would surface as corruption.
if (blk > 0xffffffffULL / SCHFS_BPB)
return -1;
for (int i = 0; i < SCHFS_BPB; i++) {
struct buf *b = bread(d->dev, (uint)(blk * SCHFS_BPB + i));
memmove((char *)out + i * BSIZE, b->data, BSIZE);
brelse(b);
}
return 0;
}
static int
kd_write(void *ctx, uint64_t blk, const void *in)
{
struct schfs_kdev *d = ctx;
if (blk == 0)
return -1;
if (blk > 0xffffffffULL / SCHFS_BPB)
return -1;
// Straight through the buffer cache, with no log. The xv6 log exists to make
// multi-block updates atomic; copy-on-write makes them atomic by never
// modifying a block the committed tree can reach, so logging every write
// would double the I/O to re-solve a problem the format has already solved.
//
// bread before the overwrite because the cache has no "give me this block
// without reading it" entry point. It costs a read the first time a block is
// touched and nothing afterwards.
for (int i = 0; i < SCHFS_BPB; i++) {
struct buf *b = bread(d->dev, (uint)(blk * SCHFS_BPB + i));
memmove(b->data, (const char *)in + i * BSIZE, BSIZE);
bwrite(b);
brelse(b);
}
return 0;
}
// Allocation goes through the volume's space manager. These take no ctx of
// their own because a schfs_bdev has one ctx shared by every hook and the block
// I/O above needs it for the device number.
static uint64_t
kd_alloc(void *ctx)
{
(void)ctx;
return schfs_vol_alloc(&kvol);
}
static uint64_t
kd_alloc_run(void *ctx, uint64_t n)
{
(void)ctx;
return schfs_vol_alloc_run(&kvol, n);
}
static void
kd_free(void *ctx, uint64_t b)
{
(void)ctx;
schfs_vol_free(&kvol, b);
}
// Pages for the allocation bitmap: one per 32768 blocks of volume, taken once
// at mount and never given back.
static void *
kd_palloc(void)
{
return kalloc();
}
// Try to mount a SchFS volume from `dev`. Never panics: a disk that is not
// SchFS, or is an unreadable SchFS, simply leaves the volume unmounted.
void
schfs_kmount(uint dev)
{
if (kmounted)
return;
initsleeplock(&schfs_lock, "schfs");
if (kcurblk == 0) kcurblk = kalloc();
if (kdatablk == 0) kdatablk = kalloc();
if (kctxa == 0) kctxa = kalloc();
if (kctxb == 0) kctxb = kalloc();
if (kctxc == 0) kctxc = kalloc();
if (kcurblk == 0 || kdatablk == 0 ||
kctxa == 0 || kctxb == 0 || kctxc == 0) {
cprintf("schfs: no memory for block buffers\n");
return;
}
kdev.dev = dev;
struct schfs_bdev bd;
bd.ctx = &kdev;
bd.bread = kd_read;
bd.bwrite = kd_write;
bd.balloc = kd_alloc;
bd.bfree = kd_free;
bd.balloc_run = kd_alloc_run;
struct schfs_ctx cx;
cx.a = kctxa;
cx.b = kctxb;
cx.c = kctxc;
// The uberblock scan doubles as the probe: no valid uberblock means this is
// simply not a SchFS volume, which is not an error worth reporting loudly.
// Opening with a page allocator also builds the allocation bitmap, by walking
// the tree — the only mount-time cost SchFS has, and what buys it a mount
// that cannot disagree with the filesystem it just adopted.
int r = schfs_vol_open(&kvol, &bd, &cx, kd_palloc);
if (r == -1)
return;
if (r == -4) {
cprintf("schfs: device %d is larger than %d GB and cannot be mapped\n",
dev, (int)(SCHFS_MAX_BLOCKS * SCHFS_BLKSIZE / (1024 * 1024 * 1024)));
return;
}
if (r == -5) {
cprintf("schfs: no memory for the allocation map of device %d\n", dev);
return;
}
if (r != 0) {
cprintf("schfs: device %d has a SchFS uberblock but an unreadable tree\n",
dev);
return;
}
kmounted = 1;
cprintf("schfs: mounted '%s' on device %d, %d MB, generation %d\n",
kvol.label, dev,
(int)(kvol.total_blocks * SCHFS_BLKSIZE / (1024 * 1024)),
(int)kvol.generation);
}
int
schfs_kmounted(void)
{
return kmounted;
}
// Resolve a path on the mounted volume. Returns the object id, or 0.
uint64_t
schfs_klookup(const char *path)
{
if (!kmounted)
return 0;
acquiresleep(&schfs_lock);
struct schfs_cursor c;
schfs_cursor_init(&c, &kvol.tree, kcurblk);
uint64_t r = schfs_path_lookup(&kvol, &c, path);
releasesleep(&schfs_lock);
return r;
}
int
schfs_kstat(uint64_t ino, struct schfs_inode *out)
{
if (!kmounted)
return -1;
acquiresleep(&schfs_lock);
struct schfs_cursor c;
schfs_cursor_init(&c, &kvol.tree, kcurblk);
int r = schfs_inode_get(&kvol, &c, ino, out);
releasesleep(&schfs_lock);
return r;
}
long
schfs_kread(uint64_t ino, uint64_t off, void *buf, unsigned n)
{
if (!kmounted)
return -1;
acquiresleep(&schfs_lock);
struct schfs_cursor c;
schfs_cursor_init(&c, &kvol.tree, kcurblk);
long r = schfs_file_read(&kvol, &c, ino, off, buf, n, kdatablk);
releasesleep(&schfs_lock);
return r;
}
int
schfs_kreaddir(uint64_t dir,
int (*fn)(uint64_t, uint16_t, const char *, unsigned, void *),
void *arg)
{
if (!kmounted)
return -1;
// NOTE: fn must not call back into schfs_k* -- the lock is not recursive and
// the cursor buffer is shared. Callers that need per-entry detail collect the
// entries first, then look them up after the walk.
acquiresleep(&schfs_lock);
struct schfs_cursor c;
schfs_cursor_init(&c, &kvol.tree, kcurblk);
int r = schfs_dir_iterate(&kvol, &c, dir, fn, arg);
releasesleep(&schfs_lock);
return r;
}
struct schfs_vol *
schfs_kvol(void)
{
return kmounted ? &kvol : 0;
}
/* ------------------------------------------------------------------ */
/* Write side */
/* */
/* Each of these is one transaction: everything it does is invisible */
/* until it commits, and a crash part way through leaves the volume at */
/* the state before the call. That is the entire failure story — there */
/* is no partial state to detect and nothing to roll back. */
/* ------------------------------------------------------------------ */
int
schfs_kcreate(uint64_t dir, const char *name, unsigned namelen, uint16_t type,
uint32_t mode, uint16_t major, uint16_t minor, uint64_t *out)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_create(&kvol, dir, name, namelen, type, mode, major, minor,
know(), out);
return ktx_end(1, r);
}
long
schfs_kwrite(uint64_t ino, uint64_t off, const void *buf, unsigned n)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
long r = schfs_file_write(&kvol, ino, off, buf, n, kdatablk);
// ktx_end speaks in error codes, and a write speaks in byte counts. Passing
// the count straight through told it the transaction had failed with status
// 17, which it duly reported -- and a short write is a panic one layer up.
int e = ktx_end(1, r < 0 ? (int)r : 0);
return e == 0 ? r : (long)e;
}
int
schfs_ktruncate(uint64_t ino, uint64_t size)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_file_truncate(&kvol, ino, size, kdatablk);
return ktx_end(1, r);
}
int
schfs_kunlink(uint64_t dir, const char *name, unsigned namelen)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_unlink(&kvol, dir, name, namelen);
return ktx_end(1, r);
}
int
schfs_klink(uint64_t dir, const char *name, unsigned namelen, uint64_t ino)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_link(&kvol, dir, name, namelen, ino);
return ktx_end(1, r);
}
int
schfs_krename(uint64_t olddir, const char *oldname, unsigned oldlen,
uint64_t newdir, const char *newname, unsigned newlen)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_rename(&kvol, olddir, oldname, oldlen,
newdir, newname, newlen);
return ktx_end(1, r);
}
// Write back an inode record the caller has modified — the one way to change
// mode, ownership or timestamps. The caller reads it with schfs_kstat, edits
// the struct, and hands it back.
int
schfs_ksetattr(uint64_t ino, const struct schfs_inode *in)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_inode_put(&kvol, ino, in);
return ktx_end(1, r);
}
// Flush without changing anything. Nothing needs this yet — every operation
// above commits already — but sync(2) has to mean something.
int
schfs_ksync(void)
{
if (!kmounted || kvol.readonly)
return 0;
ktx_begin();
return ktx_end(1, 0);
}
/* ------------------------------------------------------------------ */
/* Pieces the VFS needs */
/* */
/* The VFS keeps its own link count and decides when an object dies, */
/* so it needs the directory entry and the object as separate */
/* operations rather than the combined create/unlink above. Two layers */
/* counting links independently is exactly how a filesystem loses a */
/* file that something still has open. */
/* ------------------------------------------------------------------ */
// Resolve one name inside a directory. 0 if there is no such entry.
uint64_t
schfs_klookup_at(uint64_t dir, const char *name, unsigned namelen)
{
if (!kmounted)
return 0;
acquiresleep(&schfs_lock);
struct schfs_cursor c;
schfs_cursor_init(&c, &kvol.tree, kcurblk);
uint64_t ino = 0;
if (schfs_dir_lookup(&kvol, &c, dir, name, namelen, &ino, 0) != 0)
ino = 0;
releasesleep(&schfs_lock);
return ino;
}
// 1 if the directory holds no entries, 0 if it does, -1 on error.
int
schfs_kdirempty(uint64_t dir)
{
if (!kmounted)
return -1;
acquiresleep(&schfs_lock);
int r = schfs_dir_empty(&kvol, dir);
releasesleep(&schfs_lock);
return r == -2 ? -1 : r;
}
// An object with no name and no links yet. Nothing can reach it until the
// caller links it, and nothing reaches the disk until the transaction commits,
// so a caller that gives up in between leaves nothing behind.
int
schfs_kalloc_object(uint16_t type, uint32_t mode, uint64_t *out)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
uint64_t ino = schfs_alloc_objectid(&kvol);
struct schfs_inode in;
memset(&in, 0, sizeof(in));
in.type = type;
in.mode = mode;
in.nlink = 0;
in.atime = in.mtime = in.ctime = know();
in.gen = kvol.tree.generation;
int r = schfs_inode_put(&kvol, ino, &in);
if (r == 0 && out)
*out = ino;
return ktx_end(1, r);
}
int
schfs_kdiradd(uint64_t dir, const char *name, unsigned namelen, uint64_t ino,
uint16_t type)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_dir_add(&kvol, dir, name, namelen, ino, type);
return ktx_end(1, r);
}
// Remove one directory entry, leaving the object alone. The VFS decrements the
// link count itself and calls schfs_kfree when it reaches zero.
int
schfs_kdirdel(uint64_t dir, const char *name, unsigned namelen)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_dir_del(&kvol, dir, name, namelen);
return ktx_end(1, r);
}
// Release an object's data, checksums and inode record. Called from iput once
// the last name is gone and nothing has it open.
int
schfs_kfree(uint64_t ino)
{
if (!kmounted || kvol.readonly)
return -1;
ktx_begin();
int r = schfs_object_free(&kvol, ino);
return ktx_end(1, r);
}
uint64_t
schfs_kroot(void)
{
return kmounted ? kvol.root_objectid : 0;
}
/* ------------------------------------------------------------------ */
/* /proc/schfs */
/* */
/* Phase 2 has no VFS integration -- SchFS files are not reachable */
/* through open(2) yet -- so this is how the mount is observable from */
/* userland at all. It reports the volume and lists the root directory, */
/* which is enough to confirm from inside a running Schunix that the */
/* kernel really did parse an image mkschfs wrote. */
/* ------------------------------------------------------------------ */
static void
kp_puts(char *buf, int cap, int *len, const char *s)
{
while (*s && *len < cap - 1)
buf[(*len)++] = *s++;
}
static void
kp_putu(char *buf, int cap, int *len, uint64 v)
{
char tmp[24];
int i = 0;
if (v == 0) { if (*len < cap - 1) buf[(*len)++] = '0'; return; }
while (v && i < (int)sizeof(tmp)) { tmp[i++] = '0' + (v % 10); v /= 10; }
while (i--) { if (*len < cap - 1) buf[(*len)++] = tmp[i]; }
}
struct kp_ctx {
char *buf;
int cap;
int *len;
int count;
};
static int
kp_entry(uint64_t ino, uint16_t type, const char *name, unsigned namelen,
void *arg)
{
struct kp_ctx *c = arg;
c->count++;
// Only the first handful, and no per-entry stat: schfs_kreaddir holds the
// lock across the walk, so calling schfs_kstat from here would deadlock.
if (c->count > 16)
return 0;
kp_puts(c->buf, c->cap, c->len, " ");
kp_puts(c->buf, c->cap, c->len,
type == SCHFS_T_DIR ? "dir " :
type == SCHFS_T_SYMLINK ? "symlink " :
type == SCHFS_T_DEV ? "dev " : "file ");
for (unsigned i = 0; i < namelen && *c->len < c->cap - 1; i++)
c->buf[(*c->len)++] = name[i];
kp_puts(c->buf, c->cap, c->len, " #");
kp_putu(c->buf, c->cap, c->len, ino);
kp_puts(c->buf, c->cap, c->len, "\n");
return 0;
}
int
schfs_kinfo(char *buf, int cap)
{
int len = 0;
if (!kmounted) {
kp_puts(buf, cap, &len, "no SchFS volume mounted\n");
return len;
}
kp_puts(buf, cap, &len, "label: ");
kp_puts(buf, cap, &len, kvol.label);
kp_puts(buf, cap, &len, "\nblocksize: ");
kp_putu(buf, cap, &len, SCHFS_BLKSIZE);
kp_puts(buf, cap, &len, "\ntotal_blocks: ");
kp_putu(buf, cap, &len, kvol.total_blocks);
kp_puts(buf, cap, &len, "\nfree_blocks: ");
kp_putu(buf, cap, &len, kvol.free_blocks);
kp_puts(buf, cap, &len, "\ngeneration: ");
kp_putu(buf, cap, &len, kvol.generation);
kp_puts(buf, cap, &len, "\ntree_root: ");
kp_putu(buf, cap, &len, kvol.tree.root);
kp_puts(buf, cap, &len, "\nroot_objectid: ");
kp_putu(buf, cap, &len, kvol.root_objectid);
kp_puts(buf, cap, &len, "\n\nroot directory:\n");
struct kp_ctx c = { buf, cap, &len, 0 };
if (schfs_kreaddir(kvol.root_objectid, kp_entry, &c) != 0) {
kp_puts(buf, cap, &len, " (directory read failed)\n");
return len;
}
if (c.count > 16) {
kp_puts(buf, cap, &len, " ... ");
kp_putu(buf, cap, &len, (uint64)(c.count - 16));
kp_puts(buf, cap, &len, " more\n");
}
return len;
}

441
kernel/fs/schfs/obj.c Normal file
View file

@ -0,0 +1,441 @@
// SPDX-License-Identifier: Apache-2.0
//
// SchFS object layer, read side: inodes, directories and file data.
//
// Everything here is a record in the one B-tree, which is the point of the
// btrfs-style design — there is no inode table, no directory block format and
// no indirect-block scheme to get wrong, only key types:
//
// (ino, INODE, 0) struct schfs_inode
// (dir, DIRENT, hash) packed list of schfs_dirent_rec + name
// (ino, EXTENT, fileblk) struct schfs_extent
// (CSUM, CSUM, diskblk) uint32 CRC32c of that data block
//
// Kernel code reaches all of this through a caller-supplied cursor, so no
// function here puts a 4096-byte block on the stack.
#include "schfs_int.h"
#ifdef SCHFS_HOSTED
#include <string.h>
#else
#include "types.h"
#include "defs.h"
#endif
// Point a cursor at the volume's current tree, keeping the caller's block
// buffer. Called before every operation rather than once at open, because the
// tree root moves on every commit under copy-on-write: a cursor that cached the
// old root would keep reading a stale — though perfectly valid — filesystem.
static void
cur_bind(struct schfs_cursor *c, struct schfs_vol *v)
{
c->t = &v->tree;
c->depth = 0;
c->valid = 0;
}
void
schfs_mkkey(struct schfs_key *k, uint64_t objectid, uint8_t type,
uint64_t offset)
{
memset(k, 0, sizeof(*k));
k->objectid = objectid;
k->type = type;
k->offset = offset;
}
// FNV-1a, 64-bit. Directory lookup is a tree descent on this value, so it wants
// to spread short, highly similar names ("ls", "ln", "cp") across the keyspace.
// FNV does that, is a dozen lines, and needs no table — worth more here than
// the marginally better distribution of something larger.
//
// Collisions are handled by the packed dirent list rather than assumed away;
// see struct schfs_dirent_rec.
// Test-only: narrows the hash so collisions become common.
//
// With a full 64-bit hash, two names in one directory colliding is so unlikely
// that the packed-list code which handles it would never run — and untested
// code that only executes on rare inputs is exactly the code that loses a file
// years later. Setting this to a small mask makes every directory a pile of
// collisions, so the round-trip test exercises the path properly. Always ~0
// in the kernel and in real images.
uint64_t schfs_hash_mask = ~0ULL;
uint64_t
schfs_namehash(const char *name, unsigned len)
{
uint64_t h = 1469598103934665603ULL; /* FNV offset basis */
for (unsigned i = 0; i < len; i++) {
h ^= (uint8_t)name[i];
h *= 1099511628211ULL; /* FNV prime */
}
// Key ordering is by this value, so a directory's entries come back in an
// order that has nothing to do with their names. That is fine for readdir
// (POSIX promises no order) and is what makes lookup one descent.
return h & schfs_hash_mask;
}
/* ------------------------------------------------------------------ */
/* Inodes */
/* ------------------------------------------------------------------ */
int
schfs_inode_get(struct schfs_vol *v, struct schfs_cursor *c,
uint64_t ino, struct schfs_inode *out)
{
struct schfs_key k;
schfs_mkkey(&k, ino, SCHFS_KEY_INODE, 0);
cur_bind(c, v);
unsigned len = sizeof(*out);
int r = schfs_btree_lookup_cur(c, &k, out, &len);
if (r != 0)
return r;
// A short record means an image written by an older or broken tool. Reject
// it rather than acting on a half-filled struct.
if (len != sizeof(*out))
return -2;
return 0;
}
/* ------------------------------------------------------------------ */
/* Directories */
/* ------------------------------------------------------------------ */
// Walk a packed dirent list, calling fn per entry. Stops early if fn returns
// non-zero. Returns fn's value, or -2 if the list is malformed.
static int
dirent_walk(const void *val, unsigned len,
int (*fn)(const struct schfs_dirent_rec *, const char *, void *),
void *arg)
{
unsigned off = 0;
while (off < len) {
if (len - off < sizeof(struct schfs_dirent_rec))
return -2;
const struct schfs_dirent_rec *d =
(const struct schfs_dirent_rec *)((const char *)val + off);
unsigned step = SCHFS_DIRENT_LEN(d->namelen);
// Bounds-check before touching the name: a corrupt namelen would
// otherwise read past the end of the block.
if (d->namelen == 0 || d->namelen > SCHFS_NAME_MAX || off + step > len)
return -2;
int r = fn(d, (const char *)d + sizeof(*d), arg);
if (r)
return r;
off += step;
}
return 0;
}
struct find_ctx {
const char *name;
unsigned namelen;
uint64_t ino;
uint16_t type;
};
static int
find_one(const struct schfs_dirent_rec *d, const char *nm, void *arg)
{
struct find_ctx *f = (struct find_ctx *)arg;
if (d->namelen != f->namelen)
return 0;
if (memcmp(nm, f->name, f->namelen) != 0)
return 0;
f->ino = d->objectid;
f->type = d->type;
return 1; /* found; stop */
}
int
schfs_dir_lookup(struct schfs_vol *v, struct schfs_cursor *c,
uint64_t dir, const char *name, unsigned namelen,
uint64_t *out_ino, uint16_t *out_type)
{
if (namelen == 0 || namelen > SCHFS_NAME_MAX)
return -1;
struct schfs_key k;
schfs_mkkey(&k, dir, SCHFS_KEY_DIRENT, schfs_namehash(name, namelen));
cur_bind(c, v);
int r = schfs_cursor_seek(c, &k);
if (r == -2)
return -2;
if (r != 0)
return -1;
const struct schfs_key *got = schfs_cursor_key(c);
if (!got || schfs_key_cmp(got, &k) != 0)
return -1;
unsigned len;
const void *val = schfs_cursor_val(c, &len);
if (!val)
return -2;
struct find_ctx f;
f.name = name; f.namelen = namelen; f.ino = 0; f.type = 0;
r = dirent_walk(val, len, find_one, &f);
if (r == -2)
return -2;
if (r != 1)
return -1; /* hash matched, name did not */
if (out_ino) *out_ino = f.ino;
if (out_type) *out_type = f.type;
return 0;
}
struct iter_ctx {
int (*fn)(uint64_t, uint16_t, const char *, unsigned, void *);
void *arg;
struct schfs_cursor *cur;
uint64_t dir;
int stop;
};
// Hand each entry of one packed dirent list to the caller.
//
// The list lives in the cursor's block buffer, and almost every readdir caller
// immediately looks each entry up — to stat it, to recurse into it — through a
// cursor that may well share that buffer. Walking the list in place meant the
// name and every record after it turned to garbage the moment the callback
// touched the filesystem, which is how an extracted tree once ended up with
// directories reparented under their own children.
//
// The obvious fix, copying the whole item aside, costs SCHFS_MAX_INLINE (just
// over 1 KB) of stack — a quarter of the kernel's entire 4096-byte stack, on a
// path that also runs under procread's 2 KB buffer. So only the current entry
// is copied, and the item is re-fetched from the cursor each time round:
// schfs_cursor_val re-reads the block, which restores whatever the callback
// disturbed.
static int
iter_item(const struct schfs_key *k, const void *val, unsigned len, void *arg)
{
struct iter_ctx *ic = (struct iter_ctx *)arg;
(void)val;
// The range scan runs until something stops it, so stopping at the first
// key that is no longer this directory's is what bounds the walk.
if (k->objectid != ic->dir || k->type != SCHFS_KEY_DIRENT)
return 1;
if (len > SCHFS_MAX_INLINE)
return 1;
unsigned off = 0;
while (off < len) {
unsigned now;
const char *v = (const char *)schfs_cursor_val(ic->cur, &now);
if (!v || now != len) {
ic->stop = -2;
return 1;
}
if (len - off < sizeof(struct schfs_dirent_rec)) {
ic->stop = -2;
return 1;
}
const struct schfs_dirent_rec *d =
(const struct schfs_dirent_rec *)(v + off);
unsigned namelen = d->namelen;
unsigned step = SCHFS_DIRENT_LEN(namelen);
// Bounds-check before touching the name: a corrupt namelen would
// otherwise read past the end of the block.
if (namelen == 0 || namelen > SCHFS_NAME_MAX || off + step > len) {
ic->stop = -2;
return 1;
}
char name[SCHFS_NAME_MAX];
uint64_t oid = d->objectid;
uint16_t type = d->type;
memmove(name, (const char *)d + sizeof(*d), namelen);
int r = ic->fn(oid, type, name, namelen, ic->arg);
if (r) {
ic->stop = r;
return 1;
}
off += step;
}
return 0;
}
int
schfs_dir_iterate(struct schfs_vol *v, struct schfs_cursor *c, uint64_t dir,
int (*fn)(uint64_t, uint16_t, const char *, unsigned, void *),
void *arg)
{
struct schfs_key start;
schfs_mkkey(&start, dir, SCHFS_KEY_DIRENT, 0);
struct iter_ctx ic;
ic.fn = fn; ic.arg = arg; ic.cur = c; ic.dir = dir; ic.stop = 0;
cur_bind(c, v);
int r = schfs_btree_range(c, &start, iter_item, &ic);
if (r == -2)
return -2;
return ic.stop == 1 ? 0 : ic.stop;
}
/* ------------------------------------------------------------------ */
/* File data */
/* ------------------------------------------------------------------ */
// Map a file block to a disk block. Returns 1 if mapped, 0 for a hole, -2 on
// corruption. Extents are keyed by the file block they start at, so the record
// governing block B is the last one at or before (ino, EXTENT, B).
static int
extent_map(struct schfs_vol *v, struct schfs_cursor *c, uint64_t ino,
uint64_t fblk, uint64_t *out)
{
struct schfs_key k;
schfs_mkkey(&k, ino, SCHFS_KEY_EXTENT, fblk);
cur_bind(c, v);
int r = schfs_btree_lookup_le(c, &k);
if (r == -2)
return -2;
if (r != 0)
return 0;
const struct schfs_key *got = schfs_cursor_key(c);
if (!got)
return 0;
// lookup_le can land on some other object's records entirely; a sparse file
// whose first extent starts past this block looks the same way.
if (got->objectid != ino || got->type != SCHFS_KEY_EXTENT)
return 0;
unsigned len;
const struct schfs_extent *e =
(const struct schfs_extent *)schfs_cursor_val(c, &len);
if (!e || len != sizeof(*e))
return -2;
if (fblk >= got->offset + e->nblocks)
return 0; /* hole past the end of this extent */
*out = e->disk_block + (fblk - got->offset);
return 1;
}
// Verify a data block against its checksum record. Tree nodes carry their
// checksum in the parent pointer; data blocks have no parent, so the check must
// be an explicit lookup. Returns 0 if good, -2 if it fails, and 0 as well when
// no record exists — an image may legitimately predate checksums for a block,
// and refusing to read data over a *missing* checksum would turn an upgrade
// into a total loss.
static int
csum_check(struct schfs_vol *v, struct schfs_cursor *c, uint64_t dblk,
const void *data)
{
struct schfs_key k;
schfs_mkkey(&k, SCHFS_CSUM_OBJECTID, SCHFS_KEY_CSUM, dblk);
uint32_t want;
unsigned len = sizeof(want);
cur_bind(c, v);
int r = schfs_btree_lookup_cur(c, &k, &want, &len);
if (r == -1)
return 0;
if (r != 0 || len != sizeof(want))
return -2;
if (schfs_crc32c(0, data, SCHFS_BLKSIZE) != want)
return -2;
return 0;
}
long
schfs_file_read(struct schfs_vol *v, struct schfs_cursor *c, uint64_t ino,
uint64_t off, void *buf, unsigned n, void *dblk)
{
struct schfs_inode in;
int r = schfs_inode_get(v, c, ino, &in);
if (r != 0)
return r;
if (off >= in.size)
return 0;
if (off + n > in.size)
n = (unsigned)(in.size - off);
char *dst = (char *)buf;
unsigned done = 0;
while (done < n) {
uint64_t fblk = (off + done) / SCHFS_BLKSIZE;
unsigned inblk = (unsigned)((off + done) % SCHFS_BLKSIZE);
unsigned want = SCHFS_BLKSIZE - inblk;
if (want > n - done)
want = n - done;
uint64_t disk = 0;
int m = extent_map(v, c, ino, fblk, &disk);
if (m == -2)
return -2;
if (m == 0) {
memset(dst + done, 0, want); /* hole reads as zeros */
} else {
if (v->dev.bread(v->dev.ctx, disk, dblk) != 0)
return -2;
if (csum_check(v, c, disk, dblk) != 0)
return -2;
memmove(dst + done, (char *)dblk + inblk, want);
}
done += want;
}
return (long)done;
}
/* ------------------------------------------------------------------ */
/* Path resolution */
/* ------------------------------------------------------------------ */
uint64_t
schfs_path_lookup(struct schfs_vol *v, struct schfs_cursor *c, const char *path)
{
uint64_t cur = v->root_objectid;
while (*path == '/')
path++;
while (*path) {
const char *s = path;
while (*path && *path != '/')
path++;
unsigned len = (unsigned)(path - s);
while (*path == '/')
path++;
if (len == 1 && s[0] == '.')
continue;
if (len == 2 && s[0] == '.' && s[1] == '.') {
// ".." is synthetic: directories store a parent id rather than an
// entry, so walking up is an inode read, not a lookup.
struct schfs_inode in;
if (schfs_inode_get(v, c, cur, &in) != 0)
return 0;
cur = in.parent ? in.parent : v->root_objectid;
continue;
}
// Every component but the last must be a directory. Checking here is
// what makes "cat /etc/passwd/x" fail rather than silently succeed.
struct schfs_inode in;
if (schfs_inode_get(v, c, cur, &in) != 0)
return 0;
if (in.type != SCHFS_T_DIR)
return 0;
uint64_t next;
if (schfs_dir_lookup(v, c, cur, s, len, &next, 0) != 0)
return 0;
cur = next;
}
return cur;
}

932
kernel/fs/schfs/objw.c Normal file
View file

@ -0,0 +1,932 @@
// SPDX-License-Identifier: Apache-2.0
//
// SchFS object layer, write side: create, link, unlink, rename, write,
// truncate.
//
// The same portable C as the read side, compiled both hosted (mkschfs) and
// freestanding (the kernel mount), so an image the tools produce and one the
// kernel produces come from a single implementation rather than two that agree
// by luck. Everything below is expressed as records in the one B-tree; there is
// no inode table, no directory block format and no bitmap to keep in step.
//
// Two rules run through the whole file.
//
// Nothing here may put a block on the stack. A SchFS block is 4096 bytes and so
// is the entire kernel stack, so every function borrows the volume's three
// scratch buffers (v->ctx) instead. Where a cursor is needed, ctx.a doubles as
// its buffer — which is safe only because no path below holds a cursor across a
// tree mutation, and every one of them copies what it needs out first.
//
// Nothing reachable from a committed root is ever overwritten. For tree blocks
// the B-tree enforces that; for *data* blocks it is enforced here, by
// schfs_extent.gen. A run allocated during the group being built may be written
// in place, because no published root can see it. Anything older is copied to a
// fresh block and the extent record split around it.
#include "schfs_int.h"
#ifdef SCHFS_HOSTED
#include <string.h>
#else
#include "types.h"
#include "defs.h"
#endif
/* ------------------------------------------------------------------ */
/* Inodes */
/* ------------------------------------------------------------------ */
uint64_t
schfs_alloc_objectid(struct schfs_vol *v)
{
return v->next_objectid++;
}
int
schfs_inode_put(struct schfs_vol *v, uint64_t ino, const struct schfs_inode *in)
{
struct schfs_key k;
schfs_mkkey(&k, ino, SCHFS_KEY_INODE, 0);
return schfs_btree_insert(&v->tree, &v->ctx, &k, in, sizeof(*in));
}
// Read an inode using the volume's own scratch block. Every mutation below is a
// read-modify-write of one of these, and each one that took a cursor buffer
// from its caller was one more place to get the aliasing rules wrong.
static int
inode_load(struct schfs_vol *v, uint64_t ino, struct schfs_inode *out)
{
struct schfs_cursor c;
schfs_cursor_init(&c, &v->tree, v->ctx.a);
return schfs_inode_get(v, &c, ino, out);
}
/* ------------------------------------------------------------------ */
/* Directory entries */
/* */
/* One key per name hash, holding a packed list of records, because two */
/* names in a directory can hash the same and the second insert would */
/* otherwise silently replace the first. */
/* ------------------------------------------------------------------ */
// Load the packed list for `name`'s hash into v->dirbuf. Returns its length, or
// 0 if there is no such key, or a negative error.
static int
dirlist_load(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen, struct schfs_key *k)
{
schfs_mkkey(k, dir, SCHFS_KEY_DIRENT, schfs_namehash(name, namelen));
unsigned len = sizeof(v->dirbuf);
int r = schfs_btree_lookup(&v->tree, &v->ctx, k, v->dirbuf, &len);
if (r == -2)
return -2;
if (r != 0)
return 0;
if (len > sizeof(v->dirbuf))
return -2; /* stored value larger than can exist */
return (int)len;
}
// Offset of `name` within the packed list, or -1 if absent, -2 if malformed.
static int
dirlist_find(const char *list, unsigned len, const char *name, unsigned namelen)
{
for (unsigned off = 0; off + sizeof(struct schfs_dirent_rec) <= len; ) {
const struct schfs_dirent_rec *d =
(const struct schfs_dirent_rec *)(list + off);
unsigned step = SCHFS_DIRENT_LEN(d->namelen);
if (step == 0 || off + step > len)
return -2;
if (d->namelen == namelen &&
memcmp(list + off + sizeof(*d), name, namelen) == 0)
return (int)off;
off += step;
}
return -1;
}
int
schfs_dir_add(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen, uint64_t child, uint16_t type)
{
if (namelen == 0 || namelen > SCHFS_NAME_MAX)
return -1;
struct schfs_key k;
int got = dirlist_load(v, dir, name, namelen, &k);
if (got < 0)
return got;
unsigned len = (unsigned)got;
// Reject a duplicate outright. Without this, adding a name twice produces a
// directory with two entries that lookup can never tell apart.
int at = dirlist_find(v->dirbuf, len, name, namelen);
if (at == -2)
return -2;
if (at >= 0)
return -1;
unsigned add = SCHFS_DIRENT_LEN(namelen);
if (len + add > sizeof(v->dirbuf))
return -4; /* collision list will not fit in one item */
struct schfs_dirent_rec *d = (struct schfs_dirent_rec *)(v->dirbuf + len);
memset(d, 0, add);
d->objectid = child;
d->type = type;
d->namelen = (uint16_t)namelen;
memmove(v->dirbuf + len + sizeof(*d), name, namelen);
return schfs_btree_insert(&v->tree, &v->ctx, &k, v->dirbuf, len + add);
}
int
schfs_dir_del(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen)
{
if (namelen == 0 || namelen > SCHFS_NAME_MAX)
return -1;
struct schfs_key k;
int got = dirlist_load(v, dir, name, namelen, &k);
if (got < 0)
return got;
unsigned len = (unsigned)got;
if (len == 0)
return -1;
int at = dirlist_find(v->dirbuf, len, name, namelen);
if (at == -2)
return -2;
if (at < 0)
return -1;
const struct schfs_dirent_rec *d =
(const struct schfs_dirent_rec *)(v->dirbuf + at);
unsigned step = SCHFS_DIRENT_LEN(d->namelen);
memmove(v->dirbuf + at, v->dirbuf + at + step, len - (unsigned)at - step);
len -= step;
// An emptied list means the key itself goes, not a zero-length value: a key
// that exists with nothing in it would make the directory scan stop on a
// record readdir cannot describe.
if (len == 0)
return schfs_btree_delete(&v->tree, &v->ctx, &k);
return schfs_btree_insert(&v->tree, &v->ctx, &k, v->dirbuf, len);
}
int
schfs_dir_empty(struct schfs_vol *v, uint64_t dir)
{
struct schfs_key start;
schfs_mkkey(&start, dir, SCHFS_KEY_DIRENT, 0);
struct schfs_cursor c;
schfs_cursor_init(&c, &v->tree, v->ctx.a);
int r = schfs_cursor_seek(&c, &start);
if (r == -2)
return -2;
if (r != 0)
return 1; /* nothing at or after: empty */
const struct schfs_key *got = schfs_cursor_key(&c);
if (!got)
return 1;
return (got->objectid == dir && got->type == SCHFS_KEY_DIRENT) ? 0 : 1;
}
/* ------------------------------------------------------------------ */
/* Data-block checksums */
/* */
/* Tree nodes carry their checksum in their parent's pointer. Data */
/* blocks have no parent, so theirs live under one reserved object id, */
/* exactly as btrfs keeps a checksum tree. */
/* ------------------------------------------------------------------ */
static int
csum_put(struct schfs_vol *v, uint64_t dblk, const void *data)
{
struct schfs_key k;
schfs_mkkey(&k, SCHFS_CSUM_OBJECTID, SCHFS_KEY_CSUM, dblk);
uint32_t c = schfs_crc32c(0, data, SCHFS_BLKSIZE);
return schfs_btree_insert(&v->tree, &v->ctx, &k, &c, sizeof(c));
}
static int
csum_del(struct schfs_vol *v, uint64_t dblk)
{
struct schfs_key k;
schfs_mkkey(&k, SCHFS_CSUM_OBJECTID, SCHFS_KEY_CSUM, dblk);
int r = schfs_btree_delete(&v->tree, &v->ctx, &k);
return r == -1 ? 0 : r; /* absent is not a failure */
}
/* ------------------------------------------------------------------ */
/* Extents */
/* ------------------------------------------------------------------ */
static int
extent_put(struct schfs_vol *v, uint64_t ino, uint64_t fblk,
const struct schfs_extent *e)
{
struct schfs_key k;
schfs_mkkey(&k, ino, SCHFS_KEY_EXTENT, fblk);
return schfs_btree_insert(&v->tree, &v->ctx, &k, e, sizeof(*e));
}
static int
extent_del(struct schfs_vol *v, uint64_t ino, uint64_t fblk)
{
struct schfs_key k;
schfs_mkkey(&k, ino, SCHFS_KEY_EXTENT, fblk);
int r = schfs_btree_delete(&v->tree, &v->ctx, &k);
return r == -1 ? 0 : r;
}
// The extent covering file block `fblk`, if any. Extents are keyed by the file
// block they start at, so the record governing block B is the last one at or
// before (ino, EXTENT, B). Returns 1 and fills *start and *e, 0 for a hole, -2
// on corruption.
static int
extent_find(struct schfs_vol *v, uint64_t ino, uint64_t fblk,
uint64_t *start, struct schfs_extent *e)
{
struct schfs_cursor c;
schfs_cursor_init(&c, &v->tree, v->ctx.a);
struct schfs_key probe;
schfs_mkkey(&probe, ino, SCHFS_KEY_EXTENT, fblk);
int r = schfs_btree_lookup_le(&c, &probe);
if (r == -2)
return -2;
if (r != 0)
return 0;
const struct schfs_key *got = schfs_cursor_key(&c);
unsigned vlen;
const struct schfs_extent *found =
(const struct schfs_extent *)schfs_cursor_val(&c, &vlen);
if (!got || !found)
return 0;
// lookup_le happily lands on some other object's records; so does a sparse
// file whose first extent starts past this block.
if (got->objectid != ino || got->type != SCHFS_KEY_EXTENT)
return 0;
if (vlen != sizeof(*e))
return -2;
if (fblk >= got->offset + found->nblocks)
return 0;
*start = got->offset;
*e = *found;
return 1;
}
// The last extent record belonging to `ino`, or 0 if it has none.
static int
extent_last(struct schfs_vol *v, uint64_t ino, uint64_t *start,
struct schfs_extent *e)
{
struct schfs_cursor c;
schfs_cursor_init(&c, &v->tree, v->ctx.a);
struct schfs_key probe;
schfs_mkkey(&probe, ino, SCHFS_KEY_EXTENT, ~0ULL);
int r = schfs_btree_lookup_le(&c, &probe);
if (r == -2)
return -2;
if (r != 0)
return 0;
const struct schfs_key *got = schfs_cursor_key(&c);
unsigned vlen;
const struct schfs_extent *found =
(const struct schfs_extent *)schfs_cursor_val(&c, &vlen);
if (!got || !found)
return 0;
if (got->objectid != ino || got->type != SCHFS_KEY_EXTENT)
return 0;
if (vlen != sizeof(*e))
return -2;
*start = got->offset;
*e = *found;
return 1;
}
// Record a newly allocated single block, merging it into the preceding extent
// when the two are adjacent both in the file and on the disk.
//
// This is what keeps extents worth having. Sequential writes get consecutive
// blocks from the allocator, so without merging a one-megabyte file carries 256
// extent records instead of one, and the tree ends up mostly describing where
// the data is rather than holding it.
static int
extent_grow(struct schfs_vol *v, uint64_t ino, uint64_t fblk,
const struct schfs_extent *e)
{
if (fblk > 0) {
uint64_t pstart;
struct schfs_extent p;
int m = extent_find(v, ino, fblk - 1, &pstart, &p);
if (m == -2)
return -2;
if (m == 1 &&
pstart + p.nblocks == fblk &&
p.disk_block + p.nblocks == e->disk_block &&
p.refs == e->refs && p.gen == e->gen) {
p.nblocks++;
return extent_put(v, ino, pstart, &p);
}
}
return extent_put(v, ino, fblk, e);
}
// Release a run of data blocks and the checksum records that describe them.
// Shared runs are left alone: another file still points at them, and proper
// refcounting needs an extent tree that nothing creates yet.
static int
extent_release(struct schfs_vol *v, const struct schfs_extent *e,
uint64_t first, uint64_t n)
{
if (e->refs != 1)
return 0;
for (uint64_t i = 0; i < n; i++) {
int r = csum_del(v, first + i);
if (r != 0)
return r;
v->dev.bfree(v->dev.ctx, first + i);
}
return 0;
}
// Drop everything from file block `from` onward. Used by truncate and by the
// deletion of an object.
static int
extents_drop_from(struct schfs_vol *v, uint64_t ino, struct schfs_inode *in,
uint64_t from)
{
for (;;) {
uint64_t start;
struct schfs_extent e;
int m = extent_last(v, ino, &start, &e);
if (m == -2)
return -2;
if (m == 0)
return 0; /* no extents left */
if (start + e.nblocks <= from)
return 0; /* nothing beyond the cut */
if (start >= from) {
int r = extent_release(v, &e, e.disk_block, e.nblocks);
if (r != 0)
return r;
r = extent_del(v, ino, start);
if (r != 0)
return r;
if (in->blocks >= e.nblocks)
in->blocks -= e.nblocks;
continue; /* there may be earlier ones too */
}
// The cut falls inside this run: keep its head, release its tail.
uint64_t keep = from - start;
uint64_t tail = e.nblocks - keep;
int r = extent_release(v, &e, e.disk_block + keep, tail);
if (r != 0)
return r;
e.nblocks = keep;
r = extent_put(v, ino, start, &e);
if (r != 0)
return r;
if (in->blocks >= tail)
in->blocks -= tail;
return 0;
}
}
/* ------------------------------------------------------------------ */
/* File data */
/* ------------------------------------------------------------------ */
int
schfs_file_reserve(struct schfs_vol *v, uint64_t ino, uint64_t nblocks)
{
if (nblocks == 0)
return 0;
// Take the whole run before touching the tree.
//
// This is what makes extents worth having when the final size is already
// known. Allocating a data block, then inserting its extent record -- which
// itself allocates blocks for the copy-on-write path -- then allocating the
// next data block interleaves metadata with data, so consecutive file
// blocks are never physically adjacent. Grabbing the run up front keeps it
// contiguous and a whole file becomes one record.
uint64_t first;
if (v->dev.balloc_run) {
first = v->dev.balloc_run(v->dev.ctx, nblocks);
if (first == 0)
return -3;
} else {
first = v->dev.balloc(v->dev.ctx);
if (first == 0)
return -3;
uint64_t prev = first;
for (uint64_t i = 1; i < nblocks; i++) {
uint64_t b = v->dev.balloc(v->dev.ctx);
if (b == 0)
return -3;
if (b != prev + 1)
return -2; /* allocator is not handing out a run */
prev = b;
}
}
struct schfs_extent e;
memset(&e, 0, sizeof(e));
e.disk_block = first;
e.nblocks = nblocks;
e.refs = 1;
e.gen = v->tree.generation;
// Deliberately does not touch the inode. Re-reading and rewriting it here
// costs two extra tree inserts per file, and every insert copies its whole
// path. The caller knows the block count before it writes the inode the
// first time, so it sets in.blocks there instead.
return extent_put(v, ino, 0, &e);
}
// Where the next write to file block `fblk` should land.
struct wblk {
uint64_t disk; // block to write
uint64_t src; // block holding the current contents, 0 for a hole
int fresh; // a block that did not exist before
};
static int
block_for_write(struct schfs_vol *v, uint64_t ino, uint64_t fblk,
struct wblk *w)
{
uint64_t start;
struct schfs_extent e;
int m = extent_find(v, ino, fblk, &start, &e);
if (m == -2)
return -2;
if (m == 0) {
uint64_t d = v->dev.balloc(v->dev.ctx);
if (d == 0)
return -3;
struct schfs_extent ne;
memset(&ne, 0, sizeof(ne));
ne.disk_block = d;
ne.nblocks = 1;
ne.refs = 1;
ne.gen = v->tree.generation;
int r = extent_grow(v, ino, fblk, &ne);
if (r != 0)
return r;
w->disk = d;
w->src = 0;
w->fresh = 1;
return 0;
}
uint64_t old = e.disk_block + (fblk - start);
// Allocated by the group being built, and owned outright: no published root
// can reach it, so it may simply be overwritten.
if (e.gen == v->tree.generation && e.refs == 1) {
w->disk = old;
w->src = old;
w->fresh = 0;
return 0;
}
// Otherwise copy this one block out of the run, and split the record around
// it: a head covering what came before, the new block, and a tail covering
// what came after. No deletion is needed — when the block being replaced is
// the run's first, the middle record simply overwrites the original key.
uint64_t nd = v->dev.balloc(v->dev.ctx);
if (nd == 0)
return -3;
if (fblk > start) {
struct schfs_extent head = e;
head.nblocks = fblk - start;
int r = extent_put(v, ino, start, &head);
if (r != 0)
return r;
}
{
struct schfs_extent mid;
memset(&mid, 0, sizeof(mid));
mid.disk_block = nd;
mid.nblocks = 1;
mid.refs = 1;
mid.gen = v->tree.generation;
int r = extent_put(v, ino, fblk, &mid);
if (r != 0)
return r;
}
if (fblk + 1 < start + e.nblocks) {
struct schfs_extent tail = e;
tail.disk_block = e.disk_block + (fblk + 1 - start);
tail.nblocks = start + e.nblocks - (fblk + 1);
int r = extent_put(v, ino, fblk + 1, &tail);
if (r != 0)
return r;
}
// The block just replaced rejoins the free map at the next commit, not now:
// until then the committed tree still points at it and a crash lands there.
int r = extent_release(v, &e, old, 1);
if (r != 0)
return r;
w->disk = nd;
w->src = old;
w->fresh = 0;
return 0;
}
long
schfs_file_write(struct schfs_vol *v, uint64_t ino, uint64_t off,
const void *buf, unsigned n, void *dblk)
{
struct schfs_inode in;
int r = inode_load(v, ino, &in);
if (r != 0)
return r;
const char *src = (const char *)buf;
unsigned done = 0;
while (done < n) {
uint64_t at = off + done;
uint64_t fblk = at / SCHFS_BLKSIZE;
unsigned inblk = (unsigned)(at % SCHFS_BLKSIZE);
unsigned want = SCHFS_BLKSIZE - inblk;
if (want > n - done)
want = n - done;
struct wblk w;
r = block_for_write(v, ino, fblk, &w);
if (r != 0)
return r;
if (w.fresh)
in.blocks++;
// A whole-block write needs no read: every byte is about to be
// replaced. A partial one has to start from whatever is there, or from
// zeros where the file had a hole.
if (inblk != 0 || want != SCHFS_BLKSIZE) {
if (w.src) {
if (v->dev.bread(v->dev.ctx, w.src, dblk) != 0)
return -2;
} else {
memset(dblk, 0, SCHFS_BLKSIZE);
}
}
memmove((char *)dblk + inblk, src + done, want);
if (v->dev.bwrite(v->dev.ctx, w.disk, dblk) != 0)
return -2;
// The checksum is rewritten every time the block changes, not just when
// it is allocated, or a topped-up tail block fails its own check on the
// very next read.
r = csum_put(v, w.disk, dblk);
if (r != 0)
return r;
done += want;
}
if (off + n > in.size)
in.size = off + n;
in.gen = v->tree.generation;
r = schfs_inode_put(v, ino, &in);
if (r != 0)
return r;
return (long)done;
}
int
schfs_file_append(struct schfs_vol *v, uint64_t ino, const void *buf,
unsigned n, void *dblk)
{
struct schfs_inode in;
int r = inode_load(v, ino, &in);
if (r != 0)
return r == -1 ? -2 : r;
long got = schfs_file_write(v, ino, in.size, buf, n, dblk);
if (got < 0)
return (int)got;
return got == (long)n ? 0 : -2;
}
int
schfs_file_truncate(struct schfs_vol *v, uint64_t ino, uint64_t size,
void *dblk)
{
struct schfs_inode in;
int r = inode_load(v, ino, &in);
if (r != 0)
return r;
if (size < in.size) {
uint64_t keep = (size + SCHFS_BLKSIZE - 1) / SCHFS_BLKSIZE;
r = extents_drop_from(v, ino, &in, keep);
if (r != 0)
return r;
// Whole blocks past the new end are gone; the block the new size falls
// *inside* has to have its tail zeroed. It is tempting not to bother,
// since a read stops at in.size and can never see those bytes — but
// truncating down and then growing again is required to read back
// zeros, and without this it reads back whatever the file used to hold.
unsigned tail = (unsigned)(size % SCHFS_BLKSIZE);
if (tail != 0) {
uint64_t fblk = size / SCHFS_BLKSIZE;
uint64_t start;
struct schfs_extent e;
int m = extent_find(v, ino, fblk, &start, &e);
if (m == -2)
return -2;
if (m == 1) { /* a hole is already zeros */
struct wblk w;
r = block_for_write(v, ino, fblk, &w);
if (r != 0)
return r;
if (w.src) {
if (v->dev.bread(v->dev.ctx, w.src, dblk) != 0)
return -2;
} else {
memset(dblk, 0, SCHFS_BLKSIZE);
}
memset((char *)dblk + tail, 0, SCHFS_BLKSIZE - tail);
if (v->dev.bwrite(v->dev.ctx, w.disk, dblk) != 0)
return -2;
r = csum_put(v, w.disk, dblk);
if (r != 0)
return r;
if (w.fresh)
in.blocks++;
}
}
}
// Growing needs nothing at all: the range with no extent records reads back
// as zeros, which is a sparse file for free.
in.size = size;
in.gen = v->tree.generation;
return schfs_inode_put(v, ino, &in);
}
// Release an object entirely: its data, its checksums and its inode record.
int
schfs_object_free(struct schfs_vol *v, uint64_t ino)
{
struct schfs_inode in;
int r = inode_load(v, ino, &in);
if (r == -1)
return 0; /* already gone */
if (r != 0)
return r;
r = extents_drop_from(v, ino, &in, 0);
if (r != 0)
return r;
struct schfs_key k;
schfs_mkkey(&k, ino, SCHFS_KEY_INODE, 0);
r = schfs_btree_delete(&v->tree, &v->ctx, &k);
return r == -1 ? 0 : r;
}
/* ------------------------------------------------------------------ */
/* Namespace */
/* ------------------------------------------------------------------ */
// A directory's link count is 2 (itself and its own ".") plus one per
// subdirectory, because each subdirectory's ".." points back at it. Neither
// entry is stored, but the count still has to match what POSIX tools expect or
// "find -type d" prunes real directories and du walks the tree wrongly.
static int
parent_nlink_adjust(struct schfs_vol *v, uint64_t dir, int delta)
{
struct schfs_inode in;
int r = inode_load(v, dir, &in);
if (r != 0)
return r;
if (delta < 0 && in.nlink < (uint32_t)(-delta))
return -2;
in.nlink = (uint32_t)((int64_t)in.nlink + delta);
return schfs_inode_put(v, dir, &in);
}
int
schfs_create(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen, uint16_t type, uint32_t mode,
uint16_t rdev_major, uint16_t rdev_minor, uint64_t now,
uint64_t *out_ino)
{
if (namelen == 0 || namelen > SCHFS_NAME_MAX)
return -1;
struct schfs_inode parent;
int r = inode_load(v, dir, &parent);
if (r != 0)
return r;
if (parent.type != SCHFS_T_DIR)
return -1;
uint64_t ino = schfs_alloc_objectid(v);
struct schfs_inode in;
memset(&in, 0, sizeof(in));
in.type = type;
in.parent = dir;
in.mode = mode;
in.nlink = type == SCHFS_T_DIR ? 2 : 1;
in.rdev_major = rdev_major;
in.rdev_minor = rdev_minor;
in.atime = in.mtime = in.ctime = now;
in.gen = v->tree.generation;
r = schfs_inode_put(v, ino, &in);
if (r != 0)
return r;
// The entry goes in last, so a failure leaves an unreferenced object rather
// than a name pointing at nothing. Neither survives a crash — none of this
// is visible until the transaction commits — but an unreachable inode is
// the harmless one of the two if a later step in the same group fails.
r = schfs_dir_add(v, dir, name, namelen, ino, type);
if (r != 0)
return r;
if (type == SCHFS_T_DIR) {
r = parent_nlink_adjust(v, dir, +1);
if (r != 0)
return r;
}
if (out_ino)
*out_ino = ino;
return 0;
}
int
schfs_link(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen, uint64_t ino)
{
struct schfs_inode in;
int r = inode_load(v, ino, &in);
if (r != 0)
return r;
// Directories are not hard-linkable. Allowing it makes cycles possible, and
// a cycle turns every recursive walk into an infinite one.
if (in.type == SCHFS_T_DIR)
return -1;
r = schfs_dir_add(v, dir, name, namelen, ino, in.type);
if (r != 0)
return r;
in.nlink++;
in.ctime = in.mtime;
return schfs_inode_put(v, ino, &in);
}
int
schfs_unlink(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen)
{
uint64_t ino;
uint16_t type;
{
struct schfs_cursor c;
schfs_cursor_init(&c, &v->tree, v->ctx.a);
int r = schfs_dir_lookup(v, &c, dir, name, namelen, &ino, &type);
if (r != 0)
return r;
}
struct schfs_inode in;
int r = inode_load(v, ino, &in);
if (r != 0)
return r;
if (in.type == SCHFS_T_DIR) {
int e = schfs_dir_empty(v, ino);
if (e == -2)
return -2;
if (e == 0)
return -5; /* directory not empty */
}
r = schfs_dir_del(v, dir, name, namelen);
if (r != 0)
return r;
if (in.type == SCHFS_T_DIR) {
r = parent_nlink_adjust(v, dir, -1);
if (r != 0)
return r;
return schfs_object_free(v, ino);
}
if (in.nlink > 1) {
in.nlink--;
return schfs_inode_put(v, ino, &in);
}
return schfs_object_free(v, ino);
}
int
schfs_rename(struct schfs_vol *v, uint64_t olddir, const char *oldname,
unsigned oldlen, uint64_t newdir, const char *newname,
unsigned newlen)
{
uint64_t ino;
uint16_t type;
{
struct schfs_cursor c;
schfs_cursor_init(&c, &v->tree, v->ctx.a);
int r = schfs_dir_lookup(v, &c, olddir, oldname, oldlen, &ino, &type);
if (r != 0)
return r;
}
// Renaming a directory into its own subtree would detach that subtree from
// the root and leave it reachable only from itself. Walking up from the new
// parent is the cheap way to catch it, since every directory records its
// own parent.
if (type == SCHFS_T_DIR && newdir != olddir) {
uint64_t up = newdir;
for (int i = 0; i <= SCHFS_MAX_LEVEL * 8 && up != 0; i++) {
if (up == ino)
return -1;
struct schfs_inode t;
if (inode_load(v, up, &t) != 0)
break;
if (up == v->root_objectid)
break;
up = t.parent;
}
}
// A target that already exists is replaced, which means removing it first —
// and refusing when that would silently discard a non-empty directory.
{
struct schfs_cursor c;
schfs_cursor_init(&c, &v->tree, v->ctx.a);
uint64_t victim;
if (schfs_dir_lookup(v, &c, newdir, newname, newlen, &victim, 0) == 0) {
if (victim == ino)
return 0; /* renaming a name onto itself */
int r = schfs_unlink(v, newdir, newname, newlen);
if (r != 0)
return r;
}
}
int r = schfs_dir_add(v, newdir, newname, newlen, ino, type);
if (r != 0)
return r;
r = schfs_dir_del(v, olddir, oldname, oldlen);
if (r != 0)
return r;
if (newdir != olddir) {
if (type == SCHFS_T_DIR) {
// ".." is synthesised from the inode's parent field, so moving a
// directory means rewriting that field and moving the link its ".."
// contributes to each parent's count.
struct schfs_inode in;
r = inode_load(v, ino, &in);
if (r != 0)
return r;
in.parent = newdir;
r = schfs_inode_put(v, ino, &in);
if (r != 0)
return r;
r = parent_nlink_adjust(v, olddir, -1);
if (r != 0)
return r;
r = parent_nlink_adjust(v, newdir, +1);
if (r != 0)
return r;
}
}
return 0;
}

277
kernel/fs/schfs/schfs_int.h Normal file
View file

@ -0,0 +1,277 @@
// SPDX-License-Identifier: Apache-2.0
//
// SchFS internals: the volume (uberblock ring, block allocation) and the object
// layer (inodes, directories, file data) that sit on top of the B-tree.
//
// Split three ways so the kernel links only what it needs:
//
// vol.c open / format / commit — the uberblock ring
// obj.c read paths — kernel needs these in phase 2
// objw.c write paths — host tools only, until phase 3
//
// Everything here is portable C compiled both hosted (mkfs, fsck) and freestanding
// (kernel), so a bug is found on the host, where a test takes a second rather
// than a boot.
#ifndef SCHFS_INT_H
#define SCHFS_INT_H
#include "btree.h"
// ---------------------------------------------------------------------------
// space.c — the allocation bitmap
//
// One bit per block, held in RAM and derived from the committed tree at mount
// rather than stored on disk. See space.c for why.
//
// The bitmap is a vector of pages because the kernel allocates a page at a time
// and gives no promise that two are adjacent. That vector is fixed-size, and
// its length is therefore the hard ceiling on how large a volume this build can
// mount: SCHFS_SPACE_PAGES pages x SCHFS_SPACE_BITS_PER_PAGE blocks each.
// ---------------------------------------------------------------------------
#define SCHFS_SPACE_BITS_PER_PAGE ((uint64_t)SCHFS_BLKSIZE * 8) /* 32768 */
#define SCHFS_SPACE_PAGES 512
#define SCHFS_MAX_BLOCKS \
(SCHFS_SPACE_BITS_PER_PAGE * (uint64_t)SCHFS_SPACE_PAGES) /* 64 GiB */
struct schfs_space {
uint64_t *inuse[SCHFS_SPACE_PAGES]; // 1 = allocated
uint64_t *pend [SCHFS_SPACE_PAGES]; // 1 = freed, but not yet reusable
int npages;
uint64_t nblocks;
uint64_t free_blocks;
uint64_t pending; // bits set in pend[], counted
uint64_t hint; // rotating search start
};
// A mounted (or under-construction) volume.
//
// NOT copyable: tree.dev points at this struct's own dev field, so a copy
// leaves the tree talking to whichever struct it was built in. Open into the
// storage it will live in.
struct schfs_vol {
struct schfs_bdev dev;
struct schfs_btree tree;
// The three block buffers every mutation borrows, handed in at open or
// format and owned by the caller for the life of the volume. Nothing in
// SchFS may put a SCHFS_BLKSIZE buffer on the stack: a block is 4096 bytes
// and so is the whole kernel stack.
//
// The object layer also uses ctx.a as a cursor buffer, which is safe on the
// one condition that no cursor is still live when a tree mutation runs.
// Every write path here reads what it needs out of the cursor first.
struct schfs_ctx ctx;
struct schfs_space space;
uint64_t total_blocks;
uint64_t free_blocks;
uint64_t next_objectid;
uint64_t root_objectid;
uint64_t generation; // last *committed* transaction group
int uber_slot; // ring slot the last commit used
int readonly;
char label[SCHFS_LABEL_MAX];
uint8_t uuid[16];
// Staging for a directory's packed dirent list. Deliberately not one of the
// ctx buffers: a split reuses ctx.b and ctx.c while the value being
// inserted is still being read from, so the value needs storage of its own.
char dirbuf[SCHFS_MAX_INLINE];
};
// ---------------------------------------------------------------------------
// vol.c
// ---------------------------------------------------------------------------
// Scan the whole uberblock ring and adopt the highest generation that both
// checksums and points at a readable root. This *is* the crash recovery story:
// there is no journal to replay, because a torn update simply leaves an older
// slot as the newest valid one. Returns 0, -1 if no valid uberblock exists
// (not a SchFS volume), -2 on I/O error.
//
// `x` supplies the volume's three scratch blocks; they are kept for its whole
// lifetime, so they must outlive it and must not alias one another. `palloc`
// yields pages for the allocation bitmap and may be NULL for a read-only mount,
// which then cannot allocate at all.
int schfs_vol_open(struct schfs_vol *v, struct schfs_bdev *dev,
struct schfs_ctx *x, void *(*palloc)(void));
// Lay down an empty volume: one empty tree and one uberblock.
int schfs_vol_format(struct schfs_vol *v, struct schfs_bdev *dev,
uint64_t total_blocks, const char *label,
struct schfs_ctx *x, void *(*palloc)(void));
// Publish the current tree by writing the next slot in the ring. Until this
// returns, everything written since the last commit is invisible and a crash
// discards it — which is exactly what makes the format crash-safe.
int schfs_vol_commit(struct schfs_vol *v);
// Block allocation. Returns 0 when the volume is full.
//
// Not themselves schfs_bdev callbacks: a bdev has one ctx shared by all of its
// hooks, and the caller's block I/O needs that slot for its own state (a file
// handle, a device number). Callers wrap these in a one-line adapter instead.
uint64_t schfs_vol_alloc(struct schfs_vol *v);
uint64_t schfs_vol_alloc_run(struct schfs_vol *v, uint64_t n);
void schfs_vol_free(struct schfs_vol *v, uint64_t blk);
// ---------------------------------------------------------------------------
// space.c
// ---------------------------------------------------------------------------
// Build an empty map covering `nblocks`, with everything below
// SCHFS_FIRST_BLOCK already reserved. `palloc` yields SCHFS_BLKSIZE-sized
// pages; the map keeps them for the volume's lifetime. Returns 0, -3 if the
// volume is too small to hold anything, -4 if it is larger than this build can
// map (see SCHFS_MAX_BLOCKS), -5 if palloc ran out.
int schfs_space_init(struct schfs_space *s, uint64_t nblocks,
void *(*palloc)(void));
// Record a run as allocated. Idempotent, because an extent range may be shared
// by several files.
void schfs_space_mark(struct schfs_space *s, uint64_t blk, uint64_t n);
uint64_t schfs_space_alloc(struct schfs_space *s);
uint64_t schfs_space_alloc_run(struct schfs_space *s, uint64_t n);
// Release a block — but only for the *next* transaction group. Until the
// commit that supersedes it, the last committed tree still points here and a
// crash would land on it.
void schfs_space_free(struct schfs_space *s, uint64_t blk);
// Fold this group's deferred frees back into the free map. Called by
// schfs_vol_commit once the new uberblock is on the disk, never before.
void schfs_space_commit(struct schfs_space *s);
// Walk the committed tree and mark every block it reaches. This is what makes
// the map correct without storing it. Returns 0 or -2 on an unreadable tree.
int schfs_space_scan(struct schfs_vol *v);
// ---------------------------------------------------------------------------
// obj.c — read side
// ---------------------------------------------------------------------------
uint64_t schfs_namehash(const char *name, unsigned len);
// Test-only knob; see obj.c. Narrowing this forces directory-hash collisions
// so the packed-dirent path actually runs. Never changed outside tools/.
extern uint64_t schfs_hash_mask;
void schfs_mkkey(struct schfs_key *k, uint64_t objectid, uint8_t type,
uint64_t offset);
// Fetch an inode record. Returns 0, -1 if there is no such object, -2 corrupt.
int schfs_inode_get(struct schfs_vol *v, struct schfs_cursor *c,
uint64_t ino, struct schfs_inode *out);
// Resolve one name inside a directory. Returns 0 and fills *out_ino/*out_type,
// -1 if absent, -2 corrupt.
int schfs_dir_lookup(struct schfs_vol *v, struct schfs_cursor *c,
uint64_t dir, const char *name, unsigned namelen,
uint64_t *out_ino, uint16_t *out_type);
// Iterate a directory's real entries in hash order. "." and ".." are not
// stored and are not reported; callers that need them synthesise them.
// A non-zero return from fn stops the walk and is passed back.
int schfs_dir_iterate(struct schfs_vol *v, struct schfs_cursor *c, uint64_t dir,
int (*fn)(uint64_t ino, uint16_t type, const char *name,
unsigned namelen, void *arg),
void *arg);
// Read from a file. Short reads at end-of-file are normal; returns the byte
// count, or -1 on a missing object, -2 on corruption or a failed data checksum.
// `dblk` is a caller-owned SCHFS_BLKSIZE scratch buffer for data blocks.
long schfs_file_read(struct schfs_vol *v, struct schfs_cursor *c, uint64_t ino,
uint64_t off, void *buf, unsigned n, void *dblk);
// Walk an absolute path. Returns the object id, or 0 if any component is
// missing or is not a directory.
uint64_t schfs_path_lookup(struct schfs_vol *v, struct schfs_cursor *c,
const char *path);
// ---------------------------------------------------------------------------
// objw.c — write side
//
// Shared error codes throughout: 0 success, -1 absent or not permitted,
// -2 corruption or I/O failure, -3 out of space, -4 a directory hash-collision
// list that will not fit in one item, -5 directory not empty.
//
// None of it is visible to a reader until schfs_vol_commit publishes the tree,
// which is what lets a failure part way through a multi-record operation be
// abandoned rather than unwound.
// ---------------------------------------------------------------------------
uint64_t schfs_alloc_objectid(struct schfs_vol *v);
int schfs_inode_put(struct schfs_vol *v, uint64_t ino,
const struct schfs_inode *in);
// Add one entry, appending to the packed list if another name already hashes
// the same. -1 if the name is already present.
int schfs_dir_add(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen, uint64_t child, uint16_t type);
// Remove one entry. -1 if there is no such name.
int schfs_dir_del(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen);
// 1 if the directory holds no entries, 0 if it holds some, -2 on corruption.
int schfs_dir_empty(struct schfs_vol *v, uint64_t dir);
// Claim a contiguous run of data blocks for a file whose final size is already
// known, and record the whole run as ONE extent. Worth calling before a bulk
// write: without it, data and tree blocks interleave, and only the merging in
// schfs_file_write keeps the extent count down.
int schfs_file_reserve(struct schfs_vol *v, uint64_t ino, uint64_t nblocks);
// Write at an arbitrary offset, allocating blocks, extent records and data
// checksums, copying any block that a committed tree can still reach, and
// splitting the extent record around it. Writing past the end extends the file;
// a gap left behind stays a hole and reads back as zeros. Returns the byte
// count written, or a negative error.
long schfs_file_write(struct schfs_vol *v, uint64_t ino, uint64_t off,
const void *buf, unsigned n, void *dblk);
// Append at the current end of file. A convenience over schfs_file_write, kept
// because image builders write nothing else.
int schfs_file_append(struct schfs_vol *v, uint64_t ino, const void *buf,
unsigned n, void *dblk);
// Set a file's length, releasing whole blocks past the new end and zeroing what
// is left of the block the new end falls inside — truncating down and growing
// again has to read back zeros. Growing alone costs nothing: a range with no
// extent records is a hole. `dblk` is a caller-owned SCHFS_BLKSIZE buffer.
int schfs_file_truncate(struct schfs_vol *v, uint64_t ino, uint64_t size,
void *dblk);
// Release an object's data, checksums and inode record. The caller must already
// have removed every name pointing at it.
int schfs_object_free(struct schfs_vol *v, uint64_t ino);
// Create an object and the entry naming it. `now` is the timestamp to stamp all
// three times with — SchFS has no clock of its own.
int schfs_create(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen, uint16_t type, uint32_t mode,
uint16_t rdev_major, uint16_t rdev_minor, uint64_t now,
uint64_t *out_ino);
// A second name for an existing object. -1 for a directory: hard-linking one
// makes cycles possible, and a cycle turns every recursive walk infinite.
int schfs_link(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen, uint64_t ino);
// Remove a name, and the object with it once the last name is gone. Handles
// directories too, refusing with -5 if the directory still has entries.
int schfs_unlink(struct schfs_vol *v, uint64_t dir, const char *name,
unsigned namelen);
// Move a name, replacing the target if one exists. Refuses to move a directory
// into its own subtree, which would detach that subtree from the root.
int schfs_rename(struct schfs_vol *v, uint64_t olddir, const char *oldname,
unsigned oldlen, uint64_t newdir, const char *newname,
unsigned newlen);
#endif /* SCHFS_INT_H */

314
kernel/fs/schfs/space.c Normal file
View file

@ -0,0 +1,314 @@
// SPDX-License-Identifier: Apache-2.0
//
// SchFS space manager: which blocks are in use.
//
// There is no free-space map on disk, and that is the interesting decision
// here. The committed tree already records exactly which blocks are live —
// every tree node is named by its parent's pointer, every data block by an
// extent record — so the map is *derived* at mount by walking the tree once,
// held in RAM, and never written back.
//
// The alternative, persisting it, buys a fast mount and costs the one problem
// copy-on-write is otherwise free of: the map has to be updated atomically with
// the tree it describes, and writing the map needs blocks, which changes the
// map. ZFS spends real complexity on that recursion and btrfs resolves it with
// a fixpoint loop. Deriving the map instead cannot disagree with the filesystem
// no matter what a crash interrupts, because it is computed from it — there is
// no second source of truth to get out of step.
//
// What it costs is a metadata scan at mount, proportional to the number of tree
// blocks rather than to the size of the volume. Nothing is written, so it is
// also safe on a read-only mount. If mount time ever becomes the thing that
// hurts, the map can be cached on disk later purely as a hint, validated
// against the uberblock's generation and discarded when it does not match — an
// addition, not a format change.
//
// ---------------------------------------------------------------------------
// Deferred frees
//
// A block released during the transaction group being built is still reachable
// from the last *committed* root, which is where a crash lands. Handing it
// straight back would let the filesystem overwrite the very copy it is supposed
// to fall back to. So frees go into a second bitmap and only rejoin the free
// map when the commit that supersedes them lands. One transaction of delay, no
// more: once group N is published, group N-1's tree is no longer anybody's
// fallback.
#include "schfs_int.h"
#ifdef SCHFS_HOSTED
#include <string.h>
#else
#include "types.h"
#include "defs.h"
#endif
/* ------------------------------------------------------------------ */
/* Bit vectors, paged */
/* */
/* The bitmap has to be one flat array to be indexable, and the kernel */
/* hands out memory a page at a time with no promise that two pages */
/* are adjacent. So it is a vector of page pointers instead: one page */
/* covers SCHFS_SPACE_BITS_PER_PAGE blocks, and the page vector itself */
/* is a fixed-size array, which is what puts a hard ceiling on volume */
/* size. See docs/architecture/schfs.md for the resulting limits. */
/* ------------------------------------------------------------------ */
static int
bit_test(uint64_t *const *pages, uint64_t b)
{
uint64_t pg = b / SCHFS_SPACE_BITS_PER_PAGE;
uint64_t off = b % SCHFS_SPACE_BITS_PER_PAGE;
return (int)((pages[pg][off / 64] >> (off % 64)) & 1);
}
static void
bit_set(uint64_t *const *pages, uint64_t b)
{
uint64_t pg = b / SCHFS_SPACE_BITS_PER_PAGE;
uint64_t off = b % SCHFS_SPACE_BITS_PER_PAGE;
pages[pg][off / 64] |= 1ULL << (off % 64);
}
// The whole 64-block word containing b, so a scan can skip a fully used region
// sixty-four blocks at a time instead of one. On a nearly full volume that is
// the difference between an allocation costing microseconds and milliseconds.
static uint64_t
bit_word(uint64_t *const *pages, uint64_t b)
{
uint64_t pg = b / SCHFS_SPACE_BITS_PER_PAGE;
uint64_t off = b % SCHFS_SPACE_BITS_PER_PAGE;
return pages[pg][off / 64];
}
static int
popcount64(uint64_t v)
{
int n = 0;
while (v) { v &= v - 1; n++; }
return n;
}
/* ------------------------------------------------------------------ */
/* Setup */
/* ------------------------------------------------------------------ */
int
schfs_space_init(struct schfs_space *s, uint64_t nblocks,
void *(*palloc)(void))
{
memset(s, 0, sizeof(*s));
if (nblocks <= SCHFS_FIRST_BLOCK)
return -3;
if (nblocks > SCHFS_MAX_BLOCKS)
return -4; /* larger than this build can map */
uint64_t npages = (nblocks + SCHFS_SPACE_BITS_PER_PAGE - 1)
/ SCHFS_SPACE_BITS_PER_PAGE;
for (uint64_t i = 0; i < npages; i++) {
void *p = palloc();
if (p == 0)
return -5; /* caller frees what was taken */
memset(p, 0, SCHFS_BLKSIZE);
s->inuse[i] = (uint64_t *)p;
p = palloc();
if (p == 0)
return -5;
memset(p, 0, SCHFS_BLKSIZE);
s->pend[i] = (uint64_t *)p;
s->npages = (int)(i + 1);
}
s->nblocks = nblocks;
s->free_blocks = nblocks - SCHFS_FIRST_BLOCK;
s->hint = SCHFS_FIRST_BLOCK;
// The boot block and the uberblock ring are never allocatable, and neither
// is anything past the end of the volume: the last bitmap page covers more
// blocks than exist, and a stray allocation out there would be written to a
// sector the device does not have.
for (uint64_t b = 0; b < SCHFS_FIRST_BLOCK; b++)
bit_set(s->inuse, b);
for (uint64_t b = nblocks; b < (uint64_t)npages * SCHFS_SPACE_BITS_PER_PAGE; b++)
bit_set(s->inuse, b);
return 0;
}
void
schfs_space_mark(struct schfs_space *s, uint64_t blk, uint64_t n)
{
for (uint64_t i = 0; i < n; i++) {
uint64_t b = blk + i;
if (b < SCHFS_FIRST_BLOCK || b >= s->nblocks)
continue;
if (bit_test(s->inuse, b))
continue; /* already counted; extents may be shared */
bit_set(s->inuse, b);
s->free_blocks--;
}
}
/* ------------------------------------------------------------------ */
/* Allocation */
/* ------------------------------------------------------------------ */
uint64_t
schfs_space_alloc_run(struct schfs_space *s, uint64_t n)
{
if (n == 0 || n > s->free_blocks)
return 0;
// Two sweeps: from the rotating hint to the end of the volume, then from
// the start back to it. Starting every search at block 0 would make the
// front of the volume the only place anything is ever written and turn
// allocation into a linear scan of everything already used.
for (int pass = 0; pass < 2; pass++) {
uint64_t lo = pass == 0 ? s->hint : SCHFS_FIRST_BLOCK;
uint64_t hi = pass == 0 ? s->nblocks : s->hint;
uint64_t run = 0, first = 0;
for (uint64_t b = lo; b < hi; ) {
if (b % 64 == 0 && bit_word(s->inuse, b) == ~0ULL) {
run = 0;
b += 64;
continue;
}
if (bit_test(s->inuse, b)) {
run = 0;
b++;
continue;
}
if (run == 0)
first = b;
run++;
b++;
if (run == n) {
for (uint64_t i = 0; i < n; i++)
bit_set(s->inuse, first + i);
s->free_blocks -= n;
s->hint = first + n < s->nblocks ? first + n : SCHFS_FIRST_BLOCK;
return first;
}
}
}
return 0;
}
uint64_t
schfs_space_alloc(struct schfs_space *s)
{
return schfs_space_alloc_run(s, 1);
}
void
schfs_space_free(struct schfs_space *s, uint64_t blk)
{
if (blk < SCHFS_FIRST_BLOCK || blk >= s->nblocks)
return;
if (!bit_test(s->inuse, blk))
return; /* never allocated, or freed twice */
if (bit_test(s->pend, blk))
return; /* already queued this group */
bit_set(s->pend, blk);
s->pending++;
}
void
schfs_space_commit(struct schfs_space *s)
{
uint64_t words = SCHFS_SPACE_BITS_PER_PAGE / 64;
for (int p = 0; p < s->npages; p++) {
uint64_t *pend = s->pend[p];
uint64_t *inuse = s->inuse[p];
for (uint64_t w = 0; w < words; w++) {
if (pend[w] == 0)
continue;
s->free_blocks += popcount64(pend[w] & inuse[w]);
inuse[w] &= ~pend[w];
pend[w] = 0;
}
}
s->pending = 0;
}
/* ------------------------------------------------------------------ */
/* Deriving the map from the committed tree */
/* ------------------------------------------------------------------ */
// Every extent record in a leaf names a run of data blocks. A range shared by
// several files (a reflink, a snapshot) appears more than once, which is why
// schfs_space_mark is idempotent rather than counting.
static void
mark_leaf_extents(struct schfs_space *s, const void *blk)
{
const struct schfs_item *it = SCHFS_ITEMS(blk);
int n = SCHFS_HDR(blk)->nritems;
for (int i = 0; i < n; i++) {
if (it[i].key.type != SCHFS_KEY_EXTENT)
continue;
if (it[i].size != sizeof(struct schfs_extent))
continue;
if ((int)it[i].offset + (int)it[i].size > SCHFS_BLKSIZE)
continue;
const struct schfs_extent *e =
(const struct schfs_extent *)((const char *)blk + it[i].offset);
schfs_space_mark(s, e->disk_block, e->nblocks);
}
}
// Walk the whole tree, marking every block it reaches.
//
// Iterative, with one block buffer and a path of block numbers, for the same
// reason everything else here is: a recursive walk keeps a block per level and
// the kernel stack is one block in total. Climbing re-reads the parent, so an
// internal node is read once per child — mount-time cost that the buffer cache
// largely absorbs, in exchange for bounded memory.
int
schfs_space_scan(struct schfs_vol *v)
{
struct schfs_space *s = &v->space;
void *blk = v->ctx.a;
struct {
uint64_t nr;
uint32_t crc;
int idx; /* next child to descend into */
} path[SCHFS_MAX_LEVEL + 1];
int d = 0;
path[0].nr = v->tree.root;
path[0].crc = v->tree.root_crc;
path[0].idx = 0;
schfs_space_mark(s, v->tree.root, 1);
for (;;) {
if (schfs_node_read(&v->dev, path[d].nr, path[d].crc, blk) != 0)
return -2;
if (SCHFS_HDR(blk)->level == 0) {
mark_leaf_extents(s, blk);
d--;
} else if (path[d].idx < SCHFS_HDR(blk)->nritems) {
struct schfs_kptr *kp = &SCHFS_KPTRS(blk)[path[d].idx];
path[d].idx++;
if (d + 1 > SCHFS_MAX_LEVEL)
return -2;
d++;
path[d].nr = kp->blocknr;
path[d].crc = kp->crc;
path[d].idx = 0;
schfs_space_mark(s, path[d].nr, 1);
continue;
} else {
d--; /* every child visited */
}
if (d < 0)
return 0;
}
}

414
kernel/fs/schfs/vfs.c Normal file
View file

@ -0,0 +1,414 @@
// 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,
};

299
kernel/fs/schfs/vol.c Normal file
View file

@ -0,0 +1,299 @@
// SPDX-License-Identifier: Apache-2.0
//
// SchFS volume layer: the uberblock ring.
//
// A ring of SCHFS_UBER_COUNT slots is written round-robin, each stamped with a
// transaction number and a checksum. Mounting reads every slot and adopts the
// highest-numbered one that validates.
//
// That single rule replaces journalling entirely. A commit writes one block; it
// either lands whole or it does not. If it does not, its checksum fails, the
// previous slot is still the highest valid one, and the volume comes up at the
// previous transaction with no replay and no fsck. Rotating through many slots
// rather than overwriting one means a run of bad sectors under the current slot
// cannot take the volume with it.
#include "schfs_int.h"
#ifdef SCHFS_HOSTED
#include <string.h>
#else
#include "types.h"
#include "defs.h"
#endif
// Checksum an uberblock with its own crc field zeroed, the same convention the
// tree nodes use.
static uint32_t
uber_crc(struct schfs_uberblock *u)
{
uint32_t saved = u->crc;
u->crc = 0;
uint32_t c = schfs_crc32c(0, u, sizeof(*u));
u->crc = saved;
return c;
}
static int
uber_valid(struct schfs_uberblock *u)
{
if (u->magic != SCHFS_MAGIC)
return 0;
if (u->version != SCHFS_VERSION)
return 0;
// A volume made with a different block size would otherwise be read with
// every offset wrong, which looks exactly like corruption.
if (u->blocksize != SCHFS_BLKSIZE)
return 0;
if (uber_crc(u) != u->crc)
return 0;
// Self-consistency: a root inside the ring or past the end of the device is
// a corrupt uberblock that happens to checksum, not a usable one.
if (u->root < SCHFS_FIRST_BLOCK || u->root >= u->total_blocks)
return 0;
return 1;
}
static void
vol_from_uber(struct schfs_vol *v, struct schfs_uberblock *u, int slot)
{
v->tree.dev = &v->dev;
v->tree.root = u->root;
v->tree.root_crc = u->root_crc;
v->tree.root_gen = u->root_generation;
// The tree's `generation` is the transaction group being *built*, one past
// the newest committed one. Setting it to the committed root's generation
// instead would tell the tree that every block of the mounted filesystem
// belongs to the open transaction, and it would then rewrite the live tree
// in place — losing the very thing copy-on-write exists to preserve.
v->tree.generation = u->generation + 1;
v->total_blocks = u->total_blocks;
v->free_blocks = u->free_blocks;
v->next_objectid = u->next_objectid;
v->root_objectid = u->root_objectid;
v->generation = u->generation;
v->uber_slot = slot;
memmove(v->uuid, u->uuid, sizeof(v->uuid));
memmove(v->label, u->label, sizeof(v->label));
v->label[SCHFS_LABEL_MAX - 1] = '\0';
}
int
schfs_vol_open(struct schfs_vol *v, struct schfs_bdev *dev,
struct schfs_ctx *x, void *(*palloc)(void))
{
struct schfs_uberblock *u = (struct schfs_uberblock *)x->a;
int best_slot = -1;
uint64_t best_gen = 0;
int io_errors = 0;
v->dev = *dev;
v->ctx = *x;
v->tree.dev = &v->dev;
// Remember which slot won, not a copy of it.
//
// An uberblock is a whole SCHFS_BLKSIZE block, and keeping one as a local
// put 4096 bytes on a kernel stack that is 4096 bytes in total — the canary
// in kalloc'd stacks caught it as "kstack overflow" on the very first boot.
// Re-reading the winner costs one block read, once, at mount.
for (int i = 0; i < SCHFS_UBER_COUNT; i++) {
if (v->dev.bread(v->dev.ctx, SCHFS_UBER_START + i, u) != 0) {
io_errors++;
continue; // an unreadable slot is not fatal: try the rest
}
if (!uber_valid(u))
continue;
if (best_slot < 0 || u->generation > best_gen) {
best_gen = u->generation;
best_slot = i;
}
}
if (best_slot < 0)
return io_errors == SCHFS_UBER_COUNT ? -2 : -1;
if (v->dev.bread(v->dev.ctx, SCHFS_UBER_START + best_slot, u) != 0)
return -2;
if (!uber_valid(u))
return -2; /* changed under us, or a flaky device */
vol_from_uber(v, u, best_slot);
v->readonly = 1; // until an allocation map exists, below
// The newest uberblock passing its own checksum does not prove the tree it
// names is readable — a misdirected write could have landed on the root.
// Check now, while there is still the option of falling back, rather than
// discovering it on the first lookup.
if (v->dev.bread(v->dev.ctx, v->tree.root, x->a) != 0)
return -2;
{
struct schfs_node_hdr *h = (struct schfs_node_hdr *)x->a;
if (h->magic != SCHFS_NODE_MAGIC || h->blocknr != v->tree.root)
return -2;
}
// Rebuild the allocation map from the tree just adopted. Without a palloc
// the volume stays read-only, which is exactly right for a tool that only
// inspects an image: it can then never allocate, rather than allocating
// from a map that says everything is free.
if (palloc) {
int sr = schfs_space_init(&v->space, v->total_blocks, palloc);
if (sr != 0)
return sr;
if (schfs_space_scan(v) != 0)
return -2;
v->free_blocks = v->space.free_blocks;
v->readonly = 0;
}
return 0;
}
int
schfs_vol_commit(struct schfs_vol *v)
{
struct schfs_uberblock *u = (struct schfs_uberblock *)v->ctx.a;
v->generation++;
v->uber_slot = (v->uber_slot + 1) % SCHFS_UBER_COUNT;
memset(u, 0, sizeof(*u));
u->magic = SCHFS_MAGIC;
u->version = SCHFS_VERSION;
u->blocksize = SCHFS_BLKSIZE;
u->generation = v->generation;
u->root = v->tree.root;
u->root_crc = v->tree.root_crc;
u->root_generation = v->tree.root_gen;
u->total_blocks = v->total_blocks;
// Blocks this group released count as free *as of this uberblock*: nothing
// the root below reaches touches them. They are only still marked in use in
// memory because the group being superseded is what a crash would land on
// until this very block is written.
u->free_blocks = v->space.nblocks
? v->space.free_blocks + v->space.pending
: v->free_blocks;
u->next_objectid = v->next_objectid;
u->root_objectid = v->root_objectid;
memmove(u->uuid, v->uuid, sizeof(u->uuid));
memmove(u->label, v->label, sizeof(u->label));
u->crc = uber_crc(u);
if (v->dev.bwrite(v->dev.ctx, SCHFS_UBER_START + v->uber_slot, u) != 0)
return -2;
// Order matters here, and it is the whole crash-safety argument in three
// lines. Only now that the new uberblock is on the disk is the group this
// freed blocks in superseded, so only now may those blocks be handed out
// again; releasing them before the write would let a crash come back to a
// tree whose blocks had already been reused.
if (v->space.nblocks) {
schfs_space_commit(&v->space);
v->free_blocks = v->space.free_blocks;
}
// Everything written from here on belongs to the next transaction group, so
// the blocks just published become read-only to the tree again and the
// copy-on-write rule resumes. Advancing this is the whole reason a commit
// is a barrier rather than just another block write.
v->tree.generation = v->generation + 1;
return 0;
}
int
schfs_vol_format(struct schfs_vol *v, struct schfs_bdev *dev,
uint64_t total_blocks, const char *label,
struct schfs_ctx *x, void *(*palloc)(void))
{
if (total_blocks <= SCHFS_FIRST_BLOCK)
return -3;
memset(v, 0, sizeof(*v));
v->dev = *dev;
v->ctx = *x;
v->tree.dev = &v->dev;
v->total_blocks = total_blocks;
v->root_objectid = SCHFS_ROOT_OBJECTID;
// Nothing is allocated on a fresh volume beyond the boot block and the
// ring, so the map starts empty rather than being scanned into existence.
{
int sr = schfs_space_init(&v->space, total_blocks, palloc);
if (sr != 0)
return sr;
}
v->free_blocks = v->space.free_blocks;
// The root's object id is consumed by the root itself, so allocation must
// start *past* it. Starting at SCHFS_FIRST_OBJECTID instead handed the
// first directory created the same id as the root: that directory then
// silently became the root, so it contained itself and any tree walk
// recursed until the stack ran out.
v->next_objectid = SCHFS_ROOT_OBJECTID + 1;
v->generation = 0; /* nothing committed yet */
v->tree.generation = 1; /* ...so this is the group being built */
v->uber_slot = -1;
v->readonly = 0;
if (label) {
unsigned i = 0;
for (; i < SCHFS_LABEL_MAX - 1 && label[i]; i++)
v->label[i] = label[i];
v->label[i] = '\0';
}
// A UUID from the block count and a fixed salt. Not random — mkfs is
// deliberately reproducible, so two runs over the same tree produce
// byte-identical images and a diff means a real change.
{
uint32_t a = schfs_crc32c(0, &total_blocks, sizeof(total_blocks));
uint32_t b = schfs_crc32c(a, v->label, SCHFS_LABEL_MAX);
for (int i = 0; i < 16; i++)
v->uuid[i] = (uint8_t)((i & 1 ? a : b) >> ((i % 4) * 8));
}
// Zero the whole ring so a stale uberblock from a previous filesystem on
// this device cannot outrank ours and be mounted instead.
memset(x->a, 0, SCHFS_BLKSIZE);
for (int i = 0; i < SCHFS_UBER_COUNT; i++)
if (v->dev.bwrite(v->dev.ctx, SCHFS_UBER_START + i, x->a) != 0)
return -2;
if (schfs_btree_create(&v->tree, &v->dev, x) != 0)
return -2;
return schfs_vol_commit(v);
}
/* ------------------------------------------------------------------ */
/* Allocation */
/* */
/* Thin adapters over the space manager, so callers wiring up a */
/* schfs_bdev have something with the right shape to point at. The */
/* readonly check lives here rather than in space.c because "this */
/* volume may not be written" is a property of the mount, not of the */
/* bitmap. */
/* ------------------------------------------------------------------ */
uint64_t
schfs_vol_alloc(struct schfs_vol *v)
{
if (v->readonly)
return 0;
return schfs_space_alloc(&v->space);
}
uint64_t
schfs_vol_alloc_run(struct schfs_vol *v, uint64_t n)
{
if (v->readonly)
return 0;
return schfs_space_alloc_run(&v->space, n);
}
void
schfs_vol_free(struct schfs_vol *v, uint64_t blk)
{
schfs_space_free(&v->space, blk);
}