542 lines
32 KiB
C
542 lines
32 KiB
C
#ifndef FROSTBYTE_SCHED_H
|
|
#define FROSTBYTE_SCHED_H
|
|
|
|
#include "types.h"
|
|
#include "vfs.h"
|
|
#include "prioq.h"
|
|
#include "isr.h"
|
|
#include "signalnum.h"
|
|
#include "mmap.h"
|
|
|
|
/* Default scheduling weight (xv6/CFS-style "nice" niceWeight -- higher runs
|
|
more often, not longer). threadSetPriority scales relative to this. */
|
|
#define schedDefaultWeight 1024u
|
|
|
|
/* Returned by threadCreate/threadCreateUser when the thread table is full
|
|
(see threadCapacity) -- never a valid thread id, since id 0 is always the
|
|
permanent idle thread. Callers must check for this before using the
|
|
result to index into anything thread-related. */
|
|
#define threadInvalidId 0xFFFFFFFFu
|
|
|
|
/* Capacity of Thread.name, including the terminator. Large enough for every
|
|
name this kernel currently produces (kernel thread literals, "/bin/..."
|
|
paths passed to threadCreateUser by spawnUser/doSpawn) with room to
|
|
spare; longer names are truncated, never overflowed. */
|
|
#define threadNameMax 32
|
|
|
|
/* Capacity of Thread.env below: envMaxVars is how many "NAME=VALUE" slots
|
|
a process may hold at once, envEntryMax is the max size in bytes of a
|
|
single slot INCLUDING both the '=' separator and the nul terminator
|
|
(so "NAME=VALUE\0" must fit in envEntryMax bytes). Generous enough for
|
|
anything this kernel's own shell/demos actually set (see user/envprint.c)
|
|
with room to spare, the same "small fixed table, not a growable heap
|
|
allocation" choice Thread.cwd already made for a single path. */
|
|
#define envMaxVars 8
|
|
#define envEntryMax 64
|
|
|
|
/* One bit per signal number in Thread.sigPending/sigBlocked, and the size
|
|
of Thread.sigHandlers[] below -- see signal.h for the actual signal
|
|
subsystem this supports. Defined here, not signal.h, purely to break the
|
|
include cycle (signal.h includes sched.h for the Thread type). */
|
|
#define sigMaxSignals 32
|
|
|
|
/* Depth of the per-thread nested-handler context stack (Thread.sigSavedRegs/
|
|
sigSavedBlocked below) -- how many signal handlers may genuinely interrupt
|
|
each other (handler A running, a second signal arrives and is not blocked
|
|
by A's own sa_mask, so handler B runs nested inside A, then A resumes once
|
|
B returns) before sigConsumePending (signal.c) refuses to deliver a new
|
|
one and simply leaves it pending. A real kernel bounds this too (rlimit
|
|
on the signal-frame stack it pushes into user memory); this one has no
|
|
user-memory signal frame to overflow at all (see signal.h's file comment
|
|
-- the saved context lives in the Thread struct itself), so the bound
|
|
exists purely to keep that struct's size fixed and to fail a runaway
|
|
signal-storm safely (further deliveries just wait) instead of unboundedly
|
|
growing kernel memory per thread. 4 is generous headroom for anything
|
|
this kernel's own handlers actually do (none nest more than one deep
|
|
today -- see user/nesttarget.c) without being large enough to make
|
|
Thread noticeably bigger than sigMaxSignals-worth of handlers already
|
|
makes it. */
|
|
#define sigMaxNestDepth 4
|
|
|
|
typedef enum {
|
|
threadUnused = 0,
|
|
threadRunnable = 1,
|
|
threadRunning = 2,
|
|
threadSleeping = 3,
|
|
threadDead = 4,
|
|
threadBlocked = 5,
|
|
/* Job-control stop (signal.h's SIGSTOP/SIGTSTP): excluded from
|
|
schedule()'s ready-queue rebuild exactly like Sleeping/Blocked
|
|
already are (only threadRunnable is ever inserted), so a stopped
|
|
thread simply never gets picked until threadContinue moves it back
|
|
to threadRunnable. Distinct from threadBlocked because it is NOT
|
|
waiting on a wait-channel condition -- schedWakeChannel must never
|
|
treat it as a match (see schedWakeChannel), only threadContinue
|
|
(an explicit SIGCONT, not "the resource became available") ever
|
|
moves a thread out of this state. */
|
|
threadStopped = 6
|
|
} ThreadState;
|
|
|
|
typedef void (*ThreadEntry)(void);
|
|
|
|
typedef struct Thread {
|
|
uint64 rsp;
|
|
uint64 stackBase;
|
|
uint64 kernelStackTop;
|
|
uint64 sleepUntil;
|
|
/* Absolute pitTicks() deadline for a pending alarm(2)-style SIGALRM
|
|
(0 == no alarm pending), set by threadSetAlarm and consulted once
|
|
per timer tick by schedule() -- the exact same shape as sleepUntil
|
|
above, checked in the exact same loop, except expiry raises a real
|
|
signal (sigRaise, signal.h) against the owning thread instead of
|
|
just flipping its own ThreadState back to Runnable. Deliberately a
|
|
plain deadline, not a wait-channel/ThreadState change: unlike
|
|
threadSleep, a thread with a pending alarm keeps running (or
|
|
sleeping, or blocked on something unrelated) exactly as it already
|
|
was right up until the deadline, exactly like real alarm()'s timer
|
|
ticking down in the background regardless of what the process is
|
|
doing. */
|
|
uint64 alarmAt;
|
|
/* poll.h's sysPoll (syscall.c) uses the same restart-the-syscall park
|
|
pattern sysRead/sysWrite/sysWait already use, but a poll call can be
|
|
woken by any one of several unrelated fds, not one -- so instead of
|
|
parking on a single fd's own wait channel (vfsBlockingChannel),
|
|
sysPoll blocks every waiting thread on ONE shared channel
|
|
(schedPollChannel below) and simply re-checks its whole fd list from
|
|
scratch on every wake, spurious or not, exactly like the retry loop
|
|
every OTHER restartable syscall already has. pollActive marks "this
|
|
thread is mid a not-yet-satisfied sysPoll call" (so a retry entry
|
|
can tell itself apart from a genuinely fresh call); pollDeadline is
|
|
the absolute pitTicks() deadline schedule()'s own expiry scan forces
|
|
a wake at if no fd ever becomes ready first (0 == wait forever,
|
|
meaningful only while pollActive is true -- the same "0 reserved"
|
|
convention alarmAt above already uses, but here 0 is a real,
|
|
legal, requested state (a timeout of -1) rather than "nothing
|
|
pending"). */
|
|
bool pollActive;
|
|
uint64 pollDeadline;
|
|
uint64 brk;
|
|
uint64 vruntime;
|
|
uint64 exitCode;
|
|
uint32 weight;
|
|
uint32 id;
|
|
/* Id of whichever thread was actually running (currentIndex, sched.c)
|
|
at the moment this thread was created -- threadCreate/
|
|
threadCreateUser both set this unconditionally, so it is always
|
|
meaningful, not just for real userspace parent/child pairs: a
|
|
boot-time spawnUser (kmain.c) call happens before schedStart() ever
|
|
runs, so every process kmain.c spawns directly gets the permanent
|
|
idle thread (id 0) as its parentId, which is deliberately never a
|
|
valid sigRaise target (see signal.c's targetId==0 rejection) --
|
|
only a RUNTIME sysSpawn (syscall.c's doSpawn, called from inside a
|
|
live thread's own syscall) ever produces a parentId that is itself
|
|
a live, signalable process. Consulted by threadExit/threadKill
|
|
below to deliver a real SIGCHLD to whoever spawned this thread the
|
|
instant it actually dies, matching real wait()/SIGCHLD semantics --
|
|
NOT the same thing as "whoever later calls threadJoin/sysWait on
|
|
this id", which can be anyone that knows the pid, not just the
|
|
spawner (this kernel's honest fork()-less design already documents
|
|
that distinction, see doSpawn's own file comment in syscall.c). */
|
|
uint32 parentId;
|
|
/* Distinguishes THIS specific occupant of slot `id` from any other
|
|
thread that has occupied (or will ever occupy) the same numeric slot
|
|
before or after -- the exact same "handle number alone is not enough
|
|
once a slot is recycled" idea pipe.c's own Pipe.incarnation already
|
|
solves for pipe handles (see pipeIncarnation's own doc comment,
|
|
pipe.h). Assigned once, at creation time, from a single monotonic
|
|
counter (sched.c's nextThreadGeneration, never reused, mirroring
|
|
pipe.c's nextPipeIncarnation exactly) -- threadReap/threadDiscard's
|
|
memset resets this back to 0 when the slot is freed, and the next
|
|
threadCreate/threadCreateUser to land on that slot gets a brand new,
|
|
never-before-seen value, so 0 here always means "this exact slot is
|
|
currently unoccupied," never a live thread's own generation. Exists
|
|
purely so pidfd.h's pidfdOpen/pidfdTarget can tell "the same thread I
|
|
opened a handle on earlier" apart from "a different, unrelated thread
|
|
that has since been handed the same slot number" -- allocSlot's own
|
|
doc comment (sched.c) documents the real, previously-unguarded race
|
|
this closes for any caller that opts into pidfd.h instead of a raw
|
|
pid (kill/wait/sigSend/setpgid/nice below all still take a raw pid,
|
|
completely unchanged, exactly as that same comment explains why). */
|
|
uint32 generation;
|
|
/* Process group id (POSIX pgid): the scope real job control and real
|
|
kill(2)/sigSend's pid<0 convention actually target, not a single
|
|
thread (signalnum.h's sigInt/sigTstp arriving from keyboard.c's real
|
|
Ctrl-C/Ctrl-Z tracking are the live proof -- see keyboard.c's
|
|
keyboardHandleScancode and signal.c's sigRaiseGroup). Set once, at
|
|
creation time, by threadCreate/threadCreateUser's own initialPgid
|
|
helper (sched.c): a thread created while the idle thread (id 0) is
|
|
"current" -- i.e. one of kmain.c's boot-time spawnUser/threadCreate
|
|
calls, the only time that can ever be true, see parentId's own
|
|
comment just above -- becomes its own new group leader (pgid == its
|
|
own id), exactly like a real init process placing each service it
|
|
starts into its own job; a thread created by a genuinely running
|
|
parent (any runtime sysSpawn, syscall.c's doSpawn) instead INHERITS
|
|
that parent's pgid, matching real fork()'s own default (a child
|
|
stays in its parent's process group until something explicitly
|
|
calls setpgid). threadSetPgid (below) is the only thing that ever
|
|
changes it again after creation -- the sysSetpgid syscall's
|
|
userspace-facing half of real setpgid(2). */
|
|
uint32 pgid;
|
|
/* Real POSIX credential (uid(2)/setuid(2)): the identity vfs.c's
|
|
vfsPermitted consults, alongside a file's own owner uid and mode, to
|
|
decide owner-vs-other access at exactly the two enforcement choke
|
|
points fs.h's fsModeOwner and fsModeOther bits already gate
|
|
(resolveOpen's write check, loadImageByPath/jobctlSpawn's exec check) -- see
|
|
vfsPermitted's own doc comment for the full owner/other algorithm
|
|
(deliberately no uid-0-bypasses-everything superuser rule -- see that
|
|
same comment for why). Same creation-time default/copy split as pgid
|
|
immediately above (sched.c's new initialUid, mirroring initialPgid's
|
|
own "is the idle thread currently running" test): a boot-time thread
|
|
(kmain.c's spawnUser/threadCreate calls, idle currently "current")
|
|
starts as uid 0 ("root" in name only here, not in VFS privilege --
|
|
see fs.h's fsModeOwner* doc comment), exactly like a real init
|
|
process; a genuine runtime child (syscall.c's doSpawn) instead
|
|
INHERITS its parent's uid, matching real fork()'s own default before
|
|
any setuid ever runs. currentThreadSetUid (below) is the only thing
|
|
that ever changes it again after creation -- and, matching real
|
|
setuid(2)'s own privilege rule, only ever lets uid 0 become anyone;
|
|
any other uid may only "become" itself (a no-op), so dropping root is
|
|
real and permanent for the lifetime of this thread, exactly like a
|
|
real daemon that setuid()s away from root at startup and can never
|
|
regain it. */
|
|
uint32 uid;
|
|
/* Real POSIX group credential (getgid(2)/setgid(2)): the group identity
|
|
vfs.c's vfsPermitted consults, alongside a file's own group gid, as
|
|
the MIDDLE fallback between the owner and other checks fs.h's
|
|
fsModeGroup* bits gate. Same creation-time default/copy split as uid
|
|
immediately above (sched.c's new initialGid, mirroring initialUid's
|
|
own shape exactly): a boot-time thread starts at gid 0, a genuine
|
|
runtime child (syscall.c's doSpawn) INHERITS its parent's gid.
|
|
currentThreadSetGid (below) is the only thing that ever changes it
|
|
again -- unlike currentThreadSetUid, the privilege gate for THIS
|
|
call is the calling thread's own uid (currentThreadUid() == 0), not
|
|
its gid, matching real setgid(2): a privileged (root) PROCESS may
|
|
setgid to any group; an unprivileged one may only pass its own
|
|
current gid back in (a no-op), so dropping to a group is real and
|
|
permanent for the lifetime of a non-root thread, same shape as uid's
|
|
own one-way drop. */
|
|
uint32 gid;
|
|
/* Real per-process working directory (POSIX chdir(2)/getcwd(2)): always
|
|
a fully resolved absolute path (vfs.h's vfsChdir resolves a relative
|
|
argument against the CALLING thread's own cwd before ever storing
|
|
here, so this field itself never needs re-resolving against
|
|
anything). Set once, at creation time, by threadCreate/
|
|
threadCreateUser's own initialCwd helper (sched.c) -- a thread
|
|
created while the idle thread (id 0) is "current" (kmain.c's
|
|
boot-time spawns, see parentId's own comment above) starts at "/",
|
|
exactly like a real init process; a thread created by a genuinely
|
|
running parent (any runtime sysSpawn, syscall.c's doSpawn) instead
|
|
INHERITS a byte-for-byte COPY of that parent's cwd, matching real
|
|
fork()'s own copy-not-share semantics -- the two are independent
|
|
buffers from that point on, so a child's later chdir() can never
|
|
reach back and change its parent's, and vice versa. Before this
|
|
field existed, the only cwd concept anywhere in this kernel was a
|
|
single static local in shell.c shared by nothing else -- a spawned
|
|
user process calling open()/mkdir()/etc with a relative path had
|
|
never actually worked, since vfs.c's lookupNode/sfsLookup always
|
|
resolved relative to the filesystem root, never to any notion of
|
|
"current directory" (see vfs.c's vfsResolvePath, the only thing that
|
|
reads this field). */
|
|
char cwd[fsPathMax];
|
|
/* Real per-process environment variables (POSIX getenv(3)/setenv(3)):
|
|
up to envMaxVars "NAME=VALUE\0" strings, each at most envEntryMax-1
|
|
bytes, plus envCount tracking how many slots are actually in use.
|
|
Same creation-time default/copy split as Thread.cwd immediately
|
|
above (sched.c's new initialEnv, mirroring initialCwd's own shape):
|
|
a boot-time thread (idle is currently running) starts with an EMPTY
|
|
table, like a real init process before anything ever calls
|
|
setenv(); a genuine runtime child (syscall.c's doSpawn) instead
|
|
inherits a byte-for-byte COPY of its parent's table, so a child's
|
|
later setenv() can never reach back and change its parent's, and
|
|
vice versa -- the exact same independent-buffer guarantee cwd
|
|
already gives. */
|
|
char env[envMaxVars][envEntryMax];
|
|
int envCount;
|
|
ThreadState state;
|
|
/* Set once, permanently, the first time schedule() ever actually
|
|
switches execution to this thread. Distinguishes "freshly created,
|
|
never scheduled even once" (state threadRunnable, everRan false) from
|
|
"was running and got preempted, still Runnable" (everRan true) -- the
|
|
two look identical by state alone, but only the former is safe for
|
|
threadDiscard to tear down (see threadDiscard). */
|
|
bool everRan;
|
|
/* Fixed buffer, not a pointer: threadCreateUser/doSpawn (syscall.c) may
|
|
be called with a name that lives in a caller's stack-local buffer
|
|
(a path just copied in from userspace), which does not outlive the
|
|
call. thread->name must remain valid for the thread's entire life
|
|
(shell.c's `ps` listing and the exit log both read it long after
|
|
creation), so the bytes are always copied in, never aliased -- see
|
|
threadSetName/copyThreadName in sched.c. */
|
|
char name[threadNameMax];
|
|
const void* waitChannel;
|
|
/* Base virtual address of this thread's elfSlotSize-sized user-memory
|
|
slot (image + stack + brk-heap all live within
|
|
[userSlotBase, userSlotBase+elfSlotSize), see elf.h) -- 0 for kernel
|
|
threads (threadCreate), which own no user pages to give back.
|
|
Set from the entry point by threadCreateUser at spawn time and
|
|
updated by exec() (syscall.c doExec) when a thread's image moves to
|
|
a different slot. threadReap/threadDiscard pass this to
|
|
pagingUnmapUserRange so a dead or rolled-back process's physical
|
|
frames are actually reclaimed instead of leaking forever. */
|
|
uint64 userSlotBase;
|
|
PrioQNode schedNode;
|
|
FileDescriptor fds[maxOpenFiles];
|
|
/* mmap.h's own per-thread mapping table -- real demand-paged mmap(2)/
|
|
munmap(2), resolved lazily by isr.c's #PF handler through
|
|
mmapFaultHandler rather than eagerly mapped like pagingMapUserRange's
|
|
image/stack/brk pages. Lives inside the same [userSlotBase,
|
|
userSlotBase+elfSlotSize) slot as everything else this thread owns
|
|
(see mmap.h's own mmapReserve doc comment for exactly which sub-range),
|
|
so threadReap/threadDiscard's existing whole-slot
|
|
pagingUnmapUserRange already reclaims every frame a mapping here ever
|
|
faulted in with zero changes of their own -- this array only ever
|
|
needs its OWN bookkeeping cleared (mmapReleaseAll), which the
|
|
memset() both of those already do to the whole struct handles for
|
|
free. */
|
|
Vma vmas[maxVmasPerThread];
|
|
/* Signal state (signal.h/signal.c). Explicitly zeroed by sigResetState
|
|
from threadCreate/threadCreateUser below, the same way every other
|
|
field above is explicitly reset there too -- threadReap/threadDiscard
|
|
already memset() this whole struct before any slot is ever handed
|
|
back to allocSlot, so this is belt-and-suspenders, not a fix for a
|
|
leak that could otherwise happen (unlike keyboard.c's console-reader
|
|
table or pipe.c's refcounts, which live OUTSIDE Thread and genuinely
|
|
needed their own explicit reclaim -- see reclaimOpenFds). */
|
|
uint64 sigPending;
|
|
uint64 sigBlocked;
|
|
uint64 sigHandlers[sigMaxSignals];
|
|
/* Per-signal sa_mask (signal.h/signal.c): the ADDITIONAL signals to
|
|
block, alongside the one being delivered, for as long as
|
|
sigHandlers[signum]'s handler is running -- set by sigSetHandler,
|
|
consulted only by sigConsumePending's custom-handler branch. Index is
|
|
the signal being handled, not the signals it blocks (mirrors
|
|
sigHandlers[] being indexed by signal number, not by handler). */
|
|
uint64 sigHandlerMasks[sigMaxSignals];
|
|
/* Per-signal sa_flags (signal.h/signal.c) -- today only sigFlagRestart
|
|
(signalnum.h) is a defined bit, stored here exactly like
|
|
sigHandlerMasks is: set by sigSetHandler, indexed by the signal being
|
|
handled, consulted only by sigConsumePending when it is about to
|
|
decide whether an interrupted retryable syscall (see
|
|
sigInRetryableSyscall below) resumes transparently or returns
|
|
errInterrupted. */
|
|
uint64 sigHandlerFlags[sigMaxSignals];
|
|
/* SigInfo (signalnum.h) attached to whichever instance of signum is
|
|
CURRENTLY pending (index == signal number, same convention as
|
|
sigHandlers/sigHandlerMasks/sigHandlerFlags above) -- set by
|
|
sigRaiseInfo (signal.c) the moment sigPending's bit for it is set,
|
|
read (and copied into the shared user-visible page) by
|
|
sigConsumePending only if that signal's handler was registered with
|
|
sigFlagSigInfo. Like sigPending itself, this does not queue: raising
|
|
the same signum again before it is ever delivered overwrites this
|
|
with the newer detail, matching real Unix's own non-queuing standard
|
|
(non-realtime) signals, where only the most recent instance's info is
|
|
ever actually observable once more than one arrives uncollected. */
|
|
SigInfo sigPendingInfo[sigMaxSignals];
|
|
/* True exactly while this thread is parked mid the restart-syscall
|
|
pattern sysRead/sysWrite/sysWait/sysPoll (syscall.c) use to block: rip
|
|
has already been rewound by the interrupted `int $0x80`'s 2 bytes so
|
|
re-entering ring 3 re-executes the syscall from scratch. Set by
|
|
syscall.c immediately before each of those four calls parks and
|
|
reschedules; cleared unconditionally at the top of every
|
|
syscallDispatch call (a fresh dispatch, whether this is the retry
|
|
itself or a completely unrelated syscall, is never "still blocked" by
|
|
definition) AND by sigConsumePending the moment it actually acts on a
|
|
pending signal for this thread, win or lose -- a signal delivered
|
|
while this is false (ordinary running code, a sleep, or a nested
|
|
signal arriving inside an already-running handler) never touches
|
|
rip/rax at all, exactly as if sigFlagRestart's whole mechanism did
|
|
not exist, since there is no interrupted syscall retry to redirect. */
|
|
bool sigInRetryableSyscall;
|
|
/* How many handler contexts deep this thread currently is (0 = running
|
|
ordinary code, not inside any handler) -- sigSavedRegs[0..depth-1]/
|
|
sigSavedBlocked[0..depth-1] are the LIFO stack of interrupted contexts
|
|
sigOnSigReturn pops from, one per still-active handler, exactly like a
|
|
real call stack of signal frames. sigConsumePending refuses to push a
|
|
new one once this reaches sigMaxNestDepth. */
|
|
uint32 sigHandlerDepth;
|
|
uint64 sigSavedBlocked[sigMaxNestDepth];
|
|
Registers sigSavedRegs[sigMaxNestDepth];
|
|
} Thread;
|
|
|
|
void schedInit(void);
|
|
|
|
/* Allocates a slot from the fixed-size thread table (reusing the lowest
|
|
free slot left behind by threadReap before ever growing the high-water
|
|
mark) and initializes a new thread in it. Returns threadInvalidId,
|
|
touching nothing else, if the table is completely full (threadCapacity
|
|
threads already live) -- callers MUST check for this instead of assuming
|
|
the id is always valid. */
|
|
uint32 threadCreate(const char* name, ThreadEntry entry);
|
|
uint32 threadCreateUser(const char* name, uint64 entry, uint64 userStackTop);
|
|
uint64 schedule(uint64 currentRsp);
|
|
void schedStart(void);
|
|
|
|
/* Total number of thread-table slots (used and free). A fixed compile-time
|
|
constant, exposed so callers (and tests) can reason about exhaustion
|
|
without hardcoding the internal capacity. */
|
|
uint32 threadCapacity(void);
|
|
|
|
/* Releases thread `id`'s resources (kernel stack) and resets its slot back
|
|
to threadUnused so a later threadCreate/threadCreateUser can reuse it.
|
|
Only valid on a thread whose state is threadDead (i.e. it has already
|
|
called threadExit) -- reaping anything else (including the permanent
|
|
idle thread, a still-live thread, or an id that was never created / was
|
|
already reaped) is rejected and returns false with no effect. */
|
|
bool threadReap(uint32 id);
|
|
|
|
/* Tears down a thread that was created but has never actually been
|
|
scheduled (state threadRunnable and everRan false) -- e.g. rolling back a
|
|
spawn that failed a later setup step after threadCreate/threadCreateUser
|
|
already succeeded. Unlike threadReap this does not require the thread to
|
|
have exited: it requires the opposite, that it has not yet run even
|
|
once, since anything that has run may hold state (a partially unwound
|
|
stack, in-flight I/O) that is not safe to discard out from under it.
|
|
Returns false, changing nothing, if the thread has ever run, is not
|
|
currently Runnable (already sleeping/blocked/dead/unused), or id is out
|
|
of range. */
|
|
bool threadDiscard(uint32 id);
|
|
|
|
/* Blocks the calling thread (parking with schedBlockCurrent + halt(), the
|
|
same pattern pipeReadBlocking/pipeWriteBlocking use) until thread `id`
|
|
has called threadExit, then returns its exit code and reaps it (frees
|
|
the slot for reuse) in one step -- the same "wait for a child, get its
|
|
status, reclaim it" contract as a real wait()/join(). Returns -1
|
|
immediately, without blocking, for an invalid id (0, out of range, or a
|
|
slot that is not currently a live thread) rather than parking forever on
|
|
something that will never wake it. Joining the same id a second time (or
|
|
from two different callers) is not supported: the first joiner reaps the
|
|
slot, so a second joiner sees "no such thread" and gets -1. */
|
|
int64 threadJoin(uint32 id);
|
|
|
|
/* Non-blocking scan for waitpid(-1)-style "reap whichever child happens to
|
|
already be dead, without caring which specific pid" (sysWait's waitAnyPid
|
|
case, syscall.c/syscall.h; user/waitanytest.c is the live demo). Unlike
|
|
threadJoin above this never calls halt() itself -- it can be (and is)
|
|
invoked from inside the interrupt-context syscall handler, where parking
|
|
directly would wedge the whole machine (see sysWait's own comment on why
|
|
its specific-pid path does its own retry-syscall dance rather than just
|
|
calling threadJoin unconditionally); the caller is responsible for
|
|
actually blocking when this returns waitScanNoneDead.
|
|
Scans every live slot for one whose parentId is `parentId`:
|
|
- none exist at all (dead or alive) -> waitScanNoChildren, the real
|
|
waitpid(-1) ECHILD case -- there is nothing to ever wait for, so the
|
|
caller must not block.
|
|
- at least one exists but none are threadDead yet -> waitScanNoneDead.
|
|
- one is already threadDead -> waitScanFoundDead, with its own id and
|
|
exit code written to *outId and *outCode; it is already fully reaped
|
|
(threadReap already ran) by the time this returns, the same contract
|
|
threadJoin(id) has for an already-dead target. Picks whichever
|
|
matching dead slot has the lowest index if more than one child
|
|
happens to be dead at once -- a real waitpid(-1) makes no promise
|
|
about which one it returns in that case either. */
|
|
typedef enum { waitScanNoChildren, waitScanNoneDead, waitScanFoundDead } WaitScanResult;
|
|
WaitScanResult threadJoinAnyChild(uint32 parentId, uint32* outId, int64* outCode);
|
|
|
|
/* Terminates the CALLING thread with the given exit code (sysExit's own
|
|
underlying implementation, syscall.c, and isr.c's uncaught-fatal-signal
|
|
path). Also raises a real sigChld (signal.h/signal.c) against
|
|
Thread.parentId above -- asynchronous notification, on top of (not a
|
|
replacement for) threadJoin/sysWait's synchronous "block until this pid
|
|
is Dead" contract: a parent with a sigChld handler installed and
|
|
genuinely blocked elsewhere (see user/sigchldtest.c, which spawns
|
|
user/sigchldchild.c) is woken through the same early-wake path
|
|
threadWakeForSignal already gives any other signal, rather than only
|
|
finding out the next time it happens to call sysWait. Harmless no-op on
|
|
the sigRaise side if parentId is 0 (the idle thread, boot-time spawns'
|
|
universal parent, see parentId's own comment) or already dead/unused
|
|
(an orphan whose spawner is itself gone) -- sigRaise's own target
|
|
validity check (signal.c) is what makes that safe, not anything special
|
|
here. Also wakes the parent's OWN slot as a wait channel (on top of, not
|
|
instead of, this slot's own channel below) -- the parking spot sysWait's
|
|
waitAnyPid path (syscall.c) uses via schedBlockCurrent(currentThread()),
|
|
so a parent genuinely blocked in userWaitAny wakes the instant ANY of
|
|
its children exits, not just one it named specifically. */
|
|
void threadExit(uint64 code);
|
|
|
|
/* Exit code threadKill assigns its victim -- named after SIGKILL (9), the
|
|
nearest real-world equivalent, though this kernel has no signal delivery
|
|
at all: threadKill is a hard, unconditional termination, not something a
|
|
target thread can catch, mask, or clean up after (see threadKill). */
|
|
#define threadKilledExitCode 9u
|
|
|
|
/* Unconditionally terminates thread `id` from OUTSIDE it -- the only way to
|
|
ever get back a process that is genuinely stuck (e.g. blocked forever in
|
|
a syscall-level pipe read with no writer ever coming, now that
|
|
sysRead/sysWrite can really block, see pipe.h/vfs.h). Works on a thread
|
|
in any of Runnable/Sleeping/Blocked: its state is simply set straight to
|
|
threadDead with exitCode threadKilledExitCode, exactly as if it had
|
|
called threadExit(threadKilledExitCode) itself, and whoever is (or later
|
|
becomes) threadJoin()ing it wakes up and reaps it normally -- there is no
|
|
"half-dead" state and no cleanup callback, since a mid-syscall thread has
|
|
no user-mode cleanup path to run anyway. Returns false, changing nothing,
|
|
for id 0 (idle), an out-of-range id, an already-Dead/never-created slot,
|
|
or (implicitly, since only the CPU's one currently-executing thread is
|
|
ever threadRunning) the caller's own id -- a syscall handler always runs
|
|
as its OWN thread, which is Running, not Runnable/Sleeping/Blocked, at
|
|
the moment it would try to kill itself, so self-kill is naturally
|
|
rejected without a special-case check. Real termination (sysExit) is
|
|
still how a thread should ever end itself. Also raises sigChld against
|
|
the victim's parentId, exactly like threadExit does -- a killed child
|
|
is still a dead child as far as its parent's SIGCHLD is concerned, the
|
|
same way real kill(2)ing a child still generates SIGCHLD to its real
|
|
parent. Also wakes the victim's parent's OWN slot as a wait channel,
|
|
exactly like threadExit does -- a parent parked in userWaitAny does not
|
|
care whether a child exited on its own or was killed, same as real
|
|
waitpid(-1) does not distinguish the two either. */
|
|
bool threadKill(uint32 id);
|
|
|
|
/* Job control (signal.h): unconditionally moves thread `id` -- Runnable,
|
|
Running (including the caller itself, unlike threadKill which naturally
|
|
excludes self-targeting), Sleeping, or Blocked -- into threadStopped.
|
|
Idempotent if `id` is already Stopped (matches real kill(pid, SIGSTOP)
|
|
succeeding again on an already-stopped process). Safe on a
|
|
Sleeping/Blocked thread for the same reason threadKill already is: every
|
|
such thread is parked mid-retry-loop right after its own halt() (see
|
|
pipeReadBlocking/pipeWriteBlocking, or the restart-syscall pattern
|
|
syscall.c's sysRead/sysWrite/sysWait use), so simply not being picked by
|
|
schedule() again until threadContinue moves it back is enough -- no
|
|
wait-channel state is lost, since that retry loop re-checks its own
|
|
condition from scratch every time it runs at all. Returns false,
|
|
changing nothing, for id 0 (idle -- stopping it would wedge the whole
|
|
machine the instant nothing else was runnable), an out-of-range id, or
|
|
an already-Dead/never-created slot. */
|
|
bool threadStop(uint32 id);
|
|
|
|
/* Job control (signal.h): moves thread `id` from threadStopped back to
|
|
threadRunnable. Returns false, changing nothing, if `id` is out of
|
|
range or not currently Stopped (matches real SIGCONT delivered to a
|
|
process that was never stopped -- a harmless no-op at the process-state
|
|
level; signal.c's sigRaise still delivers the signal itself/runs a
|
|
handler regardless of this return value). */
|
|
bool threadContinue(uint32 id);
|
|
|
|
/* Real Unix's interruptible sleep/EINTR: forces thread `id` from
|
|
threadSleeping or threadBlocked straight to threadRunnable, early --
|
|
before its sleep timer expires or its wait-channel condition is ever
|
|
satisfied -- so the NEXT schedule() eventually resumes it, letting
|
|
isr.c's deliverPendingSignals (which only ever runs at a return-to-ring3
|
|
point) actually reach a thread that would otherwise never naturally
|
|
produce one. Called only by signal.c's sigRaise, and only for a signal it
|
|
has already determined would genuinely be delivered (not blocked, not
|
|
ignored) if this thread resumed right now -- callers must not use this to
|
|
wake a thread for an unrelated reason. Deliberately excludes
|
|
threadStopped (only threadContinue, an explicit SIGCONT, may ever move a
|
|
Stopped thread -- the same asymmetry schedWakeChannel already has, for
|
|
the same reason: job control's "stopped means stopped" must survive an
|
|
ordinary signal arriving too). Returns false, changing nothing, for an
|
|
out-of-range id or any state other than Sleeping/Blocked (Runnable/
|
|
Running need no forcing; Stopped/Dead/Unused have nothing to wake, or
|
|
must not be woken this way at all). */
|
|
bool threadWakeForSignal(uint32 id);
|
|
|
|
void threadSleep(uint64 milliseconds);
|
|
void threadSetBrk(uint32 id, uint64 brk);
|
|
uint64 currentThreadSbrk(int64 increment);
|
|
|
|
uint32 threadCount(void);
|
|
Thread* threadAt(uint32 index);
|
|
Thread* currentThread(void);
|
|
uint32 currentThreadId(void);
|
|
|
|
#endif
|