test: cover jobtest and killtest
This commit is contained in:
1 parent
f6a32938a6
commit
01ad058c1c
2 files changed
+199
No files matched your search
+131
@@ -0,0 +1,131 @@
|
|||||||
|
#include "ulib.h"
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Boot-time driver for job control (signal.h's SIGSTOP/SIGCONT/SIGTSTP,
|
||||||
|
* sched.h's threadStopped): spawns jobtarget.c, waits for it to actually
|
||||||
|
* be parked in a real blocking userRead (not guessed via a sleep), stops it
|
||||||
|
* there with userSigSend(target, sigStop), writes the very byte it is
|
||||||
|
* blocked waiting for WHILE it is stopped, and only then sends sigCont --
|
||||||
|
* proving the resume happens through the genuine restart-syscall retry
|
||||||
|
* path sysRead already uses (syscall.c), not some shortcut that would have
|
||||||
|
* worked even if threadStop/threadContinue did nothing at all.
|
||||||
|
*
|
||||||
|
* Two independent pipes, not one, exactly because this needs traffic in
|
||||||
|
* BOTH directions kept open across the whole exchange (sigtest.c's own
|
||||||
|
* single "ready" pipe is closed by both sides right after one byte, which
|
||||||
|
* is not enough here): jobtest attaches BOTH ends it owns (ready as
|
||||||
|
* reader, go as writer) before ever spawning jobtarget, for the same
|
||||||
|
* "attach first" reason sigtest.c documents at length -- whichever side
|
||||||
|
* would otherwise attach second risks the pipe's slot already being freed
|
||||||
|
* by pipeReleaseIfUnused if the other side raced through attach+use+close
|
||||||
|
* first.
|
||||||
|
*/
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
print("jobtest: start, pid=");
|
||||||
|
printDec(userGetpid());
|
||||||
|
print("\n");
|
||||||
|
|
||||||
|
long readyHandle = userPipeCreate();
|
||||||
|
long goHandle = userPipeCreate();
|
||||||
|
if (readyHandle < 0 || goHandle < 0) {
|
||||||
|
print("jobtest: FAIL create pipes\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
long readyFd = userPipeAttach(readyHandle, 0);
|
||||||
|
long goFd = userPipeAttach(goHandle, 1);
|
||||||
|
if (readyFd < 0 || goFd < 0) {
|
||||||
|
print("jobtest: FAIL attach pipes\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
char readyHandleStr[24];
|
||||||
|
char goHandleStr[24];
|
||||||
|
decToString(readyHandle, readyHandleStr);
|
||||||
|
decToString(goHandle, goHandleStr);
|
||||||
|
|
||||||
|
char* targetArgv[] = { "/bin/jobtarget", readyHandleStr, goHandleStr, 0 };
|
||||||
|
long target = userSpawn("/bin/jobtarget", targetArgv);
|
||||||
|
if (target < 0) {
|
||||||
|
print("jobtest: FAIL spawn jobtarget\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* sched.h's Thread.pgid: a RUNTIME userSpawn from a genuinely running,
|
||||||
|
non-idle parent (this process) must have jobtarget inherit THIS
|
||||||
|
process's own pgid, matching real fork()'s default before any
|
||||||
|
setpgid ever runs -- the opposite default from every boot-time
|
||||||
|
spawnUser call in kmain.c (each becomes its own new group leader,
|
||||||
|
sched.c's initialPgid), which schedTest.c's own KTEST coverage
|
||||||
|
cannot exercise at all (ktestRunAll always runs as the permanent
|
||||||
|
idle thread, the one case that can never be a real spawning parent
|
||||||
|
-- see that file's header comment). This is the only live ring-3
|
||||||
|
proof of the inherit-from-parent half of initialPgid. */
|
||||||
|
if (userGetpgid(target) != userGetpgid(0)) {
|
||||||
|
print("jobtest: FAIL jobtarget did not inherit parent pgid\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
print("jobtest: child-inherits-parent-pgid-ok\n");
|
||||||
|
|
||||||
|
char readyByte;
|
||||||
|
/* Real blocking read: does not return until jobtarget has actually
|
||||||
|
attached the go pipe as a reader AND written its ready byte -- so by
|
||||||
|
the time this returns, jobtarget is either about to call, or already
|
||||||
|
inside, its own blocking read on the go pipe. */
|
||||||
|
if (userRead((int)readyFd, &readyByte, 1) != 1) {
|
||||||
|
print("jobtest: FAIL read ready byte\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
userClose((int)readyFd);
|
||||||
|
print("jobtest: jobtarget ready\n");
|
||||||
|
|
||||||
|
if (userSigSend(target, sigStop) != 0) {
|
||||||
|
print("jobtest: FAIL send sigStop to jobtarget\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
print("jobtest: sent sigStop to jobtarget\n");
|
||||||
|
|
||||||
|
/* Writing the byte jobtarget is blocked waiting for WHILE it is
|
||||||
|
stopped, rather than after sigCont, is deliberate: it exercises the
|
||||||
|
realistic job-control shape (a stopped job's input keeps queuing up
|
||||||
|
behind it) and proves stop+cont do not lose or corrupt that pending
|
||||||
|
write. It is NOT, on its own, an airtight proof that sigStop above
|
||||||
|
did anything at all -- pipeWrite (pipe.c) calls schedWakeChannel
|
||||||
|
unconditionally on every successful write, so a jobtarget that was
|
||||||
|
merely left threadBlocked (sigStop silently broken/a no-op) would
|
||||||
|
ALSO be woken by this very write and finish on its own, without
|
||||||
|
needing sigCont below at all. That rigorous "would this still pass
|
||||||
|
if stop were a no-op" distinction is exactly what
|
||||||
|
schedTest.c's threadStopExcludesTargetFromUnrelatedWaitChannelWake
|
||||||
|
proves instead (fault-injection verified: a genuinely Stopped
|
||||||
|
thread's waitChannel is never matched by schedWakeChannel) -- what
|
||||||
|
THIS live demo proves is the piece that KTEST cannot: a real ring-3
|
||||||
|
program blocked in a real syscall survives being stopped and
|
||||||
|
resumed through the real syscall retry path, end to end, without
|
||||||
|
crashing or wedging the system. */
|
||||||
|
if (userWrite((int)goFd, "g", 1) != 1) {
|
||||||
|
print("jobtest: FAIL write go byte\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userSigSend(target, sigCont) != 0) {
|
||||||
|
print("jobtest: FAIL send sigCont to jobtarget\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
print("jobtest: sent sigCont to jobtarget\n");
|
||||||
|
userClose((int)goFd);
|
||||||
|
|
||||||
|
long status = userWait(target);
|
||||||
|
print("jobtest: jobtarget exit status=");
|
||||||
|
printDec(status);
|
||||||
|
print("\n");
|
||||||
|
if (status != 55) {
|
||||||
|
print("jobtest: FAIL expected jobtarget exit status 55\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
print("jobtest: job-stop-continue-ok\n");
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
#include "ulib.h"
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Exercises threadKill (sched.c) end to end through sysKill: spawns
|
||||||
|
* killvictim.c (which blocks forever in a real, syscall-level pipe read
|
||||||
|
* with no writer ever attaching), waits long enough for it to actually
|
||||||
|
* reach and park inside that read, then kills it and confirms userWait
|
||||||
|
* reports exactly the killed exit status (9) -- the only way this kernel
|
||||||
|
* can ever recover a process stuck in a genuine block, now that
|
||||||
|
* sysRead/sysWrite can really block (pipe.h/vfs.h, same wave as this).
|
||||||
|
*/
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
print("killtest: start, pid=");
|
||||||
|
printDec(userGetpid());
|
||||||
|
print("\n");
|
||||||
|
|
||||||
|
if (userKill(userGetpid()) != -1) {
|
||||||
|
print("killtest: FAIL self-kill should be rejected\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
print("killtest: self-kill rejected\n");
|
||||||
|
|
||||||
|
if (userKill(999999) != -1) {
|
||||||
|
print("killtest: FAIL kill of bogus pid should fail\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
print("killtest: kill bad pid rejected\n");
|
||||||
|
|
||||||
|
long victim = userSpawn("/bin/killvictim", 0);
|
||||||
|
if (victim < 0) {
|
||||||
|
print("killtest: FAIL spawn killvictim\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Give the victim time to actually attach and reach its blocking read
|
||||||
|
-- killing it before it ever calls userRead would still "work" (any
|
||||||
|
Runnable/Sleeping/Blocked thread is a valid target), but would not
|
||||||
|
prove the interesting case: tearing down a call already parked
|
||||||
|
mid-syscall. */
|
||||||
|
userSleep(100);
|
||||||
|
|
||||||
|
if (userKill(victim) != 0) {
|
||||||
|
print("killtest: FAIL kill victim\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
print("killtest: killed victim\n");
|
||||||
|
|
||||||
|
long status = userWait(victim);
|
||||||
|
print("killtest: victim exit status=");
|
||||||
|
printDec(status);
|
||||||
|
print("\n");
|
||||||
|
if (status != 9) {
|
||||||
|
print("killtest: FAIL expected exit status 9\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Already dead and reaped (userWait above reaped it): killing it again
|
||||||
|
must fail cleanly, not resurrect a slot that has since been reused. */
|
||||||
|
if (userKill(victim) != -1) {
|
||||||
|
print("killtest: FAIL kill of already-reaped pid should fail\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
print("killtest: kill-ok\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user