feat: extend sched with allocSlot and initialPgid
This commit is contained in:
1 parent
ae6bdd5880
commit
9d356d9a29
1 file changed
+192
-6
+192
-6
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user