SchOS/kernel/proc/proc.c
Nexoniarz 0e39e711f1 fpu: save each process's x87/SSE registers; keep the kernel off them
Nothing ever saved the FPU and SSE registers on a context switch.
musl's memcpy, the compiler's struct copies and any arithmetic on
doubles keep live values in %xmm, so two processes sharing a CPU wrote
over each other's: two shells counting in a loop on one processor took
a NULL+8 page fault within seconds, at a different rip every time. It
reproduces on kernels from before this week too -- it had always been
there, and every crash nobody could explain was a candidate.

sched() now fxsaves the process leaving the CPU and scheduler()
fxrstors the one arriving. A new process starts from the ABI's state
(FCW 0x37F, MXCSR 0x1F80), fork copies the parent's live registers,
exec resets them, and a signal handler's are put back by rt_sigreturn
so the interrupted code finds what it had.

The kernel was free to use %xmm too -- GCC vectorised struct copies
and memset into 153 instructions of it -- so an interrupt could
clobber the registers of the program it interrupted with no switch at
all. Kernel, lwIP and uACPI objects are built -mgeneral-regs-only now
and the image has none; cmd/ keeps SSE.

Checked under QEMU: three concurrent loops three times over, on 1 CPU
and on 4, no faults; before, every run faulted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-23 15:02:01 +02:00

1828 lines
60 KiB
C

#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 "sbuf.h"
#include "dbg.h"
#include "fs.h"
#include "file.h"
#include "posix.h"
#include "errno.h"
#include "blkdev.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;
// Kernel stacks, one per ptable slot, statically.
//
// **They used to be a single kalloc() page**, and a measurement said that was
// not enough: painting the stack at allocation and scanning it on every trap
// put the high-water mark at 3736 of 4096 bytes -- 91%, in netd, inside lwIP's
// input path. 360 bytes of headroom on a kernel that takes interrupts on the
// current stack is not headroom. A timer landing at that depth pushes a
// trapframe and runs trap() straight off the bottom of the page and into
// whatever kalloc handed out next, which is very often another process's
// kernel stack.
//
// That is a precise match for the symptom that had been written off as a
// mystery: roughly one shell in four boots died with `trap 14 ... rip 0x0`, a
// user-mode instruction fetch at address zero, having "jumped" somewhere it
// never branched to. A process does not corrupt itself into that state; it
// gets corrupted.
//
// The canary at kstack[0] did not catch it, and could not have. It is checked
// on trap entry and exit, and a frame allocated in one `sub $N, %rsp` can step
// clean over the bottom eight bytes and write below them without ever touching
// the canary. It catches slow leaks past the bottom, not one deep call.
//
// Static and per-slot rather than a bigger kalloc, because kalloc() hands out
// exactly one page and there is no contiguous multi-page allocator to ask. A
// process's ptable slot is fixed for its lifetime, so the stack that belongs
// to that slot is the natural answer: no allocation to fail, no free to
// forget, and the one kalloc failure path in allocproc() disappears with it.
//
// The cost is NPROC * KSTACKSIZE = 1 MB of BSS, paid at link time. That is
// what a kernel stack per process costs; it was being paid out of kalloc
// before, one page at a time, and undercounted.
__attribute__((aligned(PGSIZE)))
static char kstacks[NPROC][KSTACKSIZE];
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");
}
}
// --- what the debugger can be told about processes --------------------------
//
// docs/architecture/debugging.md, D2. These are here rather than in kernel/dev/dbg.c
// because this file owns ptable, and a view written anywhere else would need
// the lock and the struct exported to get at it. One line at the bottom of the
// file that owns the data is the whole discipline.
static const char *
state_name(enum procstate s)
{
switch (s) {
case UNUSED: return "unused";
case EMBRYO: return "embryo";
case SLEEPING: return "sleep";
case RUNNABLE: return "ready";
case RUNNING: return "run";
case ZOMBIE: return "zombie";
default: return "?";
}
}
// "4" means only pid 4. Not a general query language -- just the one filter
// that stops a table from being unreadable on a busy machine.
static int
want_pid(const char *arg)
{
int v = 0;
if (!arg || !*arg)
return 0; // no filter
while (*arg >= '0' && *arg <= '9')
v = v * 10 + (*arg++ - '0');
return v;
}
static void
proc_dump(struct sbuf *out, const char *arg)
{
struct proc *p;
int only = want_pid(arg);
sb_puts(out, " pid ppid pgid state sz(KB) mmap_next "
"name\n");
acquire(&ptable.lock);
for (p = ptable.proc; p < &ptable.proc[NPROC]; p++) {
if (p->state == UNUSED)
continue;
if (only && p->pid != only)
continue;
sb_putu_pad(out, p->pid, 5);
sb_putu_pad(out, p->parent ? p->parent->pid : 0, 6);
sb_putu_pad(out, p->pgid, 6);
sb_puts(out, " ");
sb_pad(out, state_name(p->state), 8);
sb_putu_pad(out, p->sz / 1024, 6);
sb_puts(out, " 0x");
sb_puthex(out, p->mmap_next, 16);
sb_puts(out, " ");
sb_puts(out, p->name);
if (p->killed)
sb_puts(out, " [killed]");
if (p->wedged)
sb_puts(out, " [wedged]");
sb_putc(out, '\n');
}
release(&ptable.lock);
}
// Every mapping every process holds that it does not own the pages of.
//
// This is the table that decides whether exit() frees a page or leaves it
// alone -- see proc_release_shared() in kernel/sys/sys_mem.c -- and the bug
// that made the debugger worth building was a DMA block that was never in it.
// A view of it is the cheapest possible check that the rule is being kept.
static void
vma_dump(struct sbuf *out, const char *arg)
{
struct proc *p;
int any = 0;
int only = want_pid(arg);
sb_puts(out, " pid slot va bytes "
"no_free shmid name\n");
acquire(&ptable.lock);
for (p = ptable.proc; p < &ptable.proc[NPROC]; p++) {
if (p->state == UNUSED)
continue;
if (only && p->pid != only)
continue;
for (int i = 0; i < NVMA; i++) {
if (!p->vma[i].used)
continue;
any = 1;
sb_putu_pad(out, p->pid, 5);
sb_putu_pad(out, i, 6);
sb_puts(out, " 0x");
sb_puthex(out, p->vma[i].addr, 16);
sb_putu_pad(out, p->vma[i].length, 12);
sb_puts(out, p->vma[i].no_free ? " yes " : " no ");
if (p->vma[i].shmid >= 0)
sb_putu_pad(out, p->vma[i].shmid, 5);
else
sb_puts(out, " -");
sb_puts(out, " ");
sb_puts(out, p->name);
sb_putc(out, '\n');
}
}
release(&ptable.lock);
if (!any)
sb_puts(out, only ? " (that pid holds no mappings)\n"
: " (no process is holding a mapping)\n");
}
// Every page table that maps this physical page, and which VMA it falls in.
//
// This is the half of `whose` that no allocator can answer. An allocator knows
// whether it has handed a page out; it has no idea whether anyone still has it
// mapped, and "free, but three processes are still writing to it" is the exact
// shape of the bug that prompted all of this.
//
// The walk is forward -- there is no reverse map -- so it costs one pass over
// every mapped page of every live process. See vm_walk_user().
struct maphit {
struct sbuf *out;
struct whose *w;
struct proc *p;
};
static void
maphit(void *ctx, uint64 va, uint64 pa, uint64 pte)
{
struct maphit *m = (struct maphit *)ctx;
if (pa != m->w->phys)
return;
m->w->mappings++;
sb_puts(m->out, " mapped pid ");
sb_putu(m->out, m->p->pid);
sb_puts(m->out, " (");
sb_puts(m->out, m->p->name);
sb_puts(m->out, ") at 0x");
sb_puthex(m->out, va, 16);
sb_puts(m->out, (pte & PTE_W) ? " rw" : " r-");
sb_puts(m->out, (pte & PTE_U) ? "u" : "-");
// And whether the process admits to holding it. A mapping with no VMA is
// how the DMA blocks used to look, which is why exit() freed them twice.
{
int i, found = 0;
for (i = 0; i < NVMA; i++) {
if (!m->p->vma[i].used)
continue;
if (va < m->p->vma[i].addr ||
va >= m->p->vma[i].addr + m->p->vma[i].length)
continue;
found = 1;
sb_puts(m->out, " vma[");
sb_putu(m->out, i);
sb_puts(m->out, m->p->vma[i].no_free ? "] no_free" : "]");
break;
}
if (!found) {
if (va < m->p->sz)
sb_puts(m->out, " heap");
else
sb_puts(m->out, " NO VMA -- exit() will free this page");
}
}
sb_putc(m->out, '\n');
}
static void
proc_whose(struct sbuf *out, struct whose *w)
{
struct proc *p;
acquire(&ptable.lock);
for (p = ptable.proc; p < &ptable.proc[NPROC]; p++) {
struct maphit m;
if (p->state == UNUSED || p->pgdir == 0)
continue;
m.out = out; m.w = w; m.p = p;
vm_walk_user(p->pgdir, maphit, &m);
}
release(&ptable.lock);
}
void
pinit(void)
{
initlock(&ptable.lock, "ptable");
dbg_register("proc", proc_dump, "every process, with its state and size");
dbg_register("vma", vma_dump, "mappings a process holds but does not own");
dbg_register_whose("mapped", proc_whose);
}
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->utime = p->stime = p->cutime = p->cstime = 0;
// A recycled slot must not inherit a dead process's place in the queue --
// in either direction. sched_init_proc starts it at min_vruntime, which is
// level with everything currently runnable: neither owed CPU nor in debt.
// fork() then copies the parent's vruntime over this, which is what stops a
// process from getting a fresh share of the CPU simply by forking.
sched_init_proc(p, 0);
memmove(p->fpu, fpu_default, sizeof(p->fpu));
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);
// The stack that belongs to this slot. Cannot fail, which is why there is no
// failure branch here any more -- see kstacks[] above.
p->kstack = kstacks[p - ptable.proc];
// Canary at the very bottom: a slow walk past the end clobbers this first,
// and it is checked on every trap()/syscall() entry. It does *not* catch a
// single frame large enough to step over it, which is the case kstacks[]
// being two pages is there to prevent.
*(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;
p->kfn = 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;
// The parent's registers are live in the CPU -- the kernel never touches
// them -- so the child's copy is taken straight from there.
fxsave(np->fpu);
if((np->pgdir = copyuvm(myproc()->pgdir, myproc()->sz)) == 0){
np->kstack = 0; // the slot keeps its stack; see kstacks[]
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);
np->kstack = 0; // the slot keeps its stack; see kstacks[]
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;
// The controlling terminal is a property of the *session*, so it crosses
// fork() with the session id rather than being re-established. It was set
// only by TIOCSCTTY and never inherited, so every child of a shell had
// ctty 0 -- which is why plain `ps` listed the shell and nothing it had
// started, background jobs included. A job that is running on your terminal
// is on your terminal whoever forked it.
np->ctty = myproc()->ctty;
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));
// The child inherits the parent's place in the queue, priority and all.
//
// Both halves matter. Inheriting *nice* is what POSIX says and what makes
// `nice make -j8` mean anything at all -- the point of renicing a build is
// that the compilers it forks are reniced too. Inheriting *vruntime* is the
// part with teeth: were the child to start level with everyone, a process
// could reset its own CPU debt by forking, and something that forks per
// request would collect a fresh full share every time while a long-running
// process kept paying for everything it had ever used.
sched_set_nice(np, myproc()->nice);
np->vruntime = myproc()->vruntime;
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).
// PID 1 is a role, and the kernel guarantees the role is filled.
//
// This is the rule change docs/architecture/init.md exists for. Unix's rule is "PID 1
// is special and must never die", honoured since 1975 by being careful --
// which is not a mechanism, it is a hope, and the thing it hopes about is the
// one program on the machine with no supervisor.
//
// So instead: tear nothing down, keep pid 1, and re-exec in place.
//
// The reason this is so short is the reason it is the right design. exec()
// already replaces the address space, and everything else is *deliberately*
// left alone:
//
// - the pid stays 1, so every orphan the kernel reparented to initproc is
// still correctly parented and initproc is still the pointer it was
// - the children survive, because nothing killed them
// - the open files survive, so the new image still has a console to
// complain to -- this runs before exit() closes anything, which is why
// the call site is the first line of exit() and not the last
// - the state survives, in /proc/schinit, which is the whole reason the
// job table lives outside the process holding it
//
// A sibling init resurrecting a dead one would have to solve all four of those
// and could not solve the first at all. This solves them by not creating them.
//
// The escalation ladder is what makes it safe to have. A machine that cannot
// hold PID 1 up is not a machine that should keep trying for ever, and PSOGM
// renders a stop code, a call chain and a QR code of the whole report -- which
// is a far better end than a silent loop.
//
// generation 1 normal boot
// generation 2-3 re-exec, keep the jobs, carry on
// generation 4 re-exec, but ignore /etc/schinit entirely and start only
// a console shell: a manifest that kills init must not
// kill it for ever
// generation 5 stop
uint init_generation = 1;
int
proc_reincarnate(int status)
{
static char gen[12];
char *argv[3];
int n = 0, v;
init_generation++;
if (init_generation > 5)
panic("init resurrection failed: five generations is enough");
// Decimal, by hand: this runs in a process that is dying and should call as
// little as possible.
v = (int)init_generation;
if (v == 0) {
gen[n++] = '0';
} else {
char tmp[12];
int i = 0;
while (v > 0) { tmp[i++] = '0' + (v % 10); v /= 10; }
while (i-- > 0) gen[n++] = tmp[i];
}
gen[n] = 0;
cprintf("init: pid 1 exited with status %d -- reincarnating, generation %d\n",
status, init_generation);
if (init_generation >= 4)
cprintf("init: generation %d starts a console shell only, "
"ignoring /etc/schinit\n", init_generation);
argv[0] = "schinit";
argv[1] = gen;
argv[2] = 0;
// Clear `killed` first.
//
// exit() was reached from the trap-return path *because* killed was set, and
// that path checks it again on the way back out. Leaving it set would exit
// the brand-new image before its first instruction, and the generation
// counter would climb to five in a few microseconds -- a resurrection loop
// that looks exactly like the crash it was meant to survive.
myproc()->killed = 0;
// No lock is held here and none may be taken: this is the first line of
// exit(), before the file table is walked and before ptable.lock. exec()
// does its own begin_op/end_op and is safe from process context.
if (exec("/bin/schinit", argv, 0) < 0)
return -1;
return 0;
}
void
exit(int status)
{
struct proc *p;
int fd;
if(myproc() == initproc) {
// Returning from exit() is the *success* path here. exec() does not fail
// to return -- it returns 0 having replaced the address space, and the new
// image begins running when the kernel returns to userspace through the
// trapframe it rewrote. So there is nothing more to do but go back.
if (proc_reincarnate(status) == 0)
return;
// Only reached if the re-exec itself failed, which is the one case where
// there is genuinely nothing left to try.
panic("init resurrection failed: could not exec /bin/schinit");
}
// Give back any device this process had claimed. A driver that crashed must
// not keep its hardware locked away from the process that replaces it --
// that is the entire point of running drivers outside the kernel.
pciuser_release_all(myproc());
// And any interface it was acting as. Same reason: a driver that crashed
// must not leave a link that claims to be up. See kernel/dev/netuser.c.
netuser_release_all(myproc());
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 had further gaps (see docs/history/kernel.md
// §7) that this alone did not 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, 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, uint64 *ut, uint64 *st)
{
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
// RUSAGE_CHILDREN: the child's own time *and* whatever it had already
// reaped, which is what makes the total transitive down a whole tree
// of processes rather than one generation deep.
myproc()->cutime += p->utime + p->cutime;
myproc()->cstime += p->stime + p->cstime;
// wait4()'s fourth argument wants the *child's* usage, not the
// parent's running total, so it has to be read here -- three lines
// further down the slot is UNUSED and these are zero.
if (ut) *ut = p->utime + p->cutime;
if (st) *st = p->stime + p->cstime;
p->kstack = 0; // the slot keeps its stack; see kstacks[]
// 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;
}
}
// Two different failures, and they used to share one bare -1.
//
// musl reads a return in [-4095, -1] as -errno, so -1 arrived in
// userspace as EPERM: "operation not permitted". zsh's compinit calls
// wait, got that, printed "wait failed: operation not permitted", and
// abandoned the rest of /etc/zsh/zshrc -- which looked like a broken
// config file rather than a wait() that could not say what went wrong.
if(!havekids){
release(&ptable.lock);
return -ECHILD; // nothing to wait for, which is not an error
}
if(myproc()->killed){
release(&ptable.lock);
return -EINTR; // a signal arrived while waiting
}
if (options & 1) { // WNOHANG
release(&ptable.lock);
return 0;
}
sleep(myproc(), &ptable.lock);
}
}
// Stop a process running, by name, and never start it again.
//
// This exists for one reason: docs/architecture/init.md says the heartbeat's
// timeout path should be "exercised rather than assumed", and a hang is the
// one failure that cannot be produced by killing something. A wedged process
// is still alive, still has its descriptors open, and still shows up in every
// listing -- which is exactly what makes it hard to notice and exactly what
// the heartbeat is for.
//
// Deliberately one-way. There is no unwedge: this is a test instrument for a
// machine that is about to be rebooted, not a job-control mechanism. Real
// SIGSTOP/SIGCONT is a separate thing and is not implemented.
int
proc_wedge(const char *name)
{
struct proc *p;
int found = 0;
acquire(&ptable.lock);
for (p = ptable.proc; p < &ptable.proc[NPROC]; p++) {
if (p->state == UNUSED || p->pid <= 1)
continue;
if (strncmp(p->name, name, sizeof(p->name)) != 0)
continue;
p->wedged = 1;
found = p->pid;
break;
}
release(&ptable.lock);
return found;
}
// --- The FPU ------------------------------------------------------------
//
// Every process has its own x87 and SSE registers, and they were never saved.
// musl's memcpy, the compiler's struct copies and anything doing arithmetic on
// doubles keep live values in %xmm across a context switch, so two processes
// sharing a CPU wrote over each other's: two shells counting in a loop on one
// processor took a NULL+8 fault within seconds, at a different rip each time.
// It had always been there, and every crash with no cause anyone could find
// was a candidate.
//
// The kernel is built with -mgeneral-regs-only, so it never touches these
// registers itself and they stay the user's from trap entry to return. That
// makes saving them a switch-time job and nothing more: sched() saves the
// process leaving the CPU, scheduler() restores the one arriving. No lazy
// switching (CR0.TS and a #NM handler), which would save the copy for a
// process that never uses SSE -- there is no such process; musl's string
// functions use it.
__attribute__((aligned(16)))
const uint8 fpu_default[512] = {
[0] = 0x7F, [1] = 0x03, // FCW 0x037F
[24] = 0x80, [25] = 0x1F, // MXCSR 0x1F80
};
// --- The scheduler ------------------------------------------------------
//
// It was round-robin: walk ptable from end to end, run everything RUNNABLE in
// slot order, and let the timer tick take the CPU back after 10 ms. That is
// xv6's scheduler, and three things are wrong with it on a machine that now
// runs a shell, a network stack and a display server at the same time.
//
// **Slot order is not fairness.** A process that blocks on a read after one
// microsecond and a process that burns its whole slice are treated as equals,
// because nothing counts what anyone actually used. The compile and the shell
// you are typing into get alternate turns; the shell feels it.
//
// **Every tick was a context switch.** `yield()` unconditionally on the timer
// means a process alone on a CPU still pays a full switch -- `switchuvm`, a
// `%cr3` reload, a flushed TLB -- a hundred times a second, to be handed
// straight back to itself.
//
// **There is no way to say one thing matters more than another.** No nice, no
// priority, nothing for a driver thread that must answer a device promptly.
//
// ## What replaces it
//
// Fair share by *virtual runtime*, which is the idea behind both Linux's CFS
// and Zircon's fair scheduler, and is much simpler than either implementation
// of it. Each process carries `vruntime`: the CPU time it has consumed,
// divided by its weight. The scheduler always runs the RUNNABLE process with
// the smallest vruntime.
//
// That single rule produces everything the old one lacked:
//
// - **Fairness falls out.** Running advances your clock; the process that
// has had least CPU is by definition the one whose clock is furthest
// behind, so it runs next. Equal weights converge on equal shares without
// anything having to enforce it.
//
// - **Interactivity falls out too, with no heuristic.** A process that
// spends its life blocked on a keystroke barely advances its vruntime, so
// when the keystroke arrives it is already at the front. No "is this
// interactive?" guess, which is the part of older schedulers that was
// always either gameable or wrong.
//
// - **Priority is one multiplication.** Weight scales how fast your clock
// runs. A high-weight process's vruntime advances slowly, so it is chosen
// more often, in exact proportion.
//
// ## Deliberately not a red-black tree
//
// CFS keeps the runnable set in a red-black tree and Zircon in a WAVL tree,
// so "smallest vruntime" is O(log n) and the leftmost node is cached. Here it
// is a linear scan of ptable, O(NPROC), the same scan the old scheduler did.
//
// That is not laziness. NPROC is 128 slots of a few words each; the scan is a
// handful of cache lines and runs with the lock already held, whereas a
// balanced tree is three hundred lines of rotations that must stay correct
// through every exit path in this file. The scan is the right trade until
// something measures it as the problem, and the rule this kernel keeps is
// that the measurement comes first. Swapping in a tree later changes
// pick_next() and nothing else, which is the point of it being one function.
//
// ## Still one lock, still one queue
//
// Every CPU picks out of the same table under `ptable.lock`. Per-CPU run
// queues with work stealing are the other half of a real SMP scheduler and
// they are a separate change -- they need a load balancer, and a load
// balancer that is wrong is worse than a global lock that is merely
// contended. With the number of cores this actually runs on, the lock is not
// yet what hurts.
// Weight per nice level, indexed by nice + 20.
//
// The ratio between adjacent levels is 1.25, so one step of nice is about a
// 10% change in CPU share when two processes compete -- the convention UNIX
// has used for long enough that programs and people both expect it. The table
// is 1024 / 1.25^nice, rounded, with nice 0 at 1024 so the common case
// multiplies by one.
static const uint32 nice_weight[40] = {
88818, 71054, 56843, 45475, 36380, 29104, 23283, 18626,
14901, 11921, 9537, 7629, 6104, 4883, 3906, 3125,
2500, 2000, 1600, 1280, 1024, 819, 655, 524,
419, 336, 268, 215, 172, 137, 110, 88,
70, 56, 45, 36, 29, 23, 18, 15,
};
#define WEIGHT_NICE0 1024u
// The period in which every runnable process should get a turn, and the floor
// on how short a turn may be.
//
// A slice is SCHED_LATENCY_NS divided between the runnable processes, so with
// few of them each gets a long one and switches are rare; as the count grows
// the slice shrinks until it hits SCHED_MIN_GRAN_NS, below which the switch
// costs more than the work done in it and the period is allowed to stretch
// instead. 20 ms and 2 ms on a kernel whose timer runs at 100 Hz: the minimum
// is deliberately shorter than a tick, because the tick is when the decision
// is *checked*, not the granularity at which time is *measured*.
#define SCHED_LATENCY_NS 20000000ull
#define SCHED_MIN_GRAN_NS 2000000ull
// The floor of every runnable vruntime, which is what keeps a process that
// slept from being owed the world.
//
// Without it a process blocked for ten seconds comes back ten seconds of
// vruntime behind everything else and monopolises the CPU until it catches
// up -- the classic way a naive fair scheduler turns one sleeper into a
// system-wide stall. A waking process is pulled forward to no worse than
// min_vruntime - SCHED_LATENCY_NS: far enough ahead in the queue to be
// scheduled promptly, not so far as to be owed a debt.
//
// It only ever moves forward, and it is guarded by ptable.lock like
// everything else here.
static uint64 min_vruntime;
// TSC cycles to nanoseconds.
//
// tsc_hz is measured against the PIT at boot (kernel/arch/x86_64/lapic.c), so
// this is real time rather than a cycle count that means something different
// on every machine. The multiply is done in 64 bits after dividing the clock
// down to MHz: cycles * 1000 / mhz overflows only after some centuries of
// uptime, where cycles * 1000000000 would overflow in seconds.
static uint64
tsc_to_ns(uint64 cycles)
{
uint64 mhz = tsc_hz / 1000000;
if (mhz == 0)
return 0; // before lapicinit; nothing to charge yet
return (cycles * 1000) / mhz;
}
// Give a process its scheduling state. Called from allocproc, before it is
// ever RUNNABLE.
void
sched_init_proc(struct proc *p, int nice)
{
if (nice < -20) nice = -20;
if (nice > 19) nice = 19;
p->nice = nice;
p->weight = nice_weight[nice + 20];
p->vruntime = min_vruntime; // start level with everyone, owed nothing
p->ran_at = 0;
p->slice_ns = SCHED_MIN_GRAN_NS;
p->slice_used = 0;
p->nvcsw = p->nivcsw = 0;
}
// setpriority(2)/getpriority(2), the only way nice is ever set.
//
// PRIO_PROCESS only. PRIO_PGRP and PRIO_USER are real and are not hard, but
// answering them requires deciding what "the priority of a user" returns when
// their processes disagree, and a wrong answer is worse than -EINVAL.
//
// Lowering nice -- asking for *more* CPU -- is root's alone, which is the one
// permission check in this file that is actually enforced. Without it nice is
// not a courtesy, it is a way for any process to outrank the rest of the
// system by asking.
int
proc_setnice(int pid, int nice)
{
struct proc *p;
int r = -ESRCH;
if (nice < -20) nice = -20;
if (nice > 19) nice = 19;
acquire(&ptable.lock);
for (p = ptable.proc; p < &ptable.proc[NPROC]; p++) {
if (p->state == UNUSED || (pid != 0 && p->pid != pid))
continue;
if (pid == 0 && p != myproc())
continue;
// Two separate rules, and POSIX gives them two separate errnos.
//
// EPERM: you may only renice something you own. Without it any user can
// push every root process to nice 19 and have the run of the machine.
// EACCES: *lowering* nice -- asking for more CPU -- is root's alone.
//
// Both are inert today, because everything runs as root and euid is 0
// everywhere. They are written now because the alternative is a scheduler
// that grows a priority knob and a permission model separately, and the
// window between the two is exactly the bug.
if (myproc()->euid != 0 && myproc()->euid != p->uid) {
r = -EPERM;
break;
}
if (nice < p->nice && myproc()->euid != 0) {
r = -EACCES;
break;
}
sched_set_nice(p, nice);
r = 0;
break;
}
release(&ptable.lock);
return r;
}
int
proc_getnice(int pid, int *out)
{
struct proc *p;
int r = -ESRCH;
acquire(&ptable.lock);
for (p = ptable.proc; p < &ptable.proc[NPROC]; p++) {
if (p->state == UNUSED || (pid != 0 && p->pid != pid))
continue;
if (pid == 0 && p != myproc())
continue;
*out = p->nice;
r = 0;
break;
}
release(&ptable.lock);
return r;
}
// Change a process's priority. ptable.lock must be held.
//
// vruntime is left alone rather than rescaled: it is a record of time already
// spent, and rewriting history so that renicing hands out a retroactive
// refund (or bill) is how a nice() call becomes a way to jump the queue.
void
sched_set_nice(struct proc *p, int nice)
{
if (nice < -20) nice = -20;
if (nice > 19) nice = 19;
p->nice = nice;
p->weight = nice_weight[nice + 20];
}
// Charge a process for the time it has just run, and advance its vruntime.
// ptable.lock must be held, and p must be the process leaving the CPU.
static void
sched_charge(struct proc *p)
{
uint64 now = rdtsc(), ns;
if (p->ran_at == 0 || now <= p->ran_at) {
p->ran_at = 0;
return;
}
ns = tsc_to_ns(now - p->ran_at);
p->ran_at = 0;
p->slice_used += ns;
// The weighting, and the only line in the scheduler where nice does
// anything: a process of twice the weight is charged half the vruntime for
// the same real time, so its clock falls behind and pick_next() returns to
// it sooner.
//
// The guard is for the one way weight can be zero: a ptable slot is BSS,
// and BSS is zeros until allocproc() runs sched_init_proc() on it. Nothing
// reaches here on such a slot today -- only a process that has run gets
// charged, and a process that has run went through allocproc -- but the
// failure if that ever stops being true is a divide-by-zero #GP inside the
// scheduler, which is a fault with no good place to report itself.
if (p->weight == 0)
p->weight = WEIGHT_NICE0;
p->vruntime += (ns * WEIGHT_NICE0) / p->weight;
}
// How long a slice should be, given how many processes want one.
static uint64
sched_slice(int nrunnable)
{
uint64 slice;
if (nrunnable <= 1)
return SCHED_LATENCY_NS;
slice = SCHED_LATENCY_NS / (uint64)nrunnable;
return slice < SCHED_MIN_GRAN_NS ? SCHED_MIN_GRAN_NS : slice;
}
// Place a process that has just become runnable, and keep min_vruntime
// honest. ptable.lock must be held.
//
// Called from wakeup1() and from anywhere else that moves a process into
// RUNNABLE from a state where it was not accruing vruntime.
static void
sched_enqueue(struct proc *p)
{
uint64 floor = min_vruntime > SCHED_LATENCY_NS
? min_vruntime - SCHED_LATENCY_NS : 0;
if (p->vruntime < floor)
p->vruntime = floor;
p->slice_used = 0;
}
// The runnable process with the smallest vruntime, and a count of how many
// were in the running. ptable.lock must be held.
static struct proc *
pick_next(int *nrunnable)
{
struct proc *p, *best = 0;
int n = 0;
for (p = ptable.proc; p < &ptable.proc[NPROC]; p++) {
// A wedged process is passed over for ever. It is still RUNNABLE, still
// owns its descriptors and still appears in every listing -- which is
// the point: it looks exactly like a process that is merely busy.
if (p->state != RUNNABLE || p->wedged)
continue;
n++;
if (best == 0 || p->vruntime < best->vruntime)
best = p;
}
*nrunnable = n;
return best;
}
// Whether the running process has used up its slice, so trap() knows whether
// the timer tick should become a yield(). No lock: it reads only the current
// process's own fields, and the only writer of them is the current process.
//
// This is the line that removed a hundred context switches a second from an
// idle machine. The old code yielded on *every* tick unconditionally.
int
sched_slice_expired(void)
{
struct proc *p = myproc();
uint64 now, ns;
if (p == 0 || p->ran_at == 0)
return 0;
now = rdtsc();
if (now <= p->ran_at)
return 0;
ns = tsc_to_ns(now - p->ran_at);
return (p->slice_used + ns) >= p->slice_ns;
}
void
scheduler(void)
{
struct proc *p;
int nrunnable;
for(;;){
sti();
acquire(&ptable.lock);
p = pick_next(&nrunnable);
if (p == 0) {
// Nothing to run. Drop the lock before halting -- holding it through a
// hlt() would stop every other CPU from scheduling until an interrupt
// happened to arrive here.
release(&ptable.lock);
hlt();
continue;
}
// min_vruntime tracks the front of the queue and never moves backwards,
// so a process that sleeps through a busy period cannot return with a
// claim on all the CPU it missed. See the comment on the variable.
if (p->vruntime > min_vruntime)
min_vruntime = p->vruntime;
p->slice_ns = sched_slice(nrunnable);
p->slice_used = 0;
p->ran_at = rdtsc();
proc_set(p);
switchuvm(p);
fxrstor(p->fpu);
p->state = RUNNING;
swtch(&mycpu()->scheduler, p->context);
switchkvm();
proc_set(0);
// Back from swtch: p has given the CPU up, through sched(). It has
// already been charged there, where the reason for the switch was known.
release(&ptable.lock);
}
}
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;
// Charge for the time just run before handing the CPU back. Here rather
// than in scheduler(), because this is the one point every path off a CPU
// goes through -- yield(), sleep() and exit() alike -- and doing it on the
// way out means the vruntime the scheduler then compares is already
// up to date rather than a tick stale.
sched_charge(myproc());
fxsave(myproc()->fpu);
swtch(&myproc()->context, mycpu()->scheduler);
mycpu()->intena = intena;
}
void
yield(void)
{
acquire(&ptable.lock);
myproc()->state = RUNNABLE;
myproc()->nivcsw++; // involuntary: the slice ran out
sched();
release(&ptable.lock);
}
// Which device the root filesystem is on.
//
// It was `#define ROOTDEV 1` -- IDE disk 1 -- so a kernel could only ever root
// on the disk it was *built* expecting. Booting the same image from a USB
// stick was impossible by construction, and that is the one thing standing
// between this kernel and its own acceptance test, which begins "boot on real
// hardware from a USB stick".
//
// A probe rather than a boot argument, because the answer is discoverable and
// asking someone to name a device number is a worse interface than looking.
// schfs_kmount() is already written to be used this way -- its own comment
// says "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"
// -- so each candidate costs one scan and answers yes or no. A failed probe
// leaves the slot unmounted and reusable, which is what makes a loop over
// candidates legal at all.
//
// **The order is preference, not confidence.** A ramdisk root is an explicit
// build choice and wins outright. After it, USB comes before the fixed disks:
// a stick that is plugged in was plugged in on purpose, and a machine with
// both should boot the thing the person is holding.
uint rootdev;
static int
rootdev_usable(uint dev)
{
if (dev == RAMDISK_DEV)
return ramdisk_present();
if (dev == USBDISK_DEV)
return usb_storage_present();
if (dev >= BLKDEV_FIRST)
return blkdev_present(dev);
// iderw() panics on an absent drive rather than failing, so a probe has to
// ask first. That is the whole reason ide_disk_present() exists.
return ide_disk_present((int)dev);
}
static void
rootdev_mount(void)
{
// The fixed devices, with the registered ones -- SATA, NVMe, and every
// partition on them -- between USB and IDE. A partition GPT types as SchFS
// goes ahead of the rest of the registry, because it was labelled that way
// on purpose; the others are probed too, so a volume in an untyped or MBR
// partition, or on a whole unpartitioned disk, still boots.
struct { uint dev; const char *what; } cand[4 + 2 * NBLKDEV];
int ncand = 0;
#ifdef RAMDISK_ROOT
cand[ncand].dev = RAMDISK_DEV; cand[ncand++].what = "ramdisk";
#endif
cand[ncand].dev = USBDISK_DEV; cand[ncand++].what = "USB disk";
for (int pass = 0; pass < 2; pass++)
for (int i = 0, d; (d = blkdev_nth(i)) >= 0; i++)
if (blkdev_is_schfs(d) == !pass) {
cand[ncand].dev = d;
cand[ncand++].what = blkdev_name(d);
}
cand[ncand].dev = 1; cand[ncand++].what = "IDE disk 1";
cand[ncand].dev = 0; cand[ncand++].what = "IDE disk 0";
for (int i = 0; i < ncand; i++) {
if (!rootdev_usable(cand[i].dev))
continue;
schfs_kmount(cand[i].dev);
if (schfs_kmounted()) {
rootdev = cand[i].dev;
kinfo("root: %s (device %d)\n", cand[i].what, cand[i].dev);
return;
}
kinfo("root: %s (device %d) carries no SchFS volume\n",
cand[i].what, cand[i].dev);
}
// Name what was looked at. "no root filesystem" with no list is the least
// useful thing a kernel can say at the one moment somebody needs to know
// which disk it expected.
// Plain %s and no width. cprintf() has no field-width specifier -- it
// prints "%-11s" literally and then reads the *next* argument for the %d,
// which is how the first version of this message came out as
// `%-11s device 1596400`. See print_int() in kernel/core/console.c.
cprintf("root: no SchFS volume on any candidate device. Looked at:\n");
for (int i = 0; i < ncand; i++)
cprintf(" device %d, %s: %s\n", cand[i].dev, cand[i].what,
rootdev_usable(cand[i].dev) ? "present, but no SchFS on it"
: "not present");
panic("no root filesystem");
}
void
forkret(void) {
static int first = 1;
release(&ptable.lock);
// A kernel thread starts here and never leaves. It shares the scheduler's
// rotation with everything else, so it can hold sleeplocks and block on the
// disk exactly as a syscall would -- which is the whole reason it is a
// process rather than a timer callback.
//
// Ahead of the first-boot block below on purpose: a kernel thread is only
// ever created long after the root filesystem is mounted, so it must not be
// the one that runs that setup.
if (myproc()->kfn) {
myproc()->kfn();
panic("kthread returned");
}
if (first) {
first = 0;
iinit();
mountinit();
// SchFS is the root filesystem, on whichever device turns out to have
// one. Here rather than in main() because mounting reads blocks, bread()
// sleeps, and sleeping needs a process context.
rootdev_mount();
// 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(SCHFS_DEV));
if (stale)
iput(stale);
}
}
}
// Start a kernel thread: a process that runs `fn` in kernel mode and never
// returns to userland.
//
// It gets a page directory of its own with only the kernel mapped, because the
// scheduler calls switchuvm() for every process it runs and that panics on a
// null pgdir. Sharing kpgdir directly would work equally well; a private one
// costs a page and keeps the "every proc owns its pgdir" invariant intact.
//
// No files, no cwd, no parent: it is not in the process tree, cannot be waited
// for and must never exit. Returns the pid, or -1.
int
kthread_create(void (*fn)(void), const char *name)
{
struct proc *p;
if ((p = allocproc()) == 0)
return -1;
if ((p->pgdir = setupkvm()) == 0) {
p->kstack = 0; // the slot keeps its stack; see kstacks[]
p->state = UNUSED;
return -1;
}
p->sz = PGSIZE; // never used; 0 upsets code that treats it as a size
p->parent = 0;
p->cwd = 0;
p->kfn = fn;
safestrcpy(p->name, name, sizeof(p->name));
acquire(&ptable.lock);
p->state = RUNNABLE;
release(&ptable.lock);
return p->pid;
}
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;
myproc()->nvcsw++; // voluntary: it asked to wait
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;
// Pull it forward to the front of the timeline if it has been asleep
// long enough to have fallen off the back of it -- see sched_enqueue.
sched_enqueue(p);
}
}
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.
// PID 1 does not die from a signal it never asked to handle.
//
// POSIX, and Linux in sig_task_ignored(): a signal whose disposition is the
// default action is discarded for init rather than delivered. Without this the
// system has a single keystroke that kills it -- Ctrl-C sends SIGINT to the
// foreground process group, init is pgid 1, and any moment where a terminal's fg_pgrp
// is 1 (before a shell claims the terminal, after one exits, or in the window
// between the two while the key is held down) lands SIGINT on init. init has
// no handler, so killed is set, exit() runs, and exit() panics outright on
// initproc: "init exiting". Spamming Ctrl-C at a frozen program is exactly the
// input that finds that window.
//
// Applies to SIGKILL too, which is the point: init is not killable from
// userspace on any Unix. A handler it did install is still delivered.
static int
init_ignores(struct proc *p, int sig)
{
return p == initproc && p->sig_handler[sig] == 0;
}
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){
if(init_ignores(p, sig)){
release(&ptable.lock);
return 0; // silently dropped, as POSIX requires
}
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;
sched_enqueue(p); // a signal is a wakeup too; same floor
}
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
if(init_ignores(p, sig))
continue; // see init_ignores(): this is the Ctrl-C case
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;
sched_enqueue(p); // a signal is a wakeup too; same floor
}
}
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;
sched_enqueue(p); // a signal is a wakeup too; same floor
}
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;
out->ctty = p->ctty;
out->utime = p->utime;
out->stime = p->stime;
out->cutime = p->cutime;
out->cstime = p->cstime;
out->nice = p->nice;
out->vruntime = p->vruntime;
out->nvcsw = p->nvcsw;
out->nivcsw = p->nivcsw;
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;
}
// Make PID 1 exit at its next trap return, so proc_reincarnate() runs.
//
// Not a signal: init_ignores() drops signals to initproc and should, because
// init is not killable from userspace on any Unix. This is the kernel
// deciding on its own behalf, which is a different thing, and it exists so
// the resurrection path can be exercised rather than reasoned about.
void
proc_kill_init(void)
{
if (initproc)
initproc->killed = 1;
}