SchOS/kernel/proc/proc.c
Nexoniarz 1dd4ba0186 debugger: the kernel can be asked a question
docs/plan/debugger.md, milestones D0, D1 and D2. What they replace is the
ninety-second loop -- add a cprintf, rebuild, boot, read the serial log --
which answers exactly the one question you thought of before the rebuild.

The rule the shape follows: **it observes, it does not control.** No
stepping, no software breakpoints, no stopping the world. Half the bugs
this kernel produces are timing -- an interrupt storm, a level-triggered
line nobody acknowledges, a driver that faults only on its second start --
and every one of them disappears when you halt the machine to look. And
the layering rule, which is the circularity rule the display already has:
a debugger that needs lwIP cannot debug lwIP.

D0 -- symbols. tools/ksyms.pl turns the first link pass into a table the
second pass carries. Two passes is enough because .ksym is placed after
.text and .rodata, so carrying the table cannot move anything recorded in
it; the build lists both passes' text symbols and refuses to link if they
ever disagree, because a symbol table that is subtly wrong gives confident
false answers. ksym.c reads it through weak symbols, so pass one -- which
has no table -- still links. A crash chain now reads panic+0x5f <-
proc_sysrq_cmd+0x99 <- ... <- syscall+0xf6.

D1 -- a log that outlives the kernel. Every character through consputc()
lands in a byte ring with a timestamp per line, readable as /proc/kmsg.
It sits in a region carved before the page allocator gets anything, with a
magic header, so a kernel finds the previous kernel's log still there --
including the whole PSOGM report. panic.c's own comment said "there is no
core dump and no log to go back to"; that is no longer true.

  The obvious place for it is the top of RAM beside the DMA arena. That
  was the first attempt and the magic came back as ASCII on the reboot,
  because the top of usable memory is exactly where the firmware makes its
  own allocations. It lives above the kernel image instead, which is a
  stable address across boots of the same build.

D2 -- the introspection registry. A subsystem publishes its state with one
line at the bottom of the file that owns the data:

  dbg_register("dma", dma_dump, "the DMA arena's free extents");

Seven views -- dma, mem, proc, vma, claims, pci, sock -- reachable three
ways: sysrq 'g' (always, no process and no filesystem), /dev/dbg (echo a
line, cat the answer), and schdbg. The protocol is lines of text because
the same bytes have to work where nothing can allocate or frame a record,
and because a protocol you cannot drive by hand is one you cannot debug.
struct sbuf moved out of kernel/dev/procfs.c into include/sbuf.h rather
than being written a second time.

Several views exist because of a specific bug. `vma` is the table that
decides whether exit() frees a page, and the DMA ownership bug that made
this worth building was a block that was never in it.

Also fixed, found by building with clang for the first time in a while:
struct iovec was defined in both posix.h and socket.h, and every file that
included both was a redefinition. GCC 15 defaults to C23, which permits an
identical struct redeclaration, so the gcc path never noticed -- while
clang defaults to gnu17 and rejected it, breaking the one toolchain
shell.nix exists to provide and the UEFI loader requires. Both are now
guarded.

D3 (`whose <addr>`, the cross-subsystem ownership query) and D4 (hardware
watchpoints on DR0-DR3) are not built and are described honestly as
missing in the architecture doc's "What it does not do".

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

1198 lines
35 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/plan/debugger.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");
}
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");
}
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());
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;
}