docs/plan/hybrid.md mechanisms 2 and 4: MMIO and DMA to a process.
A process can enumerate the bus, claim a device exclusively, map its BARs and
get memory the device can be pointed at. Proven end to end against an e1000e
-- the exact device class we cannot drive and would want a foreign driver for:
device 7: 8086:10d3 (network)
claimed
BAR0 at 0x81060000, 128 KB
regs[0..3] = 0x00140261 0x00000000 0x00080283 0x00000000
MMIO reaches the device
DMA 8 KB at virtual 0x40001000, physical 0x3f8eb000
Those are the e1000e's CTRL and STATUS with full-duplex and link-up set, which
is how you tell a real mapping from one reading 0xFFFFFFFF off an unmapped
address. Claiming a device the kernel drives is refused -- two writers of one
register set looks like hardware that has started lying -- and everything a
process held is released when it exits, because a driver that crashed must not
keep its hardware away from its replacement.
mmap's offset is the BAR's physical address, checked against the BARs of
devices *this process has claimed*. Without that check the device is /dev/mem
with extra steps and a process could map the kernel's own memory. Mappings are
write-through: a cached write to a doorbell arrives whenever the cache feels
like it, which from a driver's side is indistinguishable from hardware that
ignores you.
The interface is about PCI devices, not about any driver model, and its first
users will be SchOS's own drivers when virtio-gpu is evicted. That ordering is
the design rather than an accident: an interface whose first consumers are
native drivers is a general mechanism something foreign can use later, instead
of a hole shaped like something foreign.
Also: PROT_* and MAP_* moved to include/posix.h. They were defined only in
sys_mem.c, so cmd/fbinfo carried a private copy and this wanted a third --
two copies of a number the kernel compares against, where a mismatch shows up
as a mapping with the wrong permissions rather than an error.
No IOMMU, so this isolates a driver against its own bugs and not against its
own DMA. The plan says so at length and it is still true.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
1077 lines
31 KiB
C
1077 lines
31 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 "fs.h"
|
|
#include "file.h"
|
|
#include "posix.h"
|
|
#include "errno.h"
|
|
|
|
// Verify myproc()->fs_base offset matches the hardcoded 0x688 in trapasm.S.
|
|
_Static_assert(__builtin_offsetof(struct proc, fs_base) == 0x18,
|
|
"myproc()->fs_base offset changed — update trapasm.S WRITE_USER_FS macro (must be 0x18)");
|
|
|
|
struct {
|
|
struct spinlock lock;
|
|
struct proc proc[NPROC];
|
|
} ptable;
|
|
|
|
static struct proc *initproc;
|
|
|
|
int nextpid = 1;
|
|
extern void forkret(void);
|
|
extern void syscall_trapret(void);
|
|
|
|
static void wakeup1(void *chan);
|
|
|
|
void
|
|
kstack_check_canary(const char *where)
|
|
{
|
|
if(myproc() == 0 || myproc()->kstack == 0)
|
|
return;
|
|
if(*(uint64*)(myproc()->kstack) != KSTACK_CANARY){
|
|
cprintf("kstack canary corrupted for pid %d (%s) at %s: got 0x%p\n",
|
|
myproc()->pid, myproc()->name, where, *(uint64*)(myproc()->kstack));
|
|
panic("kstack overflow");
|
|
}
|
|
}
|
|
|
|
void
|
|
pinit(void)
|
|
{
|
|
initlock(&ptable.lock, "ptable");
|
|
}
|
|
|
|
static struct proc*
|
|
allocproc(void)
|
|
{
|
|
struct proc *p;
|
|
char *sp;
|
|
|
|
acquire(&ptable.lock);
|
|
|
|
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++)
|
|
if(p->state == UNUSED)
|
|
goto found;
|
|
|
|
release(&ptable.lock);
|
|
return 0;
|
|
|
|
found:
|
|
p->state = EMBRYO;
|
|
p->pid = nextpid++;
|
|
|
|
// ptable slots get reused once a previous occupant is reaped, but
|
|
// nothing previously cleared the signal-delivery fields added for
|
|
// SIGCHLD (sig_handler/sig_restorer/pending_signals/signal_mask/
|
|
// in_signal/saved_sigtf) — a freshly allocated process would silently
|
|
// inherit whatever a totally unrelated earlier process left behind,
|
|
// including stale handler addresses. deliver_signals() (syscall.c)
|
|
// would then jump to garbage on this new process's very first syscall
|
|
// return, faulting at whatever bogus rip happened to be sitting there.
|
|
memset(p->sig_handler, 0, sizeof(p->sig_handler));
|
|
memset(p->sig_restorer, 0, sizeof(p->sig_restorer));
|
|
p->pending_signals = 0;
|
|
p->signal_mask = 0;
|
|
p->in_signal = 0;
|
|
p->sigsusp_oldmask = 0;
|
|
p->sigsusp_active = 0;
|
|
|
|
// Same reasoning as the signal fields above: a recycled slot must not
|
|
// inherit a dead process's identity or exit status.
|
|
p->xstate = 0;
|
|
p->killsig = 0;
|
|
p->pgid = 0;
|
|
p->sid = 0;
|
|
p->uid = p->euid = p->suid = 0;
|
|
p->gid = p->egid = p->sgid = 0;
|
|
|
|
release(&ptable.lock);
|
|
|
|
if((p->kstack = kalloc()) == 0){
|
|
p->state = UNUSED;
|
|
return 0;
|
|
}
|
|
// Canary at the very bottom of the kstack page: if a deep/nested trap
|
|
// ever overflows this 4096-byte stack, this is the first thing clobbered.
|
|
// Checked on every trap()/syscall() entry (kstack_check_canary()).
|
|
*(uint64*)(p->kstack) = KSTACK_CANARY;
|
|
sp = p->kstack + KSTACKSIZE;
|
|
|
|
sp -= sizeof(struct trapframe);
|
|
p->tf = (struct trapframe*)sp;
|
|
|
|
sp -= sizeof(addr_t);
|
|
*(addr_t*)sp = (addr_t)syscall_trapret;
|
|
|
|
sp -= sizeof(struct context);
|
|
p->context = (struct context*)sp;
|
|
memset(p->context, 0, sizeof *p->context);
|
|
p->context->rip = (addr_t)forkret;
|
|
|
|
p->syscall_map = 0;
|
|
p->map_size = 0;
|
|
p->mmap_next = 0;
|
|
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/plan/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/plan/init.md part 3 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;
|
|
}
|