feat: extend sched with allocSlot and initialPgid

This commit is contained in:
dualfroz committed 2026-09-06 21:03:53 +02:00
1 parent ae6bdd5880
commit 9d356d9a29
1 file changed
+192 -6
+192 -6
View File
@@ -331,10 +331,161 @@ void schedInit(void)
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 = threadTotal;
threadTotal++;
uint32 slot = allocSlot();
if (slot == threadInvalidId)
return threadInvalidId;
uint64 stackBase = (uint64)kmalloc(threadStackSize);
uint64 stackTop = (stackBase + threadStackSize) & ~15ull;
@@ -348,22 +499,40 @@ uint32 threadCreate(const char* name, ThreadEntry entry)
frame->ss = kernelDataSelector;
threads[slot].id = slot;
threads[slot].name = name;
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 = threadTotal;
threadTotal++;
uint32 slot = allocSlot();
if (slot == threadInvalidId)
return threadInvalidId;
uint64 stackBase = (uint64)kmalloc(threadStackSize);
uint64 kernelStackTop = (stackBase + threadStackSize) & ~15ull;
@@ -377,14 +546,31 @@ uint32 threadCreateUser(const char* name, uint64 entry, uint64 userStackTop)
frame->ss = userDataSelector | 3;
threads[slot].id = slot;
threads[slot].name = name;
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;
}