SchOS/kernel/proc/proc.c
Nexoniarz bc39b73969 debugger: D5, the network transport -- and the plan closes
schdbgd bridges TCP to the debugger, which is the one way in that does not
need you to be sitting at the machine. It is a bridge and nothing else: a line
in becomes a line written to /dev/dbgro and the answer comes back, so `nc` is a
complete client and there is no clever client to be broken on the day you need
this.

    # schdbgd &
    schdbgd: debugger on port 4444, loopback only
    $ nc 127.0.0.1 4444
    dma
    arena  0xffff80003da41000-0xffff80003ea41000  16384 KB total, ...

A kernel debugger plus a TCP socket is a remote root hole, so three things were
decided rather than discovered:

  - It does not start by itself. schdbgd.job ships Disabled. SchOS has no
    kernel command line, so the boot flag the plan asked for is the job
    manifest, which is the same thing in this system's own idiom.
  - It binds loopback; -a names an address and says so in a paragraph.
  - It cannot reach a view that changes state, enforced in the kernel.

That last one is /dev/dbgro: /dev/dbg on minor 1. The minor is the one piece of
per-open state a character device already sees, so which node you opened is the
permission -- nothing to latch, nothing to remember between the write and the
read, no way across. Which views are refused comes from dbg_register_rw() on
the registration itself, because a central list of dangerous views goes stale
in the direction where a new view is quietly treated as safe. Today that is
`watch <addr>`; `watch` with no argument is still the listing and still
answered.

Also: socket() now accepts IPPROTO_TCP with SOCK_STREAM and IPPROTO_UDP with
SOCK_DGRAM. They name the default rather than departing from it, and toybox's
nc writes them out -- so TCP was "Protocol not supported" on a system whose
whole network is TCP.

docs/plan/debugger.md retires into docs/architecture/debugging.md; 16 files
repointed. Every plan in docs/plan/ that was open is now closed, so the next
thing is Gate 0 itself: the survey, with the instrument built.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
2026-09-14 15:33:53 +02:00

1278 lines
37 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"
// 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");
}
}
// --- 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->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;
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;
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).
// 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);
}
// 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;
}
}
// 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;
}
void
scheduler(void)
{
struct proc *p;
int skipped = 0;
for(;;){
sti();
acquire(&ptable.lock);
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) {
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);
// 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.
//
// 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(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) {
kfree(p->kstack);
p->kstack = 0;
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;
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.
// 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;
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;
}
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;
}
// 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;
}