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:
commit
d0f7acf9eb
275 changed files with 51417 additions and 0 deletions
241
kernel/fs/exec.c
Normal file
241
kernel/fs/exec.c
Normal 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
294
kernel/fs/file.c
Normal 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
1839
kernel/fs/fs.c
Normal file
File diff suppressed because it is too large
Load diff
255
kernel/fs/log.c
Normal file
255
kernel/fs/log.c
Normal 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
166
kernel/fs/pipe.c
Normal 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
524
kernel/fs/procfs.c
Normal 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
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
215
kernel/fs/schfs/btree.h
Normal 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
623
kernel/fs/schfs/kmount.c
Normal 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
441
kernel/fs/schfs/obj.c
Normal 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
932
kernel/fs/schfs/objw.c
Normal 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
277
kernel/fs/schfs/schfs_int.h
Normal 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
314
kernel/fs/schfs/space.c
Normal 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
414
kernel/fs/schfs/vfs.c
Normal 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
299
kernel/fs/schfs/vol.c
Normal 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);
|
||||
}
|
||||
Loading…
Reference in a new issue