Initial commit: Schunix as it stands before the SchOS rename

An x86-64 operating system built from xv6, now largely rewritten: SMP,
a copy-on-write root filesystem (SchFS), PCI/USB/virtio drivers, a TTF
rasteriser, and a Linux-numbered syscall ABI.

No history before this point -- the tree was never under version control.
This commit exists so the rename that follows has something to undo to.
This commit is contained in:
Nexoniarz 2026-08-09 13:47:01 +02:00
commit d0f7acf9eb
275 changed files with 51417 additions and 0 deletions

838
kernel/proc/proc.c Normal file
View file

@ -0,0 +1,838 @@
#include "types.h"
#include "defs.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "x86.h"
#include "traps.h"
#include "proc.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "file.h"
#include "posix.h"
#include "errno.h"
// Verify myproc()->fs_base offset matches the hardcoded 0x688 in trapasm.S.
_Static_assert(__builtin_offsetof(struct proc, fs_base) == 0x18,
"myproc()->fs_base offset changed — update trapasm.S WRITE_USER_FS macro (must be 0x18)");
struct {
struct spinlock lock;
struct proc proc[NPROC];
} ptable;
static struct proc *initproc;
int nextpid = 1;
extern void forkret(void);
extern void syscall_trapret(void);
static void wakeup1(void *chan);
void
kstack_check_canary(const char *where)
{
if(myproc() == 0 || myproc()->kstack == 0)
return;
if(*(uint64*)(myproc()->kstack) != KSTACK_CANARY){
cprintf("kstack canary corrupted for pid %d (%s) at %s: got 0x%p\n",
myproc()->pid, myproc()->name, where, *(uint64*)(myproc()->kstack));
panic("kstack overflow");
}
}
void
pinit(void)
{
initlock(&ptable.lock, "ptable");
}
static struct proc*
allocproc(void)
{
struct proc *p;
char *sp;
acquire(&ptable.lock);
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++)
if(p->state == UNUSED)
goto found;
release(&ptable.lock);
return 0;
found:
p->state = EMBRYO;
p->pid = nextpid++;
// ptable slots get reused once a previous occupant is reaped, but
// nothing previously cleared the signal-delivery fields added for
// SIGCHLD (sig_handler/sig_restorer/pending_signals/signal_mask/
// in_signal/saved_sigtf) — a freshly allocated process would silently
// inherit whatever a totally unrelated earlier process left behind,
// including stale handler addresses. deliver_signals() (syscall.c)
// would then jump to garbage on this new process's very first syscall
// return, faulting at whatever bogus rip happened to be sitting there.
memset(p->sig_handler, 0, sizeof(p->sig_handler));
memset(p->sig_restorer, 0, sizeof(p->sig_restorer));
p->pending_signals = 0;
p->signal_mask = 0;
p->in_signal = 0;
p->sigsusp_oldmask = 0;
p->sigsusp_active = 0;
// Same reasoning as the signal fields above: a recycled slot must not
// inherit a dead process's identity or exit status.
p->xstate = 0;
p->killsig = 0;
p->pgid = 0;
p->sid = 0;
p->uid = p->euid = p->suid = 0;
p->gid = p->egid = p->sgid = 0;
release(&ptable.lock);
if((p->kstack = kalloc()) == 0){
p->state = UNUSED;
return 0;
}
// Canary at the very bottom of the kstack page: if a deep/nested trap
// ever overflows this 4096-byte stack, this is the first thing clobbered.
// Checked on every trap()/syscall() entry (kstack_check_canary()).
*(uint64*)(p->kstack) = KSTACK_CANARY;
sp = p->kstack + KSTACKSIZE;
sp -= sizeof(struct trapframe);
p->tf = (struct trapframe*)sp;
sp -= sizeof(addr_t);
*(addr_t*)sp = (addr_t)syscall_trapret;
sp -= sizeof(struct context);
p->context = (struct context*)sp;
memset(p->context, 0, sizeof *p->context);
p->context->rip = (addr_t)forkret;
p->syscall_map = 0;
p->map_size = 0;
p->mmap_next = 0;
memset(p->ofile_flags, 0, sizeof(p->ofile_flags));
return p;
}
void
sinit(void)
{
struct proc *p;
extern char _binary_initcode_start[], _binary_initcode_size[];
p = allocproc();
initproc = p;
if((p->pgdir = setupkvm()) == 0)
panic("sinit: setupkvm failed");
inituvm(p->pgdir, _binary_initcode_start, (uint64)_binary_initcode_size);
if(allocuvm(p->pgdir, PGSIZE, 32 * PGSIZE) == 0)
panic("sinit: allocuvm failed");
p->sz = 32 * PGSIZE;
memset(p->tf, 0, sizeof(*p->tf));
p->tf->cs = (SEG_UCODE << 3) | DPL_USER;
p->tf->ss = (SEG_UDATA << 3) | DPL_USER;
p->tf->rflags = FL_IF;
p->tf->rsp = p->sz;
p->tf->rip = 0;
// PID 1 leads its own session and process group, and runs as root.
p->pgid = p->pid;
p->sid = p->pid;
p->uid = p->euid = p->suid = 0;
p->gid = p->egid = p->sgid = 0;
safestrcpy(p->name, "sinit", sizeof(p->name));
p->cwd = namei("/");
p->cwd_path[0] = '/'; p->cwd_path[1] = 0;
p->fs_base = 0;
p->state = RUNNABLE;
}
int
growproc(int64 n)
{
addr_t sz;
sz = myproc()->sz;
if(n > 0){
if((sz = allocuvm(myproc()->pgdir, sz, sz + n)) == 0)
return -1;
} else if(n < 0){
if((sz = deallocuvm(myproc()->pgdir, sz, sz + n)) == 0)
return -1;
}
myproc()->sz = sz;
switchuvm(myproc());
return 0;
}
int
fork(void)
{
int i, pid;
struct proc *np;
if((np = allocproc()) == 0)
return -1;
if((np->pgdir = copyuvm(myproc()->pgdir, myproc()->sz)) == 0){
kfree(np->kstack);
np->kstack = 0;
np->state = UNUSED;
return -1;
}
np->sz = myproc()->sz;
np->parent = myproc();
// copyuvm() only duplicates [0, sz) — mmap()'d regions live above that
// and were never copied, leaving the child's vma[] bookkeeping (stale
// from whichever process last occupied this ptable slot) pointing at
// memory that isn't actually mapped in np->pgdir. Reset and rebuild it
// from the parent's real, current mappings.
memset(np->vma, 0, sizeof(np->vma));
for(i = 0; i < NVMA; i++){
if(!myproc()->vma[i].used)
continue;
if(copy_vma_range(np->pgdir, myproc()->pgdir, myproc()->vma[i].addr,
myproc()->vma[i].length, myproc()->vma[i].no_free) < 0){
freevm(np->pgdir);
kfree(np->kstack);
np->kstack = 0;
np->state = UNUSED;
return -1;
}
np->vma[i] = myproc()->vma[i];
if(np->vma[i].shmid >= 0)
shm_ref(np->vma[i].shmid);
}
np->mmap_next = myproc()->mmap_next;
memmove(np->tf, myproc()->tf, sizeof(struct trapframe));
np->syscall_map = myproc()->syscall_map;
np->map_size = myproc()->map_size;
// fork() preserves the parent's signal handlers and mask (POSIX); the
// child starts with no pending signals of its own, though — those
// aren't inherited. allocproc() already zeroed pending_signals/
// in_signal/saved_sigtf for this fresh slot.
memmove(np->sig_handler, myproc()->sig_handler, sizeof(np->sig_handler));
memmove(np->sig_restorer, myproc()->sig_restorer, sizeof(np->sig_restorer));
np->signal_mask = myproc()->signal_mask;
np->tf->rax = 0;
for(i = 0; i < NOFILE; i++)
if(myproc()->ofile[i]) {
np->ofile[i] = filedup(myproc()->ofile[i]);
np->ofile_flags[i] = myproc()->ofile_flags[i];
}
// POSIX: the child inherits process group, session and all four ids.
np->pgid = myproc()->pgid;
np->sid = myproc()->sid;
np->uid = myproc()->uid; np->euid = myproc()->euid; np->suid = myproc()->suid;
np->gid = myproc()->gid; np->egid = myproc()->egid; np->sgid = myproc()->sgid;
np->cwd = idup(myproc()->cwd);
memmove(np->cwd_path, myproc()->cwd_path, sizeof(myproc()->cwd_path));
np->fs_base = myproc()->fs_base;
safestrcpy(np->name, myproc()->name, sizeof(myproc()->name));
pid = np->pid;
__sync_synchronize();
np->state = RUNNABLE;
return pid;
}
// status is already wait(2)-encoded -- see W_EXITCODE() in proc.h. Callers
// that die from a fault or signal pass W_EXITCODE(0, sig); a normal _exit(n)
// passes W_EXITCODE(n, 0).
void
exit(int status)
{
struct proc *p;
int fd;
if(myproc() == initproc)
panic("init exiting");
myproc()->xstate = status;
for(fd = 0; fd < NOFILE; fd++){
if(myproc()->ofile[fd]){
fileclose(myproc()->ofile[fd]);
myproc()->ofile[fd] = 0;
myproc()->ofile_flags[fd] = 0;
}
}
begin_op();
iput(myproc()->cwd);
end_op();
myproc()->cwd = 0;
acquire(&ptable.lock);
// Tell the parent a child exited — needed for mksh's sigsuspend()-based
// job-control wait loop to have any chance of making progress (without
// this, sigsuspend() can never return for the right reason and the
// parent hangs unconditionally). Signal delivery itself is verified
// working (handler invocation + sigreturn round-trip is clean); mksh's
// own job-control bookkeeping has further gaps (see docs/architecture/
// kernel-history.md) that this alone doesn't fully resolve, but
// disabling it guarantees no progress, whereas leaving it enabled at
// least allows the cases that don't hit those gaps to work.
if (myproc()->parent)
myproc()->parent->pending_signals |= (1ULL << SIGCHLD);
wakeup1(myproc()->parent);
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){
if(p->parent == myproc()){
p->parent = initproc;
if(p->state == ZOMBIE)
wakeup1(initproc);
}
}
myproc()->state = ZOMBIE;
sched();
panic("zombie exit");
}
int
wait(void)
{
return waitpid(-1, 0, 0);
}
// waitpid(2)/wait4(2) proper: a specific pid (-1 = any child, matching
// wait()'s old behavior), an optional status-out pointer (NULL is valid —
// POSIX callers commonly do waitpid(pid, NULL, WNOHANG)), and WNOHANG.
//
// This used to be missing: sys_waitpid()/sys_wait4() both ignored pid and
// options and called the any-child, always-blocking wait() above. A NULL
// status pointer additionally got rejected by argptr() (page 0 is
// unmapped) before ever reaching here. Together, a caller doing
// waitpid(child, NULL, WNOHANG) right after a child exits — exactly what
// mksh's job control does — always failed instead of immediately reaping
// the already-zombie child. mksh then fell back to sigsuspend(), which
// this kernel can't wake for a real SIGCHLD (no signal delivery), so it
// silently retried forever: a real, low-CPU hang with no error printed.
int
waitpid(int pid, int *status, int options)
{
struct proc *p;
int havekids;
acquire(&ptable.lock);
for(;;){
havekids = 0;
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){
if(p->parent != myproc())
continue;
// POSIX pid selection: >0 one child, 0 = caller's group,
// -1 = any child, < -1 = the process group with id -pid.
if(pid > 0 && p->pid != pid)
continue;
if(pid == 0 && p->pgid != myproc()->pgid)
continue;
if(pid < -1 && p->pgid != -pid)
continue;
havekids = 1;
if(p->state == ZOMBIE){
int rpid = p->pid;
int rstatus = p->xstate; // copy out before the slot is recycled
kfree(p->kstack);
p->kstack = 0;
// Give back anything shared before freevm() decides it owns the
// pages. See proc_release_shared() in kernel/sys/sys_mem.c.
proc_release_shared(p);
freevm(p->pgdir);
p->pid = 0;
p->parent = 0;
p->name[0] = 0;
p->killed = 0;
p->xstate = 0;
p->killsig = 0;
p->pgid = 0;
p->sid = 0;
p->state = UNUSED;
release(&ptable.lock);
if (status) *status = rstatus;
return rpid;
}
}
if(!havekids || myproc()->killed){
release(&ptable.lock);
return -1;
}
if (options & 1) { // WNOHANG
release(&ptable.lock);
return 0;
}
sleep(myproc(), &ptable.lock);
}
}
void
scheduler(void)
{
struct proc *p;
int skipped = 0;
for(;;){
sti();
acquire(&ptable.lock);
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){
if(p->state != RUNNABLE) {
skipped++;
continue;
}
skipped = 0;
proc_set(p);
switchuvm(p);
p->state = RUNNING;
swtch(&mycpu()->scheduler, p->context);
switchkvm();
proc_set(0);
}
release(&ptable.lock);
if (skipped > NPROC) {
hlt();
skipped = 0;
}
}
}
void
sched(void)
{
int intena;
if(!holding(&ptable.lock))
panic("sched ptable.lock");
if(mycpu()->ncli != 1)
panic("sched locks");
if(myproc()->state == RUNNING)
panic("sched running");
if(readeflags()&FL_IF)
panic("sched interruptible");
intena = mycpu()->intena;
swtch(&myproc()->context, mycpu()->scheduler);
mycpu()->intena = intena;
}
void
yield(void)
{
acquire(&ptable.lock);
myproc()->state = RUNNABLE;
sched();
release(&ptable.lock);
}
void
forkret(void) {
static int first = 1;
release(&ptable.lock);
if (first) {
first = 0;
iinit();
mountinit();
// SchFS is the root filesystem.
//
// Mounted here rather than in main() because mounting reads blocks,
// bread() sleeps, and sleeping needs a process context.
//
// schfs_kmount() reports failure by leaving the volume unmounted rather
// than by returning -- it is a probe elsewhere, where "not SchFS" is an
// ordinary answer. Here it is fatal, so ask afterwards.
schfs_kmount(ROOTDEV);
if (!schfs_kmounted())
panic("schfs root: no SchFS volume on the root device");
// Re-point pid 1's cwd at the real root, now that there is one.
//
// sinit() sets cwd = namei("/") back in main(), which is before this runs.
// The old v1 root survived that because iget(ROOTDEV, ROOTINO) is two
// compile-time constants and touches no disk -- the inode is filled in
// lazily at the first ilock(). SchFS has no constant to offer: the root
// object id comes off the uberblock, so namei("/") that early returns an
// inode built from whatever schfs_kroot() reads out of an unmounted
// volume.
//
// Nothing complains, because absolute paths never consult cwd -- exec
// "/bin/init" works fine. It is the first *relative* lookup that fails,
// and in init that is open("console"): no console means no fd 0/1/2, so
// every shell it spawns exits immediately and init respawns it forever.
// The symptom is a silent boot with no init: ready, which reads like a
// hang and is not one.
{
struct inode *stale = myproc()->cwd;
myproc()->cwd = iget(SCHFS_DEV, schfs_kroot());
if (stale)
iput(stale);
}
}
}
void
sleep(void *chan, struct spinlock *lk)
{
if(myproc() == 0)
panic("sleep");
if(lk == 0)
panic("sleep without lk");
if(lk != &ptable.lock){
acquire(&ptable.lock);
release(lk);
}
myproc()->chan = chan;
myproc()->state = SLEEPING;
sched();
myproc()->chan = 0;
if(lk != &ptable.lock){
release(&ptable.lock);
acquire(lk);
}
}
static void
wakeup1(void *chan)
{
struct proc *p;
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++)
if(p->state == SLEEPING && p->chan == chan)
p->state = RUNNABLE;
}
void
wakeup(void *chan)
{
acquire(&ptable.lock);
wakeup1(chan);
release(&ptable.lock);
}
// Raise a signal on a process: set the pending bit and wake it if it's
// sleeping so blocking syscalls (sigsuspend, pause, read) re-check their
// conditions promptly. Signals with no handler installed fall back to the
// default action — termination for the fatal set, dropped otherwise.
// mksh depends on kill(getpid(), SIGCHLD) genuinely re-raising SIGCHLD
// (its held_sigchld recovery in j_startjob); the previous sys_kill
// silently discarded every non-fatal signal.
int
sigsend(int pid, int sig)
{
struct proc *p;
if(sig <= 0 || sig >= 32)
return -1;
acquire(&ptable.lock);
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){
if(p->pid == pid && p->state != UNUSED && p->state != ZOMBIE){
p->pending_signals |= 1ULL << sig;
if(p->sig_handler[sig] == 0 &&
(sig == SIGKILL || sig == SIGTERM || sig == SIGABRT ||
sig == SIGSEGV || sig == SIGFPE || sig == SIGBUS ||
sig == SIGILL || sig == SIGINT || sig == SIGQUIT ||
sig == SIGHUP || sig == SIGPIPE || sig == SIGUSR1 ||
sig == SIGUSR2 || sig == SIGALRM))
p->killed = 1;
if(sig == SIGKILL)
p->killed = 1;
if(p->killed && p->killsig == 0)
p->killsig = sig; // so exit() can report the real cause
if(p->state == SLEEPING)
p->state = RUNNABLE;
release(&ptable.lock);
return 0;
}
}
release(&ptable.lock);
return -1;
}
// Deliver a signal to every member of a process group. This is what job
// control actually needs: a shell sends SIGINT/SIGTSTP/SIGHUP to a *group*,
// never to individual pids. sys_kill() with pid <= 0 routes here.
//
// Returns 0 if at least one process was signalled, -1 if the group is empty.
int
sigsend_pgrp(int pgid, int sig)
{
struct proc *p;
int found = 0;
if(sig < 0 || sig >= 32)
return -1;
acquire(&ptable.lock);
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){
if(p->state == UNUSED || p->state == ZOMBIE)
continue;
if(p->pgid != pgid)
continue;
found = 1;
if(sig == 0)
continue; // signal 0 is an existence probe only
p->pending_signals |= 1ULL << sig;
if(p->sig_handler[sig] == 0 &&
(sig == SIGKILL || sig == SIGTERM || sig == SIGABRT ||
sig == SIGSEGV || sig == SIGFPE || sig == SIGBUS ||
sig == SIGILL || sig == SIGINT || sig == SIGQUIT ||
sig == SIGHUP || sig == SIGPIPE || sig == SIGUSR1 ||
sig == SIGUSR2 || sig == SIGALRM)){
p->killed = 1;
if(p->killsig == 0)
p->killsig = sig;
}
if(sig == SIGKILL){
p->killed = 1;
if(p->killsig == 0)
p->killsig = sig;
}
if(p->state == SLEEPING)
p->state = RUNNABLE;
}
release(&ptable.lock);
return found ? 0 : -1;
}
// Look up a process's process-group id. Returns -1 if no such live process.
int
proc_pgid(int pid)
{
struct proc *p;
int r = -1;
acquire(&ptable.lock);
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){
if(p->pid == pid && p->state != UNUSED && p->state != ZOMBIE){
r = p->pgid;
break;
}
}
release(&ptable.lock);
return r;
}
// setpgid(pid, pgid) with POSIX's argument shorthands: pid 0 means the caller,
// pgid 0 means "use pid as the group id" (i.e. become a group leader).
int
proc_setpgid(int pid, int pgid)
{
struct proc *p;
int r = -ESRCH;
if(pid < 0 || pgid < 0)
return -EINVAL;
if(pid == 0)
pid = myproc()->pid;
if(pgid == 0)
pgid = pid;
acquire(&ptable.lock);
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){
if(p->pid != pid || p->state == UNUSED || p->state == ZOMBIE)
continue;
// A caller may only move itself or one of its own children, and never
// across a session boundary.
if(p != myproc() && p->parent != myproc())
r = -ESRCH;
else if(p->sid != myproc()->sid)
r = -EPERM;
else {
p->pgid = pgid;
r = 0;
}
break;
}
release(&ptable.lock);
return r;
}
// setsid(): become leader of a brand-new session and process group. Fails if
// the caller already leads a process group, since that group would otherwise
// end up split across two sessions.
int
proc_setsid(void)
{
struct proc *p;
int r;
acquire(&ptable.lock);
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){
if(p->state != UNUSED && p->state != ZOMBIE && p->pgid == myproc()->pid){
release(&ptable.lock);
return -EPERM;
}
}
myproc()->sid = myproc()->pid;
myproc()->pgid = myproc()->pid;
r = myproc()->sid;
release(&ptable.lock);
return r;
}
int
kill(int pid)
{
struct proc *p;
acquire(&ptable.lock);
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){
if(p->pid == pid){
p->killed = 1;
if(p->state == SLEEPING)
p->state = RUNNABLE;
release(&ptable.lock);
return 0;
}
}
release(&ptable.lock);
return -1;
}
void
procdump(void)
{
static char *states[] = {
[UNUSED] "unused",
[EMBRYO] "embryo",
[SLEEPING] "sleep ",
[RUNNABLE] "runble",
[RUNNING] "run ",
[ZOMBIE] "zombie"
};
int i;
struct proc *p;
char *state;
addr_t pc[10];
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){
if(p->state == UNUSED)
continue;
if(p->state >= 0 && p->state < NELEM(states) && states[p->state])
state = states[p->state];
else
state = "???";
cprintf("%d %s %s", p->pid, state, p->name);
if(p->state == SLEEPING){
getstackpcs((addr_t*)p->context->rbp, pc);
for(i=0; i<10 && pc[i] != 0; i++)
cprintf(" %p", pc[i]);
}
cprintf("\n");
}
}
// ---------------------------------------------------------------------------
// Snapshots, for /proc
//
// procfs must not walk ptable itself: it would need the lock, the struct
// layout, and a rule about what happens when a process exits between the
// readdir that listed it and the open that reads it. These two copy what is
// needed out from under the lock and hand back a value, so a process that dies
// mid-listing produces a stale line rather than a use-after-free.
// ---------------------------------------------------------------------------
// Fill `out` with the `idx`-th live process (0-based, in table order).
// Returns 0 on success, -1 when idx is past the end.
int
proc_snapshot(int idx, struct procinfo *out)
{
struct proc *p;
int n = 0;
acquire(&ptable.lock);
for (p = ptable.proc; p < &ptable.proc[NPROC]; p++) {
if (p->state == UNUSED)
continue;
if (n++ != idx)
continue;
out->pid = p->pid;
out->ppid = p->parent ? p->parent->pid : 0;
out->pgid = p->pgid;
out->sid = p->sid;
out->uid = p->uid;
out->state = p->state;
out->sz = p->sz;
out->killed = p->killed;
safestrcpy(out->name, p->name, sizeof(out->name));
safestrcpy(out->exe, p->exe, sizeof(out->exe));
release(&ptable.lock);
return 0;
}
release(&ptable.lock);
return -1;
}
// The same, by pid rather than index.
int
proc_snapshot_pid(int pid, struct procinfo *out)
{
int i;
struct procinfo pi;
for (i = 0; proc_snapshot(i, &pi) == 0; i++)
if (pi.pid == pid) { *out = pi; return 0; }
return -1;
}
int proc_num(void)
{
struct proc *p;
int num = 0;
acquire(&ptable.lock);
for (p = ptable.proc; p < &ptable.proc[NPROC]; p++)
if (p->state != UNUSED) num++;
release(&ptable.lock);
return num;
}
int proc_exists(int pid)
{
struct proc *p;
acquire(&ptable.lock);
for (p = ptable.proc; p < &ptable.proc[NPROC]; p++) {
if (p->pid == pid && p->state != UNUSED && p->state != ZOMBIE) {
release(&ptable.lock);
return 1;
}
}
release(&ptable.lock);
return 0;
}