diff --git a/src/kmain.c b/src/kmain.c index a2ef552..cdea35e 100644 --- a/src/kmain.c +++ b/src/kmain.c @@ -15,9 +15,16 @@ #include "initrd.h" #include "vfs.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 heapSize (8 * 1024 * 1024) +#define heapSize (48 * 1024 * 1024) #define timerFrequency 100 #define userStackSize 0x4000 @@ -37,14 +44,20 @@ static void shellThread(void) 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) { - uint64 iterations = 0; uint64 nextLog = 50; for (;;) { - iterations++; + workerAIterations++; if (pitTicks() >= nextLog) { - kprintf("[workerA] tick=%lu iterations=%lu\n", pitTicks(), iterations); + kprintf("[workerA] tick=%lu iterations=%lu\n", pitTicks(), workerAIterations); nextLog += 100; } } @@ -52,17 +65,179 @@ static void workerA(void) static void workerB(void) { - uint64 iterations = 0; uint64 nextLog = 90; for (;;) { - iterations++; + workerBIterations++; if (pitTicks() >= nextLog) { - kprintf("[workerB] tick=%lu iterations=%lu\n", pitTicks(), iterations); + kprintf("[workerB] tick=%lu iterations=%lu\n", pitTicks(), workerBIterations); 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) { 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); } -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); if (!node) { kprintf("[user] '%s' not found in vfs\n", path); - return; + return threadInvalidId; } 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); if (entry == 0) { kprintf("[user] '%s' failed to load\n", path); - return; + return threadInvalidId; } pagingMapUserRange(stackTop - userStackSize, userStackSize);