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);
|
||||
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