1262 lines
54 KiB
C
1262 lines
54 KiB
C
#include "sched.h"
|
|
#include "isr.h"
|
|
#include "heap.h"
|
|
#include "string.h"
|
|
#include "ports.h"
|
|
#include "gdt.h"
|
|
#include "tss.h"
|
|
#include "pit.h"
|
|
#include "console.h"
|
|
#include "paging.h"
|
|
#include "vfs.h"
|
|
#include "prioq.h"
|
|
#include "elf.h"
|
|
#include "keyboard.h"
|
|
#include "pipe.h"
|
|
#include "pidfd.h"
|
|
#include "signal.h"
|
|
|
|
/* Bumped from 24: boot now spawns 22 kernel/user threads before the
|
|
scheduler even starts (kmain.c) -- several of which (workerA/workerB,
|
|
schedVerify, pipeProducer/pipeConsumer) park in an infinite halt() loop
|
|
rather than ever calling threadExit, so their slots are never freed for
|
|
reuse -- plus a handful more spawned at runtime by those boot programs
|
|
themselves (fifomaster's fiforeader/fifowriter, killtest's killvictim,
|
|
proctest's spawntarget/argtest) that can be alive at the same time as
|
|
their still-running parent. 24 was exactly enough before namedreader/
|
|
namedwriter's fifo-rendezvous demo added two more boot-time processes;
|
|
with them, every one of those later spawns (fifomaster's, killtest's,
|
|
proctest's own) started failing with the table genuinely full. The
|
|
free-slot reuse machinery below (allocSlot/threadReap) also needs
|
|
headroom above whatever boot needs to demonstrate exhaustion+reuse in
|
|
tests without permanently eating into threads real boot needs.
|
|
consolereader1/2+their injector and sigtest+its two short-lived,
|
|
promptly-reaped children (signal.h's demo) landed since without needing
|
|
another bump -- 32 held across dozens of stress runs, but with exactly
|
|
zero slots of headroom left over: boot-time spawns alone (kmain.c) had
|
|
already reached the full 32 the moment sigtest's own spawnUser call
|
|
returned, before any of killtest's/fifomaster's/proctest's own
|
|
runtime-spawned children (killvictim, fifowriter, spawntarget, argtest)
|
|
ever got a chance to exist. That razor-thin margin turned into a real,
|
|
observed race the moment jobtest+jobtarget (job control's live demo,
|
|
signal.h) landed as two more boot/runtime processes: every one of those
|
|
OTHER programs' children now had to wait for exactly one freed slot
|
|
(killvictim's, once threadReap'd) to fight over, and fifomaster's
|
|
fifowriter won that fight at the exact moment killtest was mid-way
|
|
through its own "kill an already-reaped pid must fail" check --
|
|
recycling killvictim's just-freed pid number into fifowriter's very much
|
|
alive one, so killtest's second userKill(victim) call genuinely
|
|
succeeded (against fifowriter, not "nothing") when it expected -1.
|
|
Not a scheduling-timing flake (see ci-test.sh's documented host-load
|
|
flakiness elsewhere): a deterministic consequence of table pressure,
|
|
reproduced identically across repeated runs until this bump. Restored
|
|
real headroom (bump to 40, not just the 2 jobtest/jobtarget need) so a
|
|
future addition does not immediately re-create the same zero-margin
|
|
situation. nesttest/nesttarget and eintrtest/eintrtarget (4 more
|
|
boot/runtime processes, signal.h's nested-delivery and early-wake demos)
|
|
fit under 40 with zero bump needed -- confirmed as the exact high-water
|
|
mark (`[sched] 40 threads total`), i.e. zero slots of headroom left
|
|
over again. restarttest/restarttarget (2 more, signal.h's SA_RESTART/
|
|
EINTR demo) would immediately hit the same razor-thin-margin class of
|
|
bug FALA 16's maxThreads bump above already fixed once, so bumped ahead
|
|
of that this time (40 -> 48) instead of waiting to observe it.
|
|
Confirmed as the exact high-water mark yet again once restarttest/
|
|
restarttarget actually landed (`[sched] 48 threads total`, checked live
|
|
before touching anything this fala) -- zero slots of headroom left over,
|
|
same story as 40 was before it. masktest/masktarget (2 more, signal.h's
|
|
sa_mask-defers-then-delivers live demo) would immediately re-create the
|
|
exact same class of bug, so bumped ahead of that again (48 -> 56)
|
|
instead of waiting to observe it, same reasoning as every bump above.
|
|
Confirmed as the exact high-water mark yet again once masktest/
|
|
masktarget landed (`[sched] 56 threads total`, checked live before
|
|
touching anything this fala) -- zero slots of headroom left over.
|
|
sigchldtest/sigchldchild (2 more, sched.c's new parentId/SIGCHLD-on-exit
|
|
live demo) would immediately re-create the exact same class of bug, so
|
|
bumped ahead of that again (56 -> 64) instead of waiting to observe it,
|
|
same reasoning as every bump above. Confirmed as the exact high-water
|
|
mark yet again once sigchldtest/sigchldchild landed (`[sched] 64 threads
|
|
total`, checked live before touching anything this fala) -- zero slots
|
|
of headroom left over. waitanytest/waitanyfast/waitanyslow (3 more,
|
|
sched.c's new threadJoinAnyChild/userWaitAny live demo) would
|
|
immediately re-create the exact same class of bug, so bumped ahead of
|
|
that again (64 -> 72) instead of waiting to observe it, same reasoning
|
|
as every bump above. Confirmed as the exact high-water mark yet again
|
|
once waitanytest/waitanyfast/waitanyslow landed (`[sched] 72 threads
|
|
total`, checked live before touching anything this fala) -- zero slots
|
|
of headroom left over. stopchldtest/stopchldtarget (2 more, sched.c's
|
|
new threadStop/threadContinue SIGCHLD-on-stop/SIGCHLD-on-continue live
|
|
demo) would immediately re-create the exact same class of bug, so
|
|
bumped ahead of that again (72 -> 80) instead of waiting to observe it,
|
|
same reasoning as every bump above. Confirmed as the exact high-water
|
|
mark yet again once stopchldtest/stopchldtarget landed (`[sched] 80
|
|
threads total`, checked live before touching anything this fala) --
|
|
zero slots of headroom left over. nodefertest (1 more, signal.h's new
|
|
sigFlagNoDefer live demo -- unlike every pair above, self-contained, no
|
|
spawned counterpart) would immediately re-create the exact same class
|
|
of bug, so bumped ahead of that again (80 -> 88) instead of waiting to
|
|
observe it, same reasoning as every bump above. alarmtest/alarmdefault
|
|
(2 more, sched.c's new threadSetAlarm/schedule()-expiry-scan real
|
|
SIGALRM live demo) would immediately re-create the exact same class of
|
|
bug, so bumped ahead of that again (88 -> 96) instead of waiting to
|
|
observe it, same reasoning as every bump above. siginfotest/
|
|
siginfotarget (2 more, signal.h's new sigFlagSigInfo/SigInfo live demo)
|
|
would immediately re-create the exact same class of bug, so bumped ahead
|
|
of that again (96 -> 104) instead of waiting to observe it, same
|
|
reasoning as every bump above. Confirmed as the exact high-water mark
|
|
yet again once siginfotest/siginfotarget landed (`[sched] 104 threads
|
|
total`, checked live before touching anything this fala) -- zero slots
|
|
of headroom left over. pgroupfg/pgroupbg (2 more, sched.c's new pgid/
|
|
process-group live demo, plus a third kernel-only injector thread,
|
|
kmain.c's pgroupInjectorThread) would immediately re-create the exact
|
|
same class of bug, so bumped ahead of that again (104 -> 112) instead of
|
|
waiting to observe it, same reasoning as every bump above. Confirmed as
|
|
the exact high-water mark yet again once pgroupfg/pgroupbg landed
|
|
(`[sched] 112 threads total`, checked live before touching anything this
|
|
fala) -- zero slots of headroom left over. jobctlfg/jobctlbg (2 more,
|
|
jobctl.c's new generic by-path spawn+real-job-control helper, loaded on
|
|
demand by the shell's own `NAME [ARGS] [&]` fallback rather than eagerly
|
|
at boot like everything above -- the first two user programs in this
|
|
kernel never once named in a kmain.c spawnUser call), plus a third
|
|
kernel-only driver thread (kmain.c's jobctlDriverThread, which types
|
|
commands at the real shell through the same injected-keystroke mechanism
|
|
pgroupInjectorThread already uses) would immediately re-create the exact
|
|
same class of bug, so bumped ahead of that again (112 -> 120) instead of
|
|
waiting to observe it, same reasoning as every bump above. polltest (1
|
|
more, boot-time driver for the new sysPoll multiplexed-fd wait) plus
|
|
pollwriter (1 more, runtime-spawned by polltest itself, mirroring
|
|
alarmtest/alarmdefault's shape) would immediately re-create the exact
|
|
same class of bug, so bumped ahead of that again (120 -> 128) instead of
|
|
waiting to observe it, same reasoning as every bump above. pipesrc/
|
|
pipesink (2 more, jobctl.c's new real shell pipeline demo, loaded on
|
|
demand exactly like jobctlfg/jobctlbg rather than boot-time spawnUser),
|
|
plus a third kernel-only driver thread (kmain.c's pipelineDriverThread,
|
|
which types the pipeline command at the real shell through the same
|
|
injected-keystroke mechanism jobctlDriverThread already uses) would
|
|
immediately re-create the exact same class of bug, so bumped ahead of
|
|
that again (128 -> 136) instead of waiting to observe it, same reasoning
|
|
as every bump above. redirwriter (1 more, jobctl.c's new file
|
|
redirection demo, run TWICE by kmain.c's redirDriverThread against the
|
|
same on-demand-loaded binary -- but each run is its own spawn+exit, so
|
|
only ONE of its process-table slots is ever live at a time) plus
|
|
redirreader (1 more, same on-demand-loading pattern as pipesrc/pipesink
|
|
above), plus a third kernel-only driver thread (kmain.c's
|
|
redirDriverThread, which types the redirected commands at the real
|
|
shell through the same injected-keystroke mechanism jobctlDriverThread/
|
|
pipelineDriverThread already use) would immediately re-create the exact
|
|
same class of bug, so bumped ahead of that again (136 -> 144) instead of
|
|
waiting to observe it, same reasoning as every bump above. stderrwriter
|
|
(1 more, jobctl.c's new stderr (fd 2) redirection demo, run TWICE by
|
|
kmain.c's stderrDriverThread against the same on-demand-loaded binary --
|
|
the exact same "only ONE of its process-table slots is ever live at a
|
|
time" shape redirwriter already has for `>`/`>>`) plus stderrreader (1
|
|
more, same on-demand-loading pattern as redirreader above), plus a third
|
|
kernel-only driver thread (kmain.c's stderrDriverThread, which types the
|
|
`2>`/`2>>`-redirected commands at the real shell through the same
|
|
injected-keystroke mechanism redirDriverThread already uses) would
|
|
immediately re-create the exact same class of bug, so bumped ahead of
|
|
that again (144 -> 152) instead of waiting to observe it, same reasoning
|
|
as every bump above. selfstoptest (boot-time spawned, self-directed
|
|
SIGSTOP live demo) plus selfstoptarget (1 more, runtime-spawned by
|
|
selfstoptest, the same *test / *target relationship jobtest/jobtarget
|
|
already have) are two more concurrently live threads, so bumped again
|
|
(152 -> 160) for the same headroom reason as every bump above. herestest
|
|
(1 more, jobctl.c's new here-string `<<<` stdin demo, on-demand-loaded
|
|
exactly like redirreader/stderrreader above -- no writer counterpart
|
|
binary at all, since the "writer" is the shell's own kernel thread, not
|
|
another spawned process) plus a third kernel-only driver thread (kmain.c's
|
|
hereDriverThread, which types the here-string command at the real shell
|
|
through the same injected-keystroke mechanism redirDriverThread/
|
|
stderrDriverThread already use) would immediately re-create the exact
|
|
same class of bug, so bumped ahead of that again (160 -> 168) instead of
|
|
waiting to observe it, same reasoning as every bump above. hcat (1 more,
|
|
jobctl.c's new jobctlSpawnPipedHereString demo -- a here-string stdin AND
|
|
a piped stdout on the same process at once, on-demand-loaded exactly like
|
|
herestest above) plus hsink (1 more, same on-demand-loading pattern as
|
|
pipesink above), plus a third kernel-only driver thread (kmain.c's
|
|
hereStringPipelineDriverThread, which types the here-string-into-pipeline
|
|
command at the real shell through the same injected-keystroke mechanism
|
|
pipelineDriverThread/hereDriverThread already use) would immediately
|
|
re-create the exact same class of bug, so bumped ahead of that again
|
|
(168 -> 176) instead of waiting to observe it, same reasoning as every
|
|
bump above. pipecat (1 more, shell.c's new runPipelineStages demo -- the
|
|
first process in this kernel with two DIFFERENT real inter-process pipes
|
|
on its own stdio at once, a genuine middle pipeline stage rather than a
|
|
here-string-plus-pipe like hcat above), plus a third kernel-only driver
|
|
thread (kmain.c's threeStagePipelineDriverThread, which types the
|
|
three-stage pipeline command at the real shell through the same
|
|
injected-keystroke mechanism pipelineDriverThread/hereStringPipelineDriverThread
|
|
already use) would immediately re-create the exact same class of bug, so
|
|
bumped ahead of that again (176 -> 184) instead of waiting to observe it,
|
|
same reasoning as every bump above. cwdtest (boot-time spawned, real
|
|
per-process working directory live demo -- sched.h's new Thread.cwd)
|
|
plus cwdtarget (1 more, runtime-spawned by cwdtest itself, the same
|
|
*test / *target relationship jobtest/jobtarget already have) are two
|
|
more concurrently live threads, so bumped again (184 -> 192) for the
|
|
same headroom reason as every bump above. envprint (loaded on demand by
|
|
the real shell, like jobctlfg/hcat/pipecat above, never boot-spawned)
|
|
plus a kernel-only driver thread (kmain.c's envDriverThread, which types
|
|
`export`/`envprint`/`env` at the real shell through the same
|
|
injected-keystroke mechanism every driver thread above already uses)
|
|
would immediately re-create the exact same class of bug, so bumped
|
|
ahead of that again (192 -> 200) instead of waiting to observe it, same
|
|
reasoning as every bump above. nicetest (boot-time spawned, real
|
|
userspace-triggered sysNice/sysGetPriority live demo) plus nicetarget
|
|
(1 more, runtime-spawned by nicetest itself while sleeping so it can be
|
|
reniced while blocked, the same *test / *target relationship
|
|
jobtest/jobtarget and cwdtest/cwdtarget already have) are two more
|
|
concurrently live threads, so bumped again (200 -> 208) for the same
|
|
headroom reason as every bump above. date (boot-time spawned, real
|
|
sysTime/rtc.c live demo, fully synchronous like nicetest with no
|
|
runtime-spawned partner of its own) is one more, so bumped again
|
|
(208 -> 216) for the same headroom reason as every bump above. timetest
|
|
(boot-time spawned, real SFS inode mtime/ctime live demo through userOpen/
|
|
userWrite/userStat/userLink, fully synchronous like nicetest/date with no
|
|
runtime-spawned partner) is one more, so bumped again (216 -> 224) for the
|
|
same headroom reason as every bump above. lsDriverThread (kmain.c, boot-
|
|
time kernel thread that types `mkdir`/`echo`/`ls -l` at the real shell
|
|
for shell.c's new generalized `ls` builtin) is one more, so bumped again
|
|
(224 -> 232) for the same headroom reason as every bump above.
|
|
andOrDriverThread (kmain.c, boot-time kernel thread that types
|
|
`false && echo X || echo ao2` at the real shell for shell.c's new
|
|
`&&`/`||` conditional chaining) is one more, so bumped again
|
|
(232 -> 240) for the same headroom reason as every bump above. rmtest
|
|
(boot-time spawned, real vfsRmdir/sysRmdir unlink(2)-vs-rmdir(2) split
|
|
live demo on both tmpfs and SFS, fully synchronous like nicetest/date/
|
|
timetest with no runtime-spawned partner) is one more, so bumped again
|
|
(240 -> 248) for the same headroom reason as every bump above. permtest
|
|
(boot-time spawned, real chmod(2)/owner-rwx-permission-bits live demo)
|
|
plus the one short-lived runtime-spawned copy of /bin/false it chmods and
|
|
spawns to prove the exec-permission check, are two more concurrently
|
|
live threads, so bumped again (248 -> 256) for the same headroom reason
|
|
as every bump above. dirpermtest (boot-time spawned, real directory-mode
|
|
enforcement live demo -- vfsMkdir/resolveOpen/vfsUnlink/vfsRmdir's new
|
|
parent-write and ancestor-search checks, lookupNode/lookupFrom's new
|
|
exec-on-traversal check -- fully synchronous like identtest with no
|
|
runtime-spawned partner) is one more, so bumped again (256 -> 264) for
|
|
the same headroom reason as every bump above. grouptest (boot-time
|
|
spawned, real group-credential/vfsPermitted-group-branch live demo plus
|
|
the two short-lived runtime-spawned copies of /bin/false it group-execs
|
|
to prove the exec-permission check across matching/mismatched groups)
|
|
are two more concurrently live threads, so bumped again (264 -> 272) for
|
|
the same headroom reason as every bump above. pidfdtest (boot-time
|
|
spawned, real pidfd.h live demo through sysPidfdOpen/sysPidfdSend/
|
|
sysPidfdWait) plus the one runtime-spawned copy of /bin/killvictim it
|
|
reuses as its target (same proven "genuinely stuck" shape killtest.c
|
|
already established) are two more concurrently live threads, so bumped
|
|
again (272 -> 280) for the same headroom reason as every bump above.
|
|
mmaptest (boot-time spawned, real mmap(2)/munmap(2) demand-paging live
|
|
demo through sysMmap/sysMunmap -- anonymous demand-zero, file-backed
|
|
read-through, and a real SIGSEGV proven against a PROT_READ-only
|
|
mapping) plus the one runtime-spawned copy of /bin/mmapprotvictim it
|
|
uses to prove that last case are two more concurrently live threads, so
|
|
bumped again (280 -> 288) for the same headroom reason as every bump
|
|
above. */
|
|
#define maxThreads 288
|
|
#define threadStackSize 16384
|
|
|
|
/* Virtual-runtime charge for one timer tick's worth of execution at
|
|
schedDefaultWeight; scaled by schedDefaultWeight/weight for other
|
|
weights (see threadSetPriority). Purely an internal unit -- only ever
|
|
compared against other vruntime values, never against wall-clock time. */
|
|
#define schedTickCharge 1000000ull
|
|
|
|
static Thread threads[maxThreads];
|
|
static uint32 threadTotal;
|
|
static uint32 currentIndex;
|
|
static bool schedulerActive;
|
|
|
|
/* allocSlot's own round-robin cursor -- see that function's doc comment
|
|
for why this exists at all, not just "the lowest free index" the way
|
|
threadTotal's own high-water-mark growth already reads left to right. */
|
|
static uint32 nextSlotHint = 1;
|
|
|
|
/* Thread.generation's own monotonic source (see that field's doc comment,
|
|
sched.h) -- never 0 (0 is reserved to mean "this slot is currently
|
|
unoccupied", the same convention pipe.c's nextPipeIncarnation already
|
|
uses for the identical reason), never reused, incremented once per
|
|
threadCreate/threadCreateUser regardless of which slot the new thread
|
|
lands in. */
|
|
static uint32 nextThreadGeneration = 1;
|
|
|
|
/* Bounded copy into a Thread's own name buffer (see threadNameMax /
|
|
sched.h) -- never aliases the caller's storage, since threadCreateUser
|
|
is called from syscall.c with a path that lives in a stack-local buffer
|
|
for the duration of one syscall. */
|
|
static void copyThreadName(Thread* thread, const char* name)
|
|
{
|
|
uint64 len = strlen(name);
|
|
if (len >= threadNameMax)
|
|
len = threadNameMax - 1;
|
|
memcpy(thread->name, name, len);
|
|
thread->name[len] = 0;
|
|
}
|
|
|
|
void threadSetName(uint32 id, const char* name)
|
|
{
|
|
if (id >= threadTotal)
|
|
return;
|
|
copyThreadName(&threads[id], name);
|
|
}
|
|
|
|
void schedInit(void)
|
|
{
|
|
threadTotal = 1;
|
|
currentIndex = 0;
|
|
|
|
threads[0].id = 0;
|
|
threads[0].parentId = 0;
|
|
/* Reserved, permanently: idle is never reaped/recreated, and pid 0 is
|
|
already rejected as a pidfd/kill/wait target everywhere else, so it
|
|
never needs a real incarnation of its own (see Thread.generation's
|
|
own doc comment). */
|
|
threads[0].generation = 0;
|
|
threads[0].pgid = 0;
|
|
threads[0].uid = 0;
|
|
threads[0].gid = 0;
|
|
threads[0].cwd[0] = '/';
|
|
threads[0].cwd[1] = 0;
|
|
threads[0].envCount = 0;
|
|
copyThreadName(&threads[0], "idle");
|
|
threads[0].state = threadRunning;
|
|
threads[0].rsp = 0;
|
|
threads[0].stackBase = 0;
|
|
threads[0].kernelStackTop = 0;
|
|
threads[0].sleepUntil = 0;
|
|
threads[0].brk = 0;
|
|
threads[0].vruntime = 0;
|
|
threads[0].exitCode = 0;
|
|
threads[0].weight = schedDefaultWeight;
|
|
threads[0].everRan = true;
|
|
threads[0].waitChannel = nullptr;
|
|
vfsFdInit(threads[0].fds);
|
|
}
|
|
|
|
/* Finds a slot for a new thread: a threadUnused slot left by a prior
|
|
threadReap, or (if none is free) grows the high-water mark by one as
|
|
long as there is room under maxThreads. Returns threadInvalidId,
|
|
touching nothing, if the table is completely full -- the caller must not
|
|
write into threads[] in that case (this is the bounds-check the original
|
|
version of this table was missing: threadTotal used to grow unconditionally
|
|
on every create, silently writing past the array once 16 threads existed).
|
|
Scans round-robin from nextSlotHint (wrapping through 1..threadTotal-1)
|
|
rather than always restarting from the lowest index: a real,
|
|
reproducibly observed bug this fala's own gate caught fresh on BOTH
|
|
killtest.c (a pre-existing, years-old check) and the new nicetest.c --
|
|
"kill/nice a pid this process itself just userWait()-reaped must fail"
|
|
occasionally failed for real, because some COMPLETELY UNRELATED thread
|
|
elsewhere in the system (in the reproduced case, jobctlfg being loaded
|
|
by the shell) called threadCreate/threadCreateUser in the tiny window
|
|
between the reap and the recheck and got handed that exact just-freed
|
|
LOW index straight back, since the old scan always tried index 1 first.
|
|
That is table pressure racing against a single process's own two
|
|
syscalls, not a scheduling-timing flake -- ci-test.sh's needle for it
|
|
(killtest: kill-ok) had simply never been unlucky enough to hit it
|
|
before this fala's elevated host load made the race common enough to
|
|
reproduce in a handful of runs. A round-robin cursor does not make the
|
|
race impossible (a table sustained at 100% occupancy for a full sweep
|
|
could still wrap back around to a freshly-freed slot), but with this
|
|
project's real, generous headroom margins (maxThreads far above actual
|
|
concurrent occupancy at any point in the historical record, sched.h's
|
|
own bump history) it means dozens-to-hundreds of OTHER allocations have
|
|
to happen before the cursor can return to any one just-freed slot --
|
|
the same practical-elimination-via-headroom shape every other table-
|
|
pressure fix in this file's history already has, not a new kind of
|
|
guarantee. A real generation/incarnation counter on pid itself (the
|
|
pipeIncarnation/sfsGeneration shape vfs.c/sfs.c already use for pipes
|
|
and the on-disk filesystem) would close it completely regardless of
|
|
occupancy, but reshaping what a "pid" IS everywhere kill/wait/sigSend/
|
|
setpgid/nice already accept it as a raw table index is the large,
|
|
deferred, cross-cutting change this project's own history has flagged
|
|
many times over (killtest.c's own doc comment, frostbyte-budowa.md) --
|
|
out of scope for this fix.
|
|
|
|
That generation counter now exists (Thread.generation, sched.h; this
|
|
file's own nextThreadGeneration below) -- but deliberately NOT wired
|
|
into kill/wait/sigSend/setpgid/nice's raw pid argument, matching real
|
|
Linux's own resolution of this exact, well-known race: pidfd_open(2)
|
|
added a SEPARATE, opt-in handle type rather than ever making a plain
|
|
pid argument itself generation-checked. See pidfd.h for the additive
|
|
primitive built on Thread.generation, and schedTest.c's pidfd tests for
|
|
a deterministic (create/discard/recreate, no live scheduling involved)
|
|
reproduction of exactly the stale-handle-hits-an-unrelated-live-thread
|
|
scenario killtest.c's own comment warns about but never guards against. */
|
|
static uint32 allocSlot(void)
|
|
{
|
|
if (threadTotal > 1) {
|
|
uint32 start = (nextSlotHint >= 1 && nextSlotHint < threadTotal) ? nextSlotHint : 1;
|
|
uint32 i = start;
|
|
do {
|
|
if (threads[i].state == threadUnused) {
|
|
nextSlotHint = i + 1;
|
|
return i;
|
|
}
|
|
i = (i + 1 < threadTotal) ? i + 1 : 1;
|
|
} while (i != start);
|
|
}
|
|
if (threadTotal < maxThreads) {
|
|
uint32 slot = threadTotal++;
|
|
nextSlotHint = slot + 1;
|
|
return slot;
|
|
}
|
|
return threadInvalidId;
|
|
}
|
|
|
|
/* Thread.pgid's own creation-time default (see that field's doc comment,
|
|
sched.h): a thread created while the idle thread (id 0) is "current" can
|
|
only be one of kmain.c's boot-time calls (parentId's own comment already
|
|
establishes idle never issues a syscall, so it can never be "current"
|
|
again once real processes exist) -- exactly like a real init spawning
|
|
each service into its own job, it becomes its own new group leader.
|
|
Anything else is a genuine runtime child (syscall.c's doSpawn) of a
|
|
currently-Running parent, and inherits that parent's pgid, matching real
|
|
fork()'s own default before any setpgid ever runs. */
|
|
static uint32 initialPgid(uint32 newId)
|
|
{
|
|
if (currentIndex == 0)
|
|
return newId;
|
|
return threads[currentIndex].pgid;
|
|
}
|
|
|
|
/* Thread.uid's own creation-time default, same "is the idle thread
|
|
currently running" test as initialPgid immediately above but with
|
|
pgid's own conclusion inverted: a boot-time thread starts as uid 0
|
|
("root"), like a real init process running as the superuser, rather than
|
|
becoming its own new anything; a genuine runtime child instead INHERITS
|
|
its parent's uid outright (not derived from newId at all), matching real
|
|
fork()'s own default before any setuid ever runs -- the same "copy the
|
|
parent's value" shape initialCwd/initialEnv below already use for their
|
|
own fields, just a scalar instead of a buffer so it returns a value
|
|
like initialPgid does. */
|
|
static uint32 initialUid(uint32 newId)
|
|
{
|
|
(void)newId;
|
|
if (currentIndex == 0)
|
|
return 0;
|
|
return threads[currentIndex].uid;
|
|
}
|
|
|
|
/* Thread.gid's own creation-time default, byte-for-byte the same shape as
|
|
initialUid immediately above: a boot-time thread starts at gid 0, a
|
|
genuine runtime child INHERITS its parent's gid outright. */
|
|
static uint32 initialGid(uint32 newId)
|
|
{
|
|
(void)newId;
|
|
if (currentIndex == 0)
|
|
return 0;
|
|
return threads[currentIndex].gid;
|
|
}
|
|
|
|
/* Thread.cwd's own creation-time default, mirroring initialPgid's own
|
|
reasoning immediately above exactly (same "is the idle thread currently
|
|
running" test, same conclusion for the same reason): a boot-time thread
|
|
starts at the filesystem root, like a real init process; a genuine
|
|
runtime child instead gets a byte-for-byte COPY of its parent's cwd,
|
|
matching real fork()'s own copy-not-share semantics -- writes into
|
|
threads[newId].cwd directly (rather than returning a value the way
|
|
initialPgid does) since a path is a buffer, not a scalar. */
|
|
static void initialCwd(uint32 newId)
|
|
{
|
|
if (currentIndex == 0) {
|
|
threads[newId].cwd[0] = '/';
|
|
threads[newId].cwd[1] = 0;
|
|
return;
|
|
}
|
|
memcpy(threads[newId].cwd, threads[currentIndex].cwd, fsPathMax);
|
|
}
|
|
|
|
/* Thread.env's own creation-time default, mirroring initialCwd's own
|
|
reasoning immediately above exactly (same "is the idle thread currently
|
|
running" test, same conclusion for the same reason): a boot-time thread
|
|
starts with an empty table, like a real init process before anything
|
|
ever calls setenv(); a genuine runtime child instead gets a byte-for-byte
|
|
COPY of its parent's whole table (both the entries and envCount), never
|
|
a shared pointer. */
|
|
static void initialEnv(uint32 newId)
|
|
{
|
|
if (currentIndex == 0) {
|
|
threads[newId].envCount = 0;
|
|
return;
|
|
}
|
|
memcpy(threads[newId].env, threads[currentIndex].env, sizeof(threads[newId].env));
|
|
threads[newId].envCount = threads[currentIndex].envCount;
|
|
}
|
|
|
|
uint32 threadCreate(const char* name, ThreadEntry entry)
|
|
{
|
|
uint32 slot = allocSlot();
|
|
if (slot == threadInvalidId)
|
|
return threadInvalidId;
|
|
|
|
uint64 stackBase = (uint64)kmalloc(threadStackSize);
|
|
uint64 stackTop = (stackBase + threadStackSize) & ~15ull;
|
|
|
|
Registers* frame = (Registers*)(stackTop - sizeof(Registers));
|
|
memset(frame, 0, sizeof(Registers));
|
|
frame->rip = (uint64)entry;
|
|
frame->cs = kernelCodeSelector;
|
|
frame->rflags = 0x202;
|
|
frame->rsp = stackTop;
|
|
frame->ss = kernelDataSelector;
|
|
|
|
threads[slot].id = slot;
|
|
threads[slot].generation = nextThreadGeneration++;
|
|
threads[slot].parentId = currentIndex;
|
|
threads[slot].pgid = initialPgid(slot);
|
|
threads[slot].uid = initialUid(slot);
|
|
threads[slot].gid = initialGid(slot);
|
|
initialCwd(slot);
|
|
initialEnv(slot);
|
|
copyThreadName(&threads[slot], name);
|
|
threads[slot].state = threadRunnable;
|
|
threads[slot].stackBase = stackBase;
|
|
threads[slot].kernelStackTop = stackTop;
|
|
threads[slot].rsp = (uint64)frame;
|
|
threads[slot].sleepUntil = 0;
|
|
threads[slot].alarmAt = 0;
|
|
threads[slot].pollActive = false;
|
|
threads[slot].pollDeadline = 0;
|
|
threads[slot].brk = 0;
|
|
threads[slot].vruntime = 0;
|
|
threads[slot].exitCode = 0;
|
|
threads[slot].weight = schedDefaultWeight;
|
|
threads[slot].everRan = false;
|
|
threads[slot].waitChannel = nullptr;
|
|
threads[slot].userSlotBase = 0;
|
|
vfsFdInit(threads[slot].fds);
|
|
sigResetState(&threads[slot]);
|
|
|
|
return slot;
|
|
}
|
|
|
|
uint32 threadCreateUser(const char* name, uint64 entry, uint64 userStackTop)
|
|
{
|
|
uint32 slot = allocSlot();
|
|
if (slot == threadInvalidId)
|
|
return threadInvalidId;
|
|
|
|
uint64 stackBase = (uint64)kmalloc(threadStackSize);
|
|
uint64 kernelStackTop = (stackBase + threadStackSize) & ~15ull;
|
|
|
|
Registers* frame = (Registers*)(kernelStackTop - sizeof(Registers));
|
|
memset(frame, 0, sizeof(Registers));
|
|
frame->rip = entry;
|
|
frame->cs = userCodeSelector | 3;
|
|
frame->rflags = 0x202;
|
|
frame->rsp = userStackTop;
|
|
frame->ss = userDataSelector | 3;
|
|
|
|
threads[slot].id = slot;
|
|
threads[slot].generation = nextThreadGeneration++;
|
|
threads[slot].parentId = currentIndex;
|
|
threads[slot].pgid = initialPgid(slot);
|
|
threads[slot].uid = initialUid(slot);
|
|
threads[slot].gid = initialGid(slot);
|
|
initialCwd(slot);
|
|
initialEnv(slot);
|
|
copyThreadName(&threads[slot], name);
|
|
threads[slot].state = threadRunnable;
|
|
threads[slot].stackBase = stackBase;
|
|
threads[slot].kernelStackTop = kernelStackTop;
|
|
threads[slot].rsp = (uint64)frame;
|
|
threads[slot].sleepUntil = 0;
|
|
threads[slot].alarmAt = 0;
|
|
threads[slot].pollActive = false;
|
|
threads[slot].pollDeadline = 0;
|
|
threads[slot].brk = 0;
|
|
threads[slot].vruntime = 0;
|
|
threads[slot].exitCode = 0;
|
|
threads[slot].weight = schedDefaultWeight;
|
|
threads[slot].everRan = false;
|
|
threads[slot].waitChannel = nullptr;
|
|
threads[slot].userSlotBase = elfSlotBase(entry);
|
|
vfsFdInit(threads[slot].fds);
|
|
sigResetState(&threads[slot]);
|
|
|
|
return slot;
|
|
}
|
|
|
|
/* CFS-lite selection: every reschedule rebuilds a min-vruntime priority
|
|
queue (prioq.h, backed by the intrusive rbt) from every currently
|
|
Runnable thread and picks the minimum. The queue is deliberately
|
|
ephemeral (built fresh each call, discarded after one pop) rather than a
|
|
run-queue whose membership persists across calls -- a thread can become
|
|
Runnable from code paths other than schedule() itself (schedWakeChannel,
|
|
called from pipe.c on the producer/consumer side of a wait-queue), and
|
|
rebuilding from the authoritative `state` field every time means there is
|
|
never a second copy of "who is runnable" to fall out of sync. */
|
|
uint64 schedule(uint64 currentRsp)
|
|
{
|
|
if (!schedulerActive)
|
|
return currentRsp;
|
|
|
|
Thread* current = &threads[currentIndex];
|
|
current->rsp = currentRsp;
|
|
if (current->state == threadRunning) {
|
|
current->vruntime += schedTickCharge * schedDefaultWeight / current->weight;
|
|
current->state = threadRunnable;
|
|
}
|
|
|
|
uint64 now = pitTicks();
|
|
for (uint32 i = 0; i < threadTotal; i++)
|
|
if (threads[i].state == threadSleeping && now >= threads[i].sleepUntil)
|
|
threads[i].state = threadRunnable;
|
|
|
|
/* sysPoll's own timeout, forced the exact same way sleepUntil's expiry
|
|
is above: a thread parked on schedPollChannel because none of its
|
|
requested fds were ready yet still needs to wake up on its own even
|
|
if NONE of them ever becomes ready (pollDeadline == 0 means "wait
|
|
forever", i.e. a real timeout of -1, and is deliberately never
|
|
forced here). The retried sysPoll call (syscall.c) re-checks every
|
|
fd from scratch on wake regardless of why it woke, so forcing
|
|
Runnable here without clearing waitChannel first (same as the
|
|
sleepUntil scan just above) is enough -- there is no separate
|
|
"why did I wake" state to reconcile. */
|
|
for (uint32 i = 0; i < threadTotal; i++)
|
|
if (threads[i].state == threadBlocked && threads[i].pollActive &&
|
|
threads[i].pollDeadline != 0 && now >= threads[i].pollDeadline)
|
|
threads[i].state = threadRunnable;
|
|
|
|
/* Real alarm(2) delivery: a deadline set by threadSetAlarm fires here,
|
|
once per timer tick, regardless of what state its owning thread is
|
|
currently in (Running, Sleeping, Blocked -- a real alarm keeps
|
|
ticking no matter what the process is doing). Cleared BEFORE
|
|
raising, not after, so one expiry fires sigAlrm exactly once, not
|
|
once per tick until something else clears it; sigRaiseInfo (signal.h)
|
|
already refuses a Dead/Unused target harmlessly, so a thread that
|
|
exited with an alarm still pending needs no special case here.
|
|
siCodeKernel/0/0: a real SIGALRM has no sending process and no exit
|
|
status of its own to report (real Unix's own SIGALRM si_code is
|
|
SI_KERNEL/SI_TIMER for exactly this reason -- it is the kernel's
|
|
timer, not another process, that raised it). */
|
|
for (uint32 i = 0; i < threadTotal; i++)
|
|
if (threads[i].alarmAt != 0 && now >= threads[i].alarmAt) {
|
|
threads[i].alarmAt = 0;
|
|
sigRaiseInfo(threads[i].id, sigAlrm, siCodeKernel, 0, 0);
|
|
}
|
|
|
|
PrioQueue ready;
|
|
prioqInit(&ready);
|
|
for (uint32 i = 0; i < threadTotal; i++)
|
|
if (threads[i].state == threadRunnable)
|
|
prioqInsert(&ready, &threads[i].schedNode, threads[i].vruntime, threads[i].id);
|
|
|
|
PrioQNode* winner = prioqPopMin(&ready);
|
|
uint32 next = winner ? winner->id : currentIndex;
|
|
|
|
currentIndex = next;
|
|
threads[next].state = threadRunning;
|
|
threads[next].everRan = true;
|
|
tssSetRsp0(threads[next].kernelStackTop);
|
|
return threads[next].rsp;
|
|
}
|
|
|
|
void schedStart(void)
|
|
{
|
|
schedulerActive = true;
|
|
enableInterrupts();
|
|
for (;;)
|
|
halt();
|
|
}
|
|
|
|
void threadExit(uint64 code)
|
|
{
|
|
Thread* thread = &threads[currentIndex];
|
|
thread->exitCode = code;
|
|
thread->state = threadDead;
|
|
kprintf("[user] pid=%u '%s' exited with code %lu\n", thread->id, thread->name, code);
|
|
|
|
/* Real, asynchronous SIGCHLD (signal.h/signal.c) against whoever
|
|
spawned this thread (Thread.parentId, sched.h) -- see threadExit's
|
|
own doc comment for why this matters beyond threadJoin/sysWait's
|
|
already-existing synchronous reap. sigRaiseInfo itself is what makes
|
|
this safe against parentId 0 (idle) or an already-gone parent.
|
|
siCodeChldExited/this thread's own id/code: real Unix's own
|
|
WIFEXITED(status) + WEXITSTATUS(status), exposed directly as
|
|
si_pid/si_status instead of a packed word a caller must decode. */
|
|
sigRaiseInfo(thread->parentId, sigChld, siCodeChldExited, (int)thread->id, (int)code);
|
|
|
|
/* Wake whichever thread (if any) is parked in threadJoin(thread->id) --
|
|
the channel is this slot's own address, matching threadJoin's
|
|
schedBlockCurrent(&threads[id]) below. Harmless no-op if nobody is
|
|
joining this thread. */
|
|
schedWakeChannel(thread);
|
|
|
|
/* Also wake whichever thread (if any) is parked waiting for ANY child
|
|
of thread->parentId (sysWait's waitAnyPid path, syscall.c, via
|
|
schedBlockCurrent(currentThread())) -- see threadExit's own doc
|
|
comment in sched.h. Channel is the PARENT's own slot address, not
|
|
this dying thread's, so it is naturally distinct from the wake above
|
|
and a harmless no-op if the parent never called userWaitAny. */
|
|
schedWakeChannel(&threads[thread->parentId]);
|
|
}
|
|
|
|
bool threadKill(uint32 id)
|
|
{
|
|
if (id == 0 || id >= maxThreads)
|
|
return false;
|
|
|
|
ThreadState state = threads[id].state;
|
|
if (state != threadRunnable && state != threadSleeping &&
|
|
state != threadBlocked && state != threadStopped)
|
|
return false;
|
|
|
|
threads[id].exitCode = threadKilledExitCode;
|
|
threads[id].state = threadDead;
|
|
kprintf("[sched] pid=%u '%s' killed\n", id, threads[id].name);
|
|
|
|
/* Same sigChld-to-parent contract as threadExit -- a killed child is
|
|
still a dead child as far as SIGCHLD is concerned (see threadKill's
|
|
own doc comment). siCodeChldKilled/id/threadKilledExitCode: real
|
|
Unix's own WIFSIGNALED(status) + WTERMSIG(status) would report the
|
|
actual killing signal number; this kernel's threadKill is not tied
|
|
to any one specific signal (SIGKILL's sigRaiseInfo delegates
|
|
straight here without ever reaching sigPendingInfo itself, and
|
|
userKill calls this directly with no signal at all) -- si_status
|
|
reports threadKilledExitCode instead, the exact same "killed" value
|
|
userWait already surfaces elsewhere in this kernel, not a new
|
|
fabricated number. */
|
|
sigRaiseInfo(threads[id].parentId, sigChld, siCodeChldKilled, (int)id, (int)threadKilledExitCode);
|
|
|
|
/* Same wake-the-joiner contract as threadExit -- a thread already
|
|
parked in threadJoin(id) (or the retry-syscall path in sysWait,
|
|
syscall.c) must not wait for its victim to "finish" some
|
|
in-progress work first, since a killed thread never gets to. */
|
|
schedWakeChannel(&threads[id]);
|
|
|
|
/* Same any-child wake as threadExit -- see threadKill's own doc
|
|
comment in sched.h. */
|
|
schedWakeChannel(&threads[threads[id].parentId]);
|
|
return true;
|
|
}
|
|
|
|
uint32 threadPgid(uint32 id)
|
|
{
|
|
if (id >= maxThreads || threads[id].state == threadUnused)
|
|
return threadInvalidId;
|
|
return threads[id].pgid;
|
|
}
|
|
|
|
bool threadSetPgid(uint32 id, uint32 pgid)
|
|
{
|
|
if (id == 0 || id >= maxThreads)
|
|
return false;
|
|
ThreadState state = threads[id].state;
|
|
if (state == threadDead || state == threadUnused)
|
|
return false;
|
|
|
|
threads[id].pgid = pgid;
|
|
return true;
|
|
}
|
|
|
|
bool threadKillGroup(uint32 pgid)
|
|
{
|
|
bool anyKilled = false;
|
|
for (uint32 id = 1; id < threadTotal; id++) {
|
|
if (threads[id].state == threadDead || threads[id].state == threadUnused)
|
|
continue;
|
|
if (threads[id].pgid != pgid)
|
|
continue;
|
|
if (threadKill(id))
|
|
anyKilled = true;
|
|
}
|
|
return anyKilled;
|
|
}
|
|
|
|
const char* currentThreadCwd(void)
|
|
{
|
|
return threads[currentIndex].cwd;
|
|
}
|
|
|
|
bool currentThreadSetCwd(const char* path)
|
|
{
|
|
uint64 len = strlen(path);
|
|
if (len >= fsPathMax)
|
|
return false;
|
|
memcpy(threads[currentIndex].cwd, path, len);
|
|
threads[currentIndex].cwd[len] = 0;
|
|
return true;
|
|
}
|
|
|
|
uint32 currentThreadUid(void)
|
|
{
|
|
return threads[currentIndex].uid;
|
|
}
|
|
|
|
bool currentThreadSetUid(uint32 newUid)
|
|
{
|
|
uint32 uid = threads[currentIndex].uid;
|
|
if (uid != 0 && newUid != uid)
|
|
return false;
|
|
threads[currentIndex].uid = newUid;
|
|
return true;
|
|
}
|
|
|
|
uint32 currentThreadGid(void)
|
|
{
|
|
return threads[currentIndex].gid;
|
|
}
|
|
|
|
bool currentThreadSetGid(uint32 newGid)
|
|
{
|
|
/* Privilege gate is this thread's own UID (root, CAP_SETGID-style), not
|
|
its current GID -- see sched.h's own doc comment for why that
|
|
mirrors real setgid(2) rather than currentThreadSetUid's shape. */
|
|
if (threads[currentIndex].uid != 0) {
|
|
uint32 gid = threads[currentIndex].gid;
|
|
if (newGid != gid)
|
|
return false;
|
|
}
|
|
threads[currentIndex].gid = newGid;
|
|
return true;
|
|
}
|
|
|
|
/* True if `entry` (a stored "NAME=VALUE" slot) names the same variable as
|
|
`name` -- i.e. `entry` starts with `name` followed immediately by '='.
|
|
Comparing this way (rather than strcmp against a rebuilt "NAME=" prefix)
|
|
needs no scratch buffer and stops at the first mismatching byte, same
|
|
spirit as string.h's own strncmp. */
|
|
static bool envNameMatches(const char* entry, const char* name)
|
|
{
|
|
while (*name) {
|
|
if (*entry != *name)
|
|
return false;
|
|
entry++;
|
|
name++;
|
|
}
|
|
return *entry == '=';
|
|
}
|
|
|
|
const char* currentThreadGetEnv(const char* name)
|
|
{
|
|
Thread* self = &threads[currentIndex];
|
|
for (int i = 0; i < self->envCount; i++) {
|
|
if (envNameMatches(self->env[i], name)) {
|
|
const char* value = self->env[i];
|
|
while (*value != '=')
|
|
value++;
|
|
return value + 1;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
bool currentThreadSetEnv(const char* name, const char* value)
|
|
{
|
|
Thread* self = &threads[currentIndex];
|
|
uint64 nameLen = strlen(name);
|
|
uint64 valueLen = strlen(value);
|
|
if (nameLen + 1 + valueLen + 1 > envEntryMax)
|
|
return false;
|
|
|
|
int slot = -1;
|
|
for (int i = 0; i < self->envCount; i++) {
|
|
if (envNameMatches(self->env[i], name)) {
|
|
slot = i;
|
|
break;
|
|
}
|
|
}
|
|
if (slot < 0) {
|
|
if (self->envCount >= envMaxVars)
|
|
return false;
|
|
slot = self->envCount++;
|
|
}
|
|
|
|
memcpy(self->env[slot], name, nameLen);
|
|
self->env[slot][nameLen] = '=';
|
|
memcpy(self->env[slot] + nameLen + 1, value, valueLen);
|
|
self->env[slot][nameLen + 1 + valueLen] = 0;
|
|
return true;
|
|
}
|
|
|
|
bool currentThreadUnsetEnv(const char* name)
|
|
{
|
|
Thread* self = &threads[currentIndex];
|
|
int slot = -1;
|
|
for (int i = 0; i < self->envCount; i++) {
|
|
if (envNameMatches(self->env[i], name)) {
|
|
slot = i;
|
|
break;
|
|
}
|
|
}
|
|
if (slot < 0)
|
|
return false;
|
|
|
|
int last = self->envCount - 1;
|
|
if (slot != last)
|
|
memmove(self->env[slot], self->env[slot + 1], (uint64)(last - slot) * envEntryMax);
|
|
self->envCount--;
|
|
return true;
|
|
}
|
|
|
|
int currentThreadEnvCount(void)
|
|
{
|
|
return threads[currentIndex].envCount;
|
|
}
|
|
|
|
const char* currentThreadEnvEntry(int index)
|
|
{
|
|
if (index < 0 || index >= threads[currentIndex].envCount)
|
|
return nullptr;
|
|
return threads[currentIndex].env[index];
|
|
}
|
|
|
|
bool threadStop(uint32 id)
|
|
{
|
|
if (id == 0 || id >= maxThreads)
|
|
return false;
|
|
|
|
ThreadState state = threads[id].state;
|
|
if (state == threadDead || state == threadUnused)
|
|
return false;
|
|
if (state == threadStopped) {
|
|
/* Idempotent, matching real kill(pid, SIGSTOP) sent to an already-
|
|
stopped process: the state transition already happened, so there
|
|
is no new event for the parent's SIGCHLD to report -- raising
|
|
again here would be a second notification for a single stop. */
|
|
return true;
|
|
}
|
|
|
|
threads[id].state = threadStopped;
|
|
kprintf("[sched] pid=%u '%s' stopped\n", id, threads[id].name);
|
|
|
|
/* Real job-control SIGCHLD-on-stop, the same asynchronous notification
|
|
threadExit/threadKill above already raise on death (Thread.parentId,
|
|
signal.h's sigRaiseInfo) -- a shell watching a background job's
|
|
parentId finds out the instant it stops, not only when it eventually
|
|
dies or exits. Only reached on the actual ...->Stopped transition
|
|
above, never on the already-Stopped no-op return just above.
|
|
siCodeChldStopped/id/sigStop: real Unix's WIFSTOPPED(status) +
|
|
WSTOPSIG(status) would report whichever signal actually caused the
|
|
stop -- this function is called from exactly two places (signal.c's
|
|
sigRaise sigStop branch, a real SIGSTOP; isr.c's sigTstp-default-
|
|
disposition branch, a real SIGTSTP) and does not itself know which
|
|
one, so si_status always reports sigStop here regardless. A known,
|
|
documented imprecision for the SIGTSTP path (which also has no live
|
|
ring-3 demo of its own yet -- see signal.h's file comment) -- not a
|
|
fabricated distinction either path never had. */
|
|
sigRaiseInfo(threads[id].parentId, sigChld, siCodeChldStopped, (int)id, sigStop);
|
|
return true;
|
|
}
|
|
|
|
bool threadContinue(uint32 id)
|
|
{
|
|
if (id == 0 || id >= maxThreads)
|
|
return false;
|
|
if (threads[id].state != threadStopped)
|
|
return false;
|
|
|
|
threads[id].state = threadRunnable;
|
|
kprintf("[sched] pid=%u '%s' continued\n", id, threads[id].name);
|
|
|
|
/* Same job-control SIGCHLD contract as threadStop above, mirrored for
|
|
SIGCONT: real job-control shells rely on this to notice a
|
|
backgrounded job resumed without polling its state. Only reached
|
|
past the "was it actually Stopped" guard above, so this never fires
|
|
for a SIGCONT sent to a target that was never stopped in the first
|
|
place, matching real Unix (nothing to report). siCodeChldContinued/
|
|
id/sigCont: real Unix's WIFCONTINUED(status), si_status unused for
|
|
this one (POSIX defines none for CLD_CONTINUED either) -- sigCont
|
|
here is honest, not a placeholder, unlike threadStop's own sigStop
|
|
above: threadContinue really is only ever reached via a genuine
|
|
SIGCONT (signal.c's sigRaise sigCont branch is its only caller with
|
|
a live path to a Stopped target). */
|
|
sigRaiseInfo(threads[id].parentId, sigChld, siCodeChldContinued, (int)id, sigCont);
|
|
return true;
|
|
}
|
|
|
|
bool threadWakeForSignal(uint32 id)
|
|
{
|
|
if (id == 0 || id >= maxThreads)
|
|
return false;
|
|
|
|
ThreadState state = threads[id].state;
|
|
if (state != threadSleeping && state != threadBlocked)
|
|
return false;
|
|
|
|
threads[id].state = threadRunnable;
|
|
kprintf("[sig] pid=%u '%s' woken early for a signal\n", id, threads[id].name);
|
|
return true;
|
|
}
|
|
|
|
uint32 threadCapacity(void)
|
|
{
|
|
return maxThreads;
|
|
}
|
|
|
|
/* Gives a dead/discarded user thread's whole elfSlotSize slot back to the
|
|
pmm. Verified, not just invoked blindly: a byte inside the slot's stack
|
|
region (always mapped for any user thread, whatever its image looks
|
|
like -- see loadImageByPath/spawnUser) is checked present BEFORE the
|
|
unmap (proving there was really something to reclaim, not a no-op on an
|
|
already-empty slot) and absent AFTER (proving the unmap actually took
|
|
the frame back), logged so the boot gate can hold the wiring itself
|
|
accountable rather than trusting that calling pagingUnmapUserRange was
|
|
enough. Kernel threads (userSlotBase 0) are silently skipped. */
|
|
static void reclaimUserSlot(uint32 id, const Thread* thread)
|
|
{
|
|
if (thread->userSlotBase == 0)
|
|
return;
|
|
|
|
uint64 slotBase = thread->userSlotBase;
|
|
uint64 stackProbe = slotBase + elfSlotStackOffset - 1;
|
|
bool wasMapped = pagingResolve(stackProbe) != 0;
|
|
|
|
pagingUnmapUserRange(slotBase, elfSlotSize);
|
|
|
|
bool nowFreed = pagingResolve(stackProbe) == 0;
|
|
kprintf("[sched] reap pid=%u freed user slot %p (was-mapped=%s now-freed=%s) -> %s\n",
|
|
id, (void*)slotBase, wasMapped ? "yes" : "no", nowFreed ? "yes" : "no",
|
|
(wasMapped && nowFreed) ? "user-mem-reclaimed-ok" : "user-mem-reclaimed-FAIL");
|
|
}
|
|
|
|
/* Every open fd a thread still holds when it dies represents a live
|
|
reference into some other subsystem's own refcounted table -- fd 0's
|
|
console reader queue (keyboard.h, allocated unconditionally by vfsFdInit
|
|
regardless of whether the thread ever reads stdin, see vfs.c) and any
|
|
fdPipeRead/fdPipeWrite fd (pipe.h's readers/writers counts on the
|
|
underlying handle, from vfsPipe/vfsPipeAttach/a named fifo's vfsOpen).
|
|
vfsClose already decrements both correctly, but only for a LIVE thread
|
|
closing its OWN fd through currentThread() -- a thread that dies
|
|
(threadKill, or simply returns/threadExit) while still holding either
|
|
open skips vfsClose entirely, so without this the memset below would
|
|
just drop the handle on the floor.
|
|
For the console reader this was caught live (FALA 13): the reader table
|
|
is a small fixed pool (maxConsoleReaders), and schedTest.c alone creates
|
|
and tears down dozens of throwaway threads while proving the sched table
|
|
itself reuses freed slots (threadCreateExhaustsThenThreadDiscardFrees...),
|
|
which permanently exhausted the ENTIRELY SEPARATE console reader pool
|
|
before boot ever reached kmain.c's real spawnUser calls -- every
|
|
boot-time process's fd 0 silently went non-blocking (keyboardOpenReader
|
|
returning consoleReaderInvalid), so consolereader1.c/consolereader2.c's
|
|
userRead(0, ...) returned -1 immediately instead of blocking for the
|
|
synthetic keystroke.
|
|
The pipe/fifo case is the same shape of leak, just against pipe.c's own
|
|
readers/writers counters instead of a fixed pool: the OTHER end of a
|
|
shared pipe (a second, independent process, e.g. fifomaster.c's
|
|
children) would never see the true EOF/broken-pipe pipeRead/pipeWrite
|
|
report once every attached end on this side is gone, and would instead
|
|
retry "try again" forever -- exactly the resource-exhaustion shape
|
|
bug#2 above had, just not yet observed breaking anything live because
|
|
nothing today loses a pipe/fifo fd without an orderly close() first (see
|
|
schedTest.c's reclaimOpenFds tests, which prove the leak by fault
|
|
injection rather than waiting for a live symptom). */
|
|
static void reclaimOpenFds(const Thread* thread)
|
|
{
|
|
for (uint32 i = 0; i < maxOpenFiles; i++) {
|
|
const FileDescriptor* fd = &thread->fds[i];
|
|
if (fd->kind == fdConsole && fd->pipeHandle >= 0)
|
|
keyboardCloseReader(fd->pipeHandle);
|
|
else if (fd->kind == fdPipeRead)
|
|
pipeCloseRead(fd->pipeHandle);
|
|
else if (fd->kind == fdPipeWrite)
|
|
pipeCloseWrite(fd->pipeHandle);
|
|
else if (fd->kind == fdPidfd)
|
|
pidfdClose(fd->pipeHandle);
|
|
}
|
|
}
|
|
|
|
bool threadReap(uint32 id)
|
|
{
|
|
if (id == 0 || id >= maxThreads)
|
|
return false;
|
|
if (threads[id].state != threadDead)
|
|
return false;
|
|
|
|
reclaimUserSlot(id, &threads[id]);
|
|
reclaimOpenFds(&threads[id]);
|
|
if (threads[id].stackBase)
|
|
kfree((void*)threads[id].stackBase);
|
|
memset(&threads[id], 0, sizeof(Thread));
|
|
/* memset leaves state at 0, which is threadUnused -- exactly the value
|
|
allocSlot() scans for when looking for a slot to reuse. */
|
|
return true;
|
|
}
|
|
|
|
bool threadDiscard(uint32 id)
|
|
{
|
|
if (id == 0 || id >= maxThreads)
|
|
return false;
|
|
if (threads[id].state != threadRunnable || threads[id].everRan)
|
|
return false;
|
|
|
|
if (threads[id].userSlotBase != 0)
|
|
pagingUnmapUserRange(threads[id].userSlotBase, elfSlotSize);
|
|
reclaimOpenFds(&threads[id]);
|
|
if (threads[id].stackBase)
|
|
kfree((void*)threads[id].stackBase);
|
|
memset(&threads[id], 0, sizeof(Thread));
|
|
return true;
|
|
}
|
|
|
|
int64 threadJoin(uint32 id)
|
|
{
|
|
if (id == 0 || id >= maxThreads)
|
|
return -1;
|
|
|
|
for (;;) {
|
|
ThreadState state = threads[id].state;
|
|
if (state == threadUnused)
|
|
return -1;
|
|
if (state == threadDead)
|
|
break;
|
|
schedBlockCurrent(&threads[id]);
|
|
halt();
|
|
}
|
|
|
|
uint64 code = threads[id].exitCode;
|
|
threadReap(id);
|
|
return (int64)code;
|
|
}
|
|
|
|
WaitScanResult threadJoinAnyChild(uint32 parentId, uint32* outId, int64* outCode)
|
|
{
|
|
bool hasChild = false;
|
|
for (uint32 i = 1; i < threadTotal; i++) {
|
|
if (threads[i].state == threadUnused || threads[i].parentId != parentId)
|
|
continue;
|
|
hasChild = true;
|
|
if (threads[i].state == threadDead) {
|
|
*outId = i;
|
|
*outCode = (int64)threads[i].exitCode;
|
|
threadReap(i);
|
|
return waitScanFoundDead;
|
|
}
|
|
}
|
|
return hasChild ? waitScanNoneDead : waitScanNoChildren;
|
|
}
|
|
|
|
void threadSleep(uint64 milliseconds)
|
|
{
|
|
uint64 ticks = milliseconds * pitFrequency() / 1000;
|
|
if (ticks == 0)
|
|
ticks = 1;
|
|
threads[currentIndex].sleepUntil = pitTicks() + ticks;
|
|
threads[currentIndex].state = threadSleeping;
|
|
}
|
|
|
|
uint64 threadSetAlarm(uint64 seconds)
|
|
{
|
|
Thread* self = &threads[currentIndex];
|
|
uint64 now = pitTicks();
|
|
|
|
uint64 previousRemaining = 0;
|
|
if (self->alarmAt != 0 && self->alarmAt > now) {
|
|
uint64 remainingTicks = self->alarmAt - now;
|
|
uint32 freq = pitFrequency();
|
|
/* Ceiling division: a deadline anywhere inside the current second
|
|
still counts as "1 second remaining", never "0" -- matching real
|
|
alarm()'s own contract that a not-yet-fired alarm never reports
|
|
0 remaining (0 is reserved for "none pending" everywhere in this
|
|
API, see the header comment). */
|
|
previousRemaining = (remainingTicks + freq - 1) / freq;
|
|
}
|
|
|
|
if (seconds == 0)
|
|
self->alarmAt = 0;
|
|
else
|
|
self->alarmAt = now + seconds * (uint64)pitFrequency();
|
|
|
|
return previousRemaining;
|
|
}
|
|
|
|
void schedBlockCurrent(const void* channel)
|
|
{
|
|
threads[currentIndex].waitChannel = channel;
|
|
threads[currentIndex].state = threadBlocked;
|
|
}
|
|
|
|
const void* schedPollChannel(void)
|
|
{
|
|
static const int pollChannelToken;
|
|
return (const void*)&pollChannelToken;
|
|
}
|
|
|
|
void schedWakeChannel(const void* channel)
|
|
{
|
|
for (uint32 i = 0; i < threadTotal; i++) {
|
|
if (threads[i].state == threadBlocked && threads[i].waitChannel == channel) {
|
|
threads[i].waitChannel = nullptr;
|
|
threads[i].state = threadRunnable;
|
|
}
|
|
}
|
|
}
|
|
|
|
void threadSetBrk(uint32 id, uint64 brk)
|
|
{
|
|
if (id >= maxThreads)
|
|
return;
|
|
threads[id].brk = brk;
|
|
}
|
|
|
|
uint64 threadUserSlotBase(uint32 id)
|
|
{
|
|
if (id >= maxThreads)
|
|
return 0;
|
|
return threads[id].userSlotBase;
|
|
}
|
|
|
|
void threadSetUserSlotBase(uint32 id, uint64 base)
|
|
{
|
|
if (id >= maxThreads)
|
|
return;
|
|
threads[id].userSlotBase = base;
|
|
}
|
|
|
|
bool threadSetPriority(uint32 id, uint32 weight)
|
|
{
|
|
if (id == 0 || id >= maxThreads || weight == 0)
|
|
return false;
|
|
ThreadState state = threads[id].state;
|
|
if (state == threadDead || state == threadUnused)
|
|
return false;
|
|
|
|
threads[id].weight = weight;
|
|
return true;
|
|
}
|
|
|
|
uint32 threadPriority(uint32 id)
|
|
{
|
|
if (id >= maxThreads || threads[id].state == threadUnused)
|
|
return 0;
|
|
return threads[id].weight;
|
|
}
|
|
|
|
uint64 threadVruntime(uint32 id)
|
|
{
|
|
if (id >= threadTotal)
|
|
return 0;
|
|
return threads[id].vruntime;
|
|
}
|
|
|
|
uint64 currentThreadSbrk(int64 increment)
|
|
{
|
|
Thread* thread = &threads[currentIndex];
|
|
uint64 previous = thread->brk;
|
|
|
|
if (increment > 0)
|
|
pagingMapUserRangeFlags(previous, (uint64)increment, pageNoExecute);
|
|
|
|
thread->brk = previous + (uint64)increment;
|
|
return previous;
|
|
}
|
|
|
|
uint32 threadCount(void)
|
|
{
|
|
return threadTotal;
|
|
}
|
|
|
|
Thread* threadAt(uint32 index)
|
|
{
|
|
return &threads[index];
|
|
}
|
|
|
|
Thread* currentThread(void)
|
|
{
|
|
return &threads[currentIndex];
|
|
}
|
|
|
|
uint32 currentThreadId(void)
|
|
{
|
|
return threads[currentIndex].id;
|
|
}
|