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;
|
uint64 userSlotBase;
|
||||||
PrioQNode schedNode;
|
PrioQNode schedNode;
|
||||||
FileDescriptor fds[maxOpenFiles];
|
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;
|
} Thread;
|
||||||
|
|
||||||
void schedInit(void);
|
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 threadCreate(const char* name, ThreadEntry entry);
|
||||||
uint32 threadCreateUser(const char* name, uint64 entry, uint64 userStackTop);
|
uint32 threadCreateUser(const char* name, uint64 entry, uint64 userStackTop);
|
||||||
uint64 schedule(uint64 currentRsp);
|
uint64 schedule(uint64 currentRsp);
|
||||||
void schedStart(void);
|
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 threadExit(uint64 code);
|
||||||
void threadSleep(uint64 milliseconds);
|
void threadSleep(uint64 milliseconds);
|
||||||
void threadSetBrk(uint32 id, uint64 brk);
|
void threadSetBrk(uint32 id, uint64 brk);
|
||||||
|
|||||||
Reference in new issue
Block a user