feat: expand sched
This commit is contained in:
1 parent
268707b571
commit
1a8cd39270
1 file changed
+162
+162
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user