Files
frostbyte/src/sched.c
T

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;
}