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;
|
||||
}
|
||||
Reference in new issue
Block a user