feat: extend kmain with schedPriorityVerifierThread

This commit is contained in:
dualfroz committed 2026-09-04 19:09:03 +02:00
1 parent 0cd28abb5d
commit 661ff83253
1 file changed
+185 -10
+185 -10
View File
@@ -15,9 +15,16 @@
#include "initrd.h" #include "initrd.h"
#include "vfs.h" #include "vfs.h"
#include "elf.h" #include "elf.h"
#include "ktest.h"
#include "pipe.h"
#include "klib.h"
#include "signal.h"
#include "ports.h"
#include "ata.h"
#include "sfs.h"
#define multibootBootMagic 0x36D76289 #define multibootBootMagic 0x36D76289
#define heapSize (8 * 1024 * 1024) #define heapSize (48 * 1024 * 1024)
#define timerFrequency 100 #define timerFrequency 100
#define userStackSize 0x4000 #define userStackSize 0x4000
@@ -37,14 +44,20 @@ static void shellThread(void)
shellRun(); shellRun();
} }
/* Exposed at file scope (rather than local to workerA/workerB) so
schedPriorityVerifierThread can read them back and turn "the scheduler
favors higher weight" into an automated, gate-checked assertion instead
of something a human has to eyeball in the boot log. */
static volatile uint64 workerAIterations;
static volatile uint64 workerBIterations;
static void workerA(void) static void workerA(void)
{ {
uint64 iterations = 0;
uint64 nextLog = 50; uint64 nextLog = 50;
for (;;) { for (;;) {
iterations++; workerAIterations++;
if (pitTicks() >= nextLog) { if (pitTicks() >= nextLog) {
kprintf("[workerA] tick=%lu iterations=%lu\n", pitTicks(), iterations); kprintf("[workerA] tick=%lu iterations=%lu\n", pitTicks(), workerAIterations);
nextLog += 100; nextLog += 100;
} }
} }
@@ -52,17 +65,179 @@ static void workerA(void)
static void workerB(void) static void workerB(void)
{ {
uint64 iterations = 0;
uint64 nextLog = 90; uint64 nextLog = 90;
for (;;) { for (;;) {
iterations++; workerBIterations++;
if (pitTicks() >= nextLog) { if (pitTicks() >= nextLog) {
kprintf("[workerB] tick=%lu iterations=%lu\n", pitTicks(), iterations); kprintf("[workerB] tick=%lu iterations=%lu\n", pitTicks(), workerBIterations);
nextLog += 100; nextLog += 100;
} }
} }
} }
#define schedPriorityCheckDelayMs 15000
#define schedPriorityWeightRatio 3
static void schedPriorityVerifierThread(void)
{
/* threadSleep() only flips this thread's own state to Sleeping; unlike
the sysSleep syscall path (syscall.c), which forces a reschedule
synchronously in the same interrupt frame right after calling it,
a plain kernel thread body must yield for itself or it simply falls
through to the next line on its own timeslice, sleep or not. Loop on
halt() until schedule()'s "wake sleeping threads whose time has come"
pass (sched.c) actually flips the state back, the same wait pattern
pipeReadBlocking/pipeWriteBlocking (pipe.c) use for wait-queue waits:
any spurious wake (e.g. a keyboard IRQ, which never calls schedule()
and so never touches this state) just loops back into another
halt() instead of proceeding early. */
threadSleep(schedPriorityCheckDelayMs);
while (currentThread()->state == threadSleeping)
halt();
uint64 a = workerAIterations;
uint64 b = workerBIterations;
/* workerA runs at schedPriorityWeightRatio times workerB's weight, so
over any sufficiently long stretch where both are always runnable
(true here: both are pure CPU hogs, never sleeping or blocking) the
CFS-lite scheduler should give workerA that many times as many turns.
Allow a wide [1.5x, 6x] band rather than pinning to exactly 3x: this
is a real preemptive scheduler racing against wall-clock QEMU
performance, not a deterministic simulation, so the achieved ratio
will vary run to run - the assertion that matters is "meaningfully
and consistently favors the higher-weight thread", not an exact
number. */
bool ok = b > 0 && a > b + b / 2 && a < b * (schedPriorityWeightRatio * 2);
kprintf("[sched] priority check: workerA=%lu workerB=%lu (weight ratio %u) -> %s\n",
a, b, schedPriorityWeightRatio, ok ? "sched-priority-ok" : "sched-priority-FAIL");
for (;;)
halt();
}
#define pipeDemoMessages 10
static int pipeDemoHandle = -1;
static void pipeProducerThread(void)
{
for (int i = 0; i < pipeDemoMessages; i++) {
char line[32];
int len = snprintf(line, sizeof(line), "msg %d\n", i);
uint64 offset = 0;
while (offset < (uint64)len) {
int64 n = pipeWriteBlocking(pipeDemoHandle, line + offset, (uint64)len - offset);
if (n <= 0)
break;
offset += (uint64)n;
}
}
pipeCloseWrite(pipeDemoHandle);
/* Kernel thread entries run atop a synthetic iret frame, not a normal
call stack: falling off the end here would pop garbage as the next
instruction pointer. Every kernel thread in this scheduler must park
itself forever instead of returning (see workerA/workerB). */
for (;;)
halt();
}
static void pipeConsumerThread(void)
{
char lineBuffer[32];
uint64 lineLength = 0;
int received = 0;
for (;;) {
char chunk[16];
int64 n = pipeReadBlocking(pipeDemoHandle, chunk, sizeof(chunk));
if (n > 0) {
for (int64 i = 0; i < n; i++) {
char c = chunk[i];
if (c == '\n') {
lineBuffer[lineLength] = 0;
kprintf("[pipe] consumer got: %s\n", lineBuffer);
received++;
lineLength = 0;
} else if (lineLength + 1 < sizeof(lineBuffer)) {
lineBuffer[lineLength++] = c;
}
}
} else {
/* 0 = true EOF, -1 = invalid handle: either way there is
nothing left to wait for. */
break;
}
}
pipeCloseRead(pipeDemoHandle);
kprintf("pipe demo: eof, count=%d\n", received);
for (;;)
halt();
}
/* Demonstrates threadJoin/threadReap's real, live blocking path (sched.c,
sched.h) end to end -- the one path schedTest.c cannot exercise, since
ktestRunAll() runs before the scheduler is live and there is no second
thread to actually park on and be woken by. shortLivedWorkerThread does a
little busy-work then calls threadExit() directly (a kernel thread may
call it as a plain function -- no syscall needed, unlike a ring-3
program) -- joinerThread almost certainly starts first (both are created
with default weight, and the worker has real work to do before it can
exit), so this also proves the schedBlockCurrent+halt() park actually
happens and schedWakeChannel actually wakes it, not just the fast "already
dead" path. */
#define joinDemoBusyIterations 20000000ull
#define joinDemoExitCode 42ull
static volatile uint32 joinDemoWorkerId = threadInvalidId;
static void shortLivedWorkerThread(void)
{
for (volatile uint64 i = 0; i < joinDemoBusyIterations; i++)
;
threadExit(joinDemoExitCode);
/* Marked Dead above; the CPU is still executing this thread's code
until the next timer tick preempts it into schedule(), which will
never pick a Dead thread again. Must park, not return (see
workerA/workerB). */
for (;;)
halt();
}
static void joinProbeThread(void)
{
for (;;)
halt();
}
static void joinerThread(void)
{
int64 code = threadJoin(joinDemoWorkerId);
bool codeOk = (code == (int64)joinDemoExitCode);
/* threadJoin already reaped the worker's slot -- prove it is genuinely
reusable, not just marked dead, by immediately creating a new thread
and checking it lands in that exact freed slot rather than growing
the table further. */
uint32 reused = threadCreate("joinReuseProbe", joinProbeThread);
bool reuseOk = (reused == joinDemoWorkerId);
if (reused != threadInvalidId)
threadDiscard(reused);
kprintf("[sched] join demo: worker pid=%u exitCode=%ld (expected %lu), "
"freed slot reused=%s -> %s\n",
joinDemoWorkerId, code, joinDemoExitCode, reuseOk ? "yes" : "no",
(codeOk && reuseOk) ? "join-reap-ok" : "join-reap-FAIL");
for (;;)
halt();
}
static void runHeapTest(void) static void runHeapTest(void)
{ {
kprintf("[heap] free-list allocator self-test:\n"); kprintf("[heap] free-list allocator self-test:\n");
@@ -116,12 +291,12 @@ static void runPagingTest(void)
kprintf("[paging] readback FAILED (value=%lx resolved=%lx)\n", *probe, resolved); kprintf("[paging] readback FAILED (value=%lx resolved=%lx)\n", *probe, resolved);
} }
static void spawnUser(const char* path, const char* name, uint64 stackTop, uint64 brkStart) static uint32 spawnUser(const char* path, const char* name, uint64 stackTop, uint64 brkStart)
{ {
VfsNode* node = vfsLookup(path); VfsNode* node = vfsLookup(path);
if (!node) { if (!node) {
kprintf("[user] '%s' not found in vfs\n", path); kprintf("[user] '%s' not found in vfs\n", path);
return; return threadInvalidId;
} }
const void* image = vfsFileData(node); const void* image = vfsFileData(node);
@@ -129,7 +304,7 @@ static void spawnUser(const char* path, const char* name, uint64 stackTop, uint6
uint64 entry = elfLoad(image, size); uint64 entry = elfLoad(image, size);
if (entry == 0) { if (entry == 0) {
kprintf("[user] '%s' failed to load\n", path); kprintf("[user] '%s' failed to load\n", path);
return; return threadInvalidId;
} }
pagingMapUserRange(stackTop - userStackSize, userStackSize); pagingMapUserRange(stackTop - userStackSize, userStackSize);