feat: expand sched

This commit is contained in:
dualfroz committed 2026-09-01 21:53:24 +02:00
1 parent 268707b571
commit 1a8cd39270
1 file changed
+162
+162
View File
@@ -281,14 +281,176 @@ typedef struct Thread {
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);
void threadSleep(uint64 milliseconds);
void threadSetBrk(uint32 id, uint64 brk);