feat: extend kmain with schedPriorityVerifierThread
This commit is contained in:
1 parent
0cd28abb5d
commit
661ff83253
1 file changed
+185
-10
+185
-10
@@ -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);
|
||||||
|
|||||||
Reference in new issue
Block a user