test: add more cases for schedTest

This commit is contained in:
dualfroz committed 2026-09-11 14:31:44 +02:00
1 parent d992d971e7
commit 67d097e51b
1 file changed
+161
+161
View File
@@ -468,3 +468,164 @@ KTEST(threadKillMarksRunnableThreadDeadWithKilledExitCode)
uint32 id = threadCreate("killMe", noopThreadEntry);
KT_NE(t, id, threadInvalidId);
KT_TRUE(t, threadKill(id));
KT_EQ(t, threadAt(id)->state, threadDead);
KT_EQU(t, threadAt(id)->exitCode, threadKilledExitCode);
/* A killed thread reaps exactly like one that called threadExit on its
own -- no separate "killed" bookkeeping survives past exitCode. */
KT_TRUE(t, threadReap(id));
}
KTEST(threadKillOnAlreadyDeadOrUnusedSlotFails)
{
uint32 id = threadCreate("killTwice", noopThreadEntry);
KT_NE(t, id, threadInvalidId);
KT_TRUE(t, threadKill(id));
/* Already Dead: a second kill must not succeed (nor reset exitCode). */
KT_FALSE(t, threadKill(id));
KT_EQU(t, threadAt(id)->exitCode, threadKilledExitCode);
KT_TRUE(t, threadReap(id));
/* threadUnused again after reap: still rejected. */
KT_FALSE(t, threadKill(id));
}
KTEST(threadKillWakesAThreadParkedInJoin)
{
ThreadState saved = currentThread()->state;
uint32 victim = threadCreate("victim", noopThreadEntry);
KT_NE(t, victim, threadInvalidId);
/* Mirrors exactly what threadJoin (sched.c) itself does while waiting
for `victim`: block the current thread (the idle thread, id 0, at
this point in boot) on the victim slot's own address -- the same
channel threadExit/threadKill both wake. */
schedBlockCurrent(threadAt(victim));
KT_EQ(t, currentThread()->state, threadBlocked);
KT_TRUE(t, threadKill(victim));
KT_EQ(t, currentThread()->state, threadRunnable);
currentThread()->state = saved;
KT_TRUE(t, threadReap(victim));
}
/*
* Thread.parentId (sched.h) and the real, asynchronous SIGCHLD it drives
* from threadExit/threadKill (signal.h/signal.c's sigRaise) -- the live
* ring-3 proof that a genuinely blocked parent actually wakes for its own
* child's death is user/sigchldtest.c spawning user/sigchldchild.c (needle
* "sigchldtest: sigchld-delivered-to-parent-ok" in tools/ci-test.sh); what
* is deterministic here is the plumbing itself: threadCreate/
* threadCreateUser attributing parentId to whoever is truly running at
* creation time, and threadKill (parameterized by an explicit id, unlike
* threadExit which only ever acts on the CURRENT thread and so cannot be
* driven this way with ktestRunAll's single, never-context-switched
* thread -- see this file's own header comment) actually raising sigChld
* against exactly that id when the id it names dies.
*/
KTEST(threadCreateAttributesParentIdToWhicheverThreadIsActuallyCurrent)
{
uint32 id = threadCreate("childOfCurrent", noopThreadEntry);
KT_NE(t, id, threadInvalidId);
KT_EQU(t, threadAt(id)->parentId, currentThreadId());
KT_TRUE(t, threadDiscard(id));
}
KTEST(threadKillRaisesSigChldAgainstParentId)
{
uint32 parent = threadCreate("chldParent", noopThreadEntry);
KT_NE(t, parent, threadInvalidId);
uint32 child = threadCreate("chldVictim", noopThreadEntry);
KT_NE(t, child, threadInvalidId);
/* ktestRunAll runs single-threaded (see file header) -- threadCreate
always attributes parentId to THIS KTEST runner itself, so the
"child was spawned by parent" relationship this test actually wants
is set up directly here, the same white-box technique
threadStopExcludesTargetFromUnrelatedWaitChannelWake below already
uses to reach a state the real call path would only reach through
an actual live sysSpawn. */
threadAt(child)->parentId = parent;
KT_EQU(t, threadAt(parent)->sigPending & (1ull << sigChld), 0ull);
KT_TRUE(t, threadKill(child));
KT_EQU(t, threadAt(parent)->sigPending & (1ull << sigChld), (1ull << sigChld));
KT_TRUE(t, threadReap(child));
KT_TRUE(t, threadDiscard(parent));
}
KTEST(threadKillEarlyWakesAParentGenuinelyBlockedWaitingForSigChld)
{
int channel;
uint32 parent = threadCreate("chldBlockedParent", noopThreadEntry);
KT_NE(t, parent, threadInvalidId);
uint32 child = threadCreate("chldVictim2", noopThreadEntry);
KT_NE(t, child, threadInvalidId);
threadAt(child)->parentId = parent;
/* sigChld's real-world default disposition is Ignore (sigDefaultIsIgnore,
signal.c), which sigWouldDeliver (also signal.c) treats exactly like
an installed sigHandlerIgnore -- correctly declining to early-wake a
thread for a signal that would just be silently dismissed on
delivery anyway, real Unix's own "SIGCHLD does nothing without a
handler" behavior. A parent that actually wants this notification
(like user/sigchldtest.c) always installs a real handler first, so
this test must too, or threadWakeForSignal below would (correctly)
never fire and this would be testing the wrong thing. */
sigSetHandler(threadAt(parent), sigChld, 0x40005000ull, 0, 0);
/* Fabricate exactly what schedBlockCurrent would have left behind had
parent been the one calling it (see
threadStopExcludesTargetFromUnrelatedWaitChannelWake above for the
same convention) -- e.g. a real sigchldtest.c genuinely blocked in
userRead on a pipe with no writer (see user/sigchldtest.c) while
its own sigChld handler is installed. threadWakeForSignal's own
state-machine bookkeeping is already proven generically by
threadWakeForSignalMovesSleepingOrBlockedThreadToRunnable above (for
any signal); this proves specifically that threadKill's own new
sigRaise(parentId, sigChld) call actually reaches that same
early-wake path for THIS signal, not just the pending-bit half. */
threadAt(parent)->state = threadBlocked;
threadAt(parent)->waitChannel = &channel;
KT_TRUE(t, threadKill(child));
KT_EQ(t, threadAt(parent)->state, threadRunnable);
KT_TRUE(t, threadReap(child));
/* Runnable and never actually scheduled (ktestRunAll runs
single-threaded, see file header) -- threadDiscard, not threadReap,
is the correct teardown here, same as every other fabricated-but-
never-run thread in this file. */
KT_TRUE(t, threadDiscard(parent));
}
KTEST(threadKillAgainstAnOrphanWithADeadParentIdRaisesNothing)
{
uint32 deadParent = threadCreate("deadParent", noopThreadEntry);
KT_NE(t, deadParent, threadInvalidId);
KT_TRUE(t, threadKill(deadParent));
KT_EQ(t, threadAt(deadParent)->state, threadDead);
uint32 orphan = threadCreate("orphan", noopThreadEntry);
KT_NE(t, orphan, threadInvalidId);
threadAt(orphan)->parentId = deadParent;
/* sigRaise's own targetId-state guard (signal.c) is what makes this
safe, not anything special here -- an orphan whose spawning parent
is already gone simply has nowhere for sigChld to land, the same
way a real kernel would reparent it to init instead of crashing or
misdelivering into whatever unrelated thread later reuses
deadParent's slot; this kernel does not implement reparenting (one
shared address space, no true fork(), see doSpawn's own file
comment in syscall.c), so the signal is just silently dropped. */
KT_TRUE(t, threadKill(orphan));
KT_EQ(t, threadAt(orphan)->state, threadDead);
KT_EQU(t, threadAt(deadParent)->sigPending & (1ull << sigChld), 0ull);