From 1a8cd39270b3ec064cb741117ab6402a7b595c15 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 1 Sep 2026 21:53:24 +0200 Subject: [PATCH] feat: expand sched --- include/sched.h | 162 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) diff --git a/include/sched.h b/include/sched.h index 68a6abe..1641638 100644 --- a/include/sched.h +++ b/include/sched.h @@ -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);