test: add more cases for schedTest
This commit is contained in:
1 parent
d992d971e7
commit
67d097e51b
1 file changed
+161
+161
@@ -468,3 +468,164 @@ KTEST(threadKillMarksRunnableThreadDeadWithKilledExitCode)
|
|||||||
uint32 id = threadCreate("killMe", noopThreadEntry);
|
uint32 id = threadCreate("killMe", noopThreadEntry);
|
||||||
KT_NE(t, id, threadInvalidId);
|
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);
|
||||||
|
|
||||||
Reference in new issue
Block a user