feat: extend signal
This commit is contained in:
1 parent
1ef1ab370a
commit
2b2252dac5
1 file changed
+217
@@ -149,3 +149,220 @@
|
|||||||
as much mapping as this needs. */
|
as much mapping as this needs. */
|
||||||
#define sigTrampolineVirtualAddr 0x0000700000000000ull
|
#define sigTrampolineVirtualAddr 0x0000700000000000ull
|
||||||
|
|
||||||
|
/* Fixed virtual address of the shared, read-only (never write, never
|
||||||
|
execute), user-accessible SigInfo (signalnum.h) data page(s) every
|
||||||
|
process's sigFlagSigInfo handlers are handed a pointer into --
|
||||||
|
sigInstallInfoPage maps it once at boot, right after the trampoline page
|
||||||
|
above, in the same reserved high range for the same reasons that page's
|
||||||
|
own comment already gives (nowhere near either the boot-time identity
|
||||||
|
map or any real per-binary elfSlotSize slot). One frameSize-sized page
|
||||||
|
holds frameSize/sizeof(SigInfo) slots; sigInstallInfoPage maps as many
|
||||||
|
consecutive pages, starting here, as threadCapacity()*sigMaxNestDepth
|
||||||
|
slots actually need (today: 96*4 = 384 slots, comfortably inside 2
|
||||||
|
pages) -- see sigConsumePending for how a (thread id, handler-nesting
|
||||||
|
depth) pair maps to one specific slot, the same indexing scheme
|
||||||
|
sigSavedRegs/sigSavedBlocked (sched.h) already use for the exact same
|
||||||
|
pair, just kept in a user-readable page here instead of Thread itself
|
||||||
|
since a handler running in ring 3 has no way to reach into kernel
|
||||||
|
memory. */
|
||||||
|
#define sigInfoVirtualAddr 0x0000700000010000ull
|
||||||
|
|
||||||
|
/* How many consecutive frameSize pages, starting at sigInfoVirtualAddr, a
|
||||||
|
fixed-size static array of physical frame addresses (signal.c) can hold
|
||||||
|
-- sigInstallInfoPage only ever maps as many as threadCapacity()*
|
||||||
|
sigMaxNestDepth slots actually need (2 today), so this is deliberately
|
||||||
|
generous headroom, not a tight fit: maxThreads (sched.c) has grown by a
|
||||||
|
fixed +8 roughly once per session for a long history of falas, and this
|
||||||
|
comfortably absorbs many more such bumps (8 pages * frameSize/sizeof
|
||||||
|
(SigInfo) slots-per-page / sigMaxNestDepth == room for hundreds of
|
||||||
|
threads) before ever needing to be revisited, the same "generous
|
||||||
|
headroom, not exact" reasoning sigMaxNestDepth's own comment (sched.h)
|
||||||
|
already gives for itself. */
|
||||||
|
#define sigInfoMaxPages 8
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
sigOutcomeNone = 0,
|
||||||
|
sigOutcomeHandler = 1,
|
||||||
|
sigOutcomeTerminate = 2,
|
||||||
|
/* Default disposition of a catchable job-control signal (SIGTSTP only
|
||||||
|
-- SIGSTOP never reaches sigConsumePending at all, see the file
|
||||||
|
comment). Same contract as sigOutcomeTerminate: this function only
|
||||||
|
REPORTS it, the caller (isr.c) must actually call threadStop and
|
||||||
|
reschedule, since only schedule() can produce the next thread's own
|
||||||
|
rsp to resume with. */
|
||||||
|
sigOutcomeStop = 3
|
||||||
|
} SigOutcome;
|
||||||
|
|
||||||
|
/* Resets ALL signal state on a Thread slot to fresh defaults: no pending or
|
||||||
|
blocked signals, no installed handlers or their masks, handler-nesting
|
||||||
|
depth zero. Called by threadCreate/threadCreateUser (sched.c) on every
|
||||||
|
(re)use of a table slot,
|
||||||
|
the same explicit-field-by-field convention threadCreate/threadCreateUser
|
||||||
|
already use for every other Thread field -- threadReap/threadDiscard's
|
||||||
|
own memset() of the whole struct already guarantees this before a slot
|
||||||
|
is ever reused, so this is defensive documentation of the invariant, not
|
||||||
|
a fix for a leak that could otherwise happen (unlike the console-reader/
|
||||||
|
pipe-fd leaks this subsystem's sibling code had to actually fix, which
|
||||||
|
lived in tables OUTSIDE Thread that no memset here ever touched). */
|
||||||
|
void sigResetState(Thread* thread);
|
||||||
|
|
||||||
|
/* Raises signal `signum` against thread `targetId`. sigKill and sigStop are
|
||||||
|
both delegated straight to threadKill/threadStop (see file comment) and
|
||||||
|
never touch sigPending at all. sigCont ALSO calls threadContinue as an
|
||||||
|
unconditional side effect (resuming a Stopped target regardless of
|
||||||
|
sigBlocked) but, unlike sigKill/sigStop, still falls through to set its
|
||||||
|
own pending bit afterwards, so an installed handler still runs through
|
||||||
|
the normal scan. Any other signum just sets the pending bit, then --
|
||||||
|
if sigWouldDeliver agrees this signal is not blocked or ignored --
|
||||||
|
calls threadWakeForSignal (sched.c) to force a Sleeping/Blocked target
|
||||||
|
straight to Runnable early, real Unix's interruptible sleep/EINTR (see
|
||||||
|
file comment); actual delivery itself still only ever happens later,
|
||||||
|
off the interrupt-return path (isr.c), once that thread genuinely
|
||||||
|
resumes. Returns false, changing nothing, for an out-of-range signum, an
|
||||||
|
invalid/out-of-range targetId, or a target that is Dead/Unused (mirrors
|
||||||
|
threadKill's own validation for the non-SIGKILL path, and threadKill's/
|
||||||
|
threadStop's own for their delegated paths). */
|
||||||
|
bool sigRaise(uint32 targetId, int signum);
|
||||||
|
|
||||||
|
/* Exactly sigRaise above, plus attaching a real SigInfo (signalnum.h) to
|
||||||
|
this specific raise -- `siCode`/`siPid`/`siStatus` become
|
||||||
|
target->sigPendingInfo[signum] (sched.h) the moment the pending bit
|
||||||
|
itself is set, later handed to a sigFlagSigInfo handler by
|
||||||
|
sigConsumePending. sigRaise itself is now a thin wrapper around this
|
||||||
|
(siCodeKernel, 0, 0 -- "no specific detail available/tracked", the
|
||||||
|
honest default for the many pre-existing callers, including sigTest.c's
|
||||||
|
whole KTEST suite, that only ever cared about signum/pending-bit
|
||||||
|
semantics and never plumbed real sender/child detail through). Real
|
||||||
|
production call sites needing genuine detail (sched.c's SIGALRM/SIGCHLD
|
||||||
|
raises, syscall.c's sysSigSend) call this directly instead. Same return
|
||||||
|
value, same validation, same sigKill/sigStop delegation, same early-wake
|
||||||
|
side effect as sigRaise -- see that function's own doc comment for all
|
||||||
|
of it, not repeated here. */
|
||||||
|
bool sigRaiseInfo(uint32 targetId, int signum, int siCode, int siPid, int siStatus);
|
||||||
|
|
||||||
|
/* Real kill(-pgid, signum)/kill(0, signum)'s group-targeted half (sched.h's
|
||||||
|
Thread.pgid; userSigSend/userKill's negative/zero-pid convention,
|
||||||
|
syscall.c's sysSigSend/sysKill) and keyboard.c's own real Ctrl-C/Ctrl-Z
|
||||||
|
(SIGINT/SIGTSTP delivered to the FOREGROUND process group only, never a
|
||||||
|
single target thread): scans every currently live thread once and calls
|
||||||
|
sigRaiseInfo above against each one whose pgid matches, independently --
|
||||||
|
SIGKILL/SIGSTOP/SIGCONT's own special-cased bypasses, per-target
|
||||||
|
early-wake, and SigInfo attachment therefore all apply exactly as they
|
||||||
|
already do for a plain, single-target send, simply repeated once per
|
||||||
|
group member, rather than this function sharing or reimplementing any of
|
||||||
|
that delivery logic itself. A pgid with no currently live member at all
|
||||||
|
(nobody has ever claimed the foreground yet, or every process that once
|
||||||
|
shared it has since died) is a harmless no-op, matching real kill(2)'s
|
||||||
|
ESRCH for an empty target set -- reported back as false, exactly like a
|
||||||
|
single-target sigRaiseInfo already reports false for an invalid/dead
|
||||||
|
target. */
|
||||||
|
bool sigRaiseGroup(uint32 pgid, int signum, int siCode, int siPid);
|
||||||
|
|
||||||
|
/* Installs `handlerAddr` (a real user-code address, or sigHandlerDefault /
|
||||||
|
sigHandlerIgnore) as thread's disposition for `signum`, `mask` as its
|
||||||
|
sa_mask -- the additional signals sigConsumePending blocks for as long as
|
||||||
|
THIS handler is active (stripped of sigKill/sigStop first, same as
|
||||||
|
sigProcMask: neither can ever be blocked, so a mask cannot "achieve" it
|
||||||
|
either) -- and `flags` (signalnum.h's sigFlagRestart, or 0) as its
|
||||||
|
sa_flags. Returns the PREVIOUS disposition (like real signal(2)/
|
||||||
|
sigaction(2)'s oldact->sa_handler) or sigSetFailed for an out-of-range
|
||||||
|
signum or an attempt to touch sigKill or sigStop (both uncatchable, like
|
||||||
|
real EINVAL on SIGKILL/SIGSTOP) -- `mask`/`flags` are stored regardless of
|
||||||
|
whether `handlerAddr` is a real handler or a sentinel, but only ever
|
||||||
|
consulted for a real one (sigHandlerDefault/sigHandlerIgnore never reach
|
||||||
|
the custom-handler branch of sigConsumePending). */
|
||||||
|
uint64 sigSetHandler(Thread* thread, int signum, uint64 handlerAddr, uint64 mask, uint64 flags);
|
||||||
|
|
||||||
|
/* Blocks/unblocks/replaces thread's blocked-signal mask according to `how`
|
||||||
|
(sigMaskBlock/sigMaskUnblock/sigMaskSetMask; any other value is a no-op),
|
||||||
|
returning the mask as it was BEFORE this call (like real sigprocmask's
|
||||||
|
oldset). sigKill's and sigStop's bits are silently stripped from `mask`
|
||||||
|
first -- neither can ever be blocked, matching real Unix (sigCont CAN be
|
||||||
|
blocked here, since only whether its handler runs is gated by
|
||||||
|
sigBlocked -- the resume side effect in sigRaise never consults it). */
|
||||||
|
uint64 sigProcMask(Thread* thread, int how, uint64 mask);
|
||||||
|
|
||||||
|
/* The delivery decision, called from isr.c's deliverPendingSignals on
|
||||||
|
every return-to-ring3 point for whichever thread is about to resume.
|
||||||
|
Refuses to deliver anything (returns sigOutcomeNone, touching nothing)
|
||||||
|
once thread->sigHandlerDepth already reached sigMaxNestDepth -- the small
|
||||||
|
fixed context stack is full, so whatever is pending simply waits for a
|
||||||
|
sigOnSigReturn to free a slot; this is the only depth-independent gate,
|
||||||
|
unlike the old single "one handler at a time" bool it replaced. Otherwise
|
||||||
|
scans sigPending & ~sigBlocked from signum 1 upward. An ignored signal
|
||||||
|
(explicit sigHandlerIgnore, or sigHandlerDefault whose real-world default
|
||||||
|
is Ignore -- see sigDefaultIsIgnore in signal.c) is dismissed and the
|
||||||
|
scan continues, so it can never mask a real one behind it. Otherwise:
|
||||||
|
default disposition -> sigOutcomeStop for sigTstp (sigDefaultIsStop) or
|
||||||
|
sigOutcomeTerminate for everything else (caller must actually stop/
|
||||||
|
terminate the thread, e.g. via threadStop/threadExit, and reschedule --
|
||||||
|
this function never does, since it has no way to hand back a new rsp).
|
||||||
|
Custom handler -> the interrupted context is pushed onto
|
||||||
|
thread->sigSavedRegs[depth]/sigSavedBlocked[depth] (depth being
|
||||||
|
sigHandlerDepth's value on entry, then incremented), sigBlocked becomes
|
||||||
|
the union of what it already was, sigHandlerMasks[signum] (the sa_mask
|
||||||
|
sigSetHandler stored), and signum's own bit (self-blocked, stripped of
|
||||||
|
sigKill/sigStop like every other mask operation in this file), and `regs`
|
||||||
|
is rewritten in place to enter the trampoline page (rip), call the
|
||||||
|
handler (rax) with signum as its sole argument (rdi) -- sigOutcomeHandler.
|
||||||
|
If sigHandlerFlags[signum] also has sigFlagSigInfo set, thread->
|
||||||
|
sigPendingInfo[signum] (whatever sigRaiseInfo last stashed there for this
|
||||||
|
signum, or sigRaise's own siCodeKernel/0/0 default) is additionally
|
||||||
|
copied into this (thread id, depth) pair's slot in the shared
|
||||||
|
sigInfoVirtualAddr page (signal.h), and rsi/rdx are rewritten too -- rsi
|
||||||
|
to that slot's real user-visible address, rdx to 0 (no ucontext_t this
|
||||||
|
kernel can expose) -- so the handler genuinely receives (signum,
|
||||||
|
SigInfo*, NULL) exactly as sigaction()'s SA_SIGINFO promises. Without the
|
||||||
|
flag, rsi/rdx are left completely untouched (whatever they already held),
|
||||||
|
exactly as before this existed, since a plain one-argument handler never
|
||||||
|
reads either. Before any of that pushing happens, thread->sigInRetryableSyscall is
|
||||||
|
consumed (read, then unconditionally cleared): if it was true (this
|
||||||
|
thread is genuinely mid a sysRead/sysWrite/sysWait retry, see sched.h)
|
||||||
|
and signum's sigHandlerFlags lacks sigFlagRestart, `regs` -- the
|
||||||
|
interrupted context about to be saved -- is rewritten first (rip past
|
||||||
|
the interrupted `int $0x80`, rax to errInterrupted) so the eventual
|
||||||
|
sigOnSigReturn resumes the caller having observed a genuine EINTR-style
|
||||||
|
failure instead of transparently retrying; sigFlagRestart (or
|
||||||
|
sigInRetryableSyscall already being false, e.g. a signal that interrupts
|
||||||
|
plain running code or a nested handler) leaves `regs` untouched here,
|
||||||
|
preserving today's transparent-retry behavior exactly.
|
||||||
|
A DIFFERENT signal not covered by that union can still be delivered on a
|
||||||
|
later call while this one is active -- pushed onto depth+1 as a genuinely
|
||||||
|
NESTED handler, sigOnSigReturn popping it back off once it returns, the
|
||||||
|
interrupted (still mid-handler) context resuming exactly where it was.
|
||||||
|
Either way `*outSignum` (if non-null) is set to whichever signal was
|
||||||
|
consumed. sigStop itself never appears here at all (see file comment --
|
||||||
|
sigRaise bypasses this whole scan for it). */
|
||||||
|
SigOutcome sigConsumePending(Thread* thread, Registers* regs, int* outSignum);
|
||||||
|
|
||||||
|
/* Undoes exactly what the sigOutcomeHandler branch of sigConsumePending did
|
||||||
|
for the MOST RECENTLY entered still-active handler (LIFO, like a real call
|
||||||
|
stack of signal frames): pops thread->sigHandlerDepth, restoring `*regs`
|
||||||
|
from thread->sigSavedRegs[depth] and thread->sigBlocked from
|
||||||
|
thread->sigSavedBlocked[depth] at the popped index -- resuming the
|
||||||
|
interrupted program (which may itself be a still-active OUTER handler, if
|
||||||
|
this was a nested one) at the exact instruction, with the exact
|
||||||
|
registers, it had before that signal arrived, as if it had never been
|
||||||
|
interrupted at all.
|
||||||
|
Returns false, changing nothing, if the thread was not actually inside a
|
||||||
|
handler (a user program calling the sigreturn syscall on its own, never
|
||||||
|
having been signaled, is a misuse this kernel simply ignores rather than
|
||||||
|
trusting). */
|
||||||
|
bool sigOnSigReturn(Thread* thread, Registers* regs);
|
||||||
|
|
||||||
|
/* Allocates one physical frame, fills it with the trampoline's machine
|
||||||
|
code (sigTrampolineStart..sigTrampolineEnd, sigTrampoline.S -- position
|
||||||
|
independent by construction, see that file), and maps it read+execute+
|
||||||
|
user (never write) at sigTrampolineVirtualAddr. Called exactly once from
|
||||||
|
kmain.c, before any user thread can possibly receive a signal. */
|
||||||
|
void sigInstallTrampoline(void);
|
||||||
|
|
||||||
|
/* Allocates and maps sigInfoVirtualAddr's data page(s) -- see that
|
||||||
|
constant's own comment for the layout and sizing. Called exactly once
|
||||||
|
from kmain.c, alongside sigInstallTrampoline (before any user thread can
|
||||||
|
possibly receive a sigFlagSigInfo delivery), and before ktestRunAll so
|
||||||
|
sigTest.c's own deterministic coverage can read the same shared page a
|
||||||
|
live ring-3 handler would. */
|
||||||
|
void sigInstallInfoPage(void);
|
||||||
|
|
||||||
|
#endif
|
||||||
Reference in new issue
Block a user