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. */
|
||||
#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