diff --git a/user/alarmtest.c b/user/alarmtest.c new file mode 100644 index 0000000..396555d --- /dev/null +++ b/user/alarmtest.c @@ -0,0 +1,168 @@ +#include "ulib.h" + +/* + * Boot-time driver for real alarm(2)/SIGALRM (sched.c's threadSetAlarm, + * schedule()'s own timer-tick expiry scan, signalnum.h's sigAlrm): + * schedule() already fires against a per-thread pitTicks() deadline + * regardless of what that thread is doing, the exact same shape sleepUntil + * already has for threadSleep -- but expiry here raises a REAL signal + * (sigRaise) instead of just flipping ThreadState, which means everything + * this kernel already built for signals (catch, default-terminate, + * SA_RESTART/EINTR on a retryable syscall, early-wake for a Sleeping/ + * Blocked target) applies to it for free. Three phases, each proving a + * DIFFERENT piece of that: + * + * phase 1: userAlarm's own "at most one pending alarm" bookkeeping -- + * scheduling one, then replacing it, then canceling it, checking the + * PREVIOUS remaining-seconds return value each time. Purely arithmetic + * (see threadSetAlarm's own KTEST coverage in schedTest.c for the same + * logic driven directly, deterministically, with no real waiting) -- + * this phase proves the userspace-visible contract of that same logic, + * with no real elapsed time required at all. + * + * phase 2: a REAL 1-second alarm actually firing against a thread + * genuinely blocked forever in userRead (the same "attach a victim pipe + * no writer will ever attach to" setup eintrtarget.c/restarttarget.c + * already use) -- WITH a caught handler, no sigFlagRestart, so the read + * must return errInterrupted the instant the handler returns, exactly + * like restarttarget.c's own phase 1, just self-triggered by a hardware + * timer instead of another process's userSigSend. + * + * phase 3: the SAME real firing, but against a completely separate, + * freshly spawned child (alarmdefault.c) that installs NO handler at + * all -- SIGALRM's real default disposition is Terminate (signal.c's + * sigDefaultIsIgnore deliberately does not list it), so that child must + * actually die, with exit code 128+sigAlrm, exactly like sigtest.c's own + * sigdefault.c proves for sigTerm. + * + * alarmdefault is deliberately SPAWNED before phase 2's own 1-second alarm + * is even set, not after phase 2 finishes: its own 1-second countdown then + * runs CONCURRENTLY with this process's phase-2 wait (this kernel's real + * preemptive scheduler already runs every Runnable/Sleeping thread's own + * timer independently -- see schedule()'s alarm-expiry scan), so the two + * real-time waits overlap into roughly one real second of added boot time + * total, not two sequential ones -- every boot-time demo shares one fixed + * 25s QEMU budget (tools/ci-test.sh), and stacking real-time waits serially + * eats into every OTHER demo's own margin against that same clock for no + * added proof value (phase 2 and phase 3 exercise two independent code + * paths -- caught+EINTR vs default-Terminate -- neither depends on the + * other's outcome, so nothing is lost by racing them). + * + * If schedule()'s alarm-expiry scan were ever broken (deadline set but + * never actually checked, or checked but sigRaise never called), phase 2's + * read call below would simply never return and phase 3's userWait would + * simply never see its child die -- both hang the whole boot gate to its + * 25s QEMU timeout rather than merely failing a check, the same + * fault-injection-visible shape every other "blocked forever until a + * signal rescues it" demo in this kernel already has. + */ + +static volatile int handlerFired = 0; + +static void onAlrm(int signum) +{ + (void)signum; + handlerFired++; + print("alarmtest: onAlrm fired\n"); +} + +int main(void) +{ + print("alarmtest: start, pid="); + printDec(userGetpid()); + print("\n"); + + /* Phase 1: pure bookkeeping, no real waiting. */ + unsigned long previous1 = userAlarm(10); + if (previous1 != 0) { + print("alarmtest: FAIL expected no prior alarm\n"); + return 1; + } + unsigned long previous2 = userAlarm(3); + if (previous2 != 10) { + print("alarmtest: FAIL expected 10s remaining on replaced alarm, got "); + printDec((long)previous2); + print("\n"); + return 1; + } + unsigned long previous3 = userAlarm(0); + if (previous3 != 3) { + print("alarmtest: FAIL expected 3s remaining on canceled alarm, got "); + printDec((long)previous3); + print("\n"); + return 1; + } + unsigned long previous4 = userAlarm(0); + if (previous4 != 0) { + print("alarmtest: FAIL expected no alarm pending after cancel, got "); + printDec((long)previous4); + print("\n"); + return 1; + } + print("alarmtest: alarm-replace-and-cancel-bookkeeping-ok\n"); + + /* Spawned here, deliberately BEFORE phase 2 sets its own alarm below -- + see the file comment: this lets alarmdefault's own 1-second alarm + count down concurrently with phase 2's, instead of only starting + once phase 2 has already finished. */ + long child = userSpawn("/bin/alarmdefault", 0); + if (child < 0) { + print("alarmtest: FAIL spawn alarmdefault\n"); + return 1; + } + + /* Phase 2: real firing, caught, no sigFlagRestart -- must EINTR. */ + if (userSigAction(sigAlrm, onAlrm, 0, 0) == (long)sigSetFailed) { + print("alarmtest: FAIL install sigAlrm handler\n"); + return 1; + } + + long victimHandle = userPipeCreate(); + if (victimHandle < 0) { + print("alarmtest: FAIL create victim pipe\n"); + return 1; + } + long victimFd = userPipeAttach(victimHandle, 0); + if (victimFd < 0) { + print("alarmtest: FAIL attach victim pipe\n"); + return 1; + } + + if (userAlarm(1) != 0) { + print("alarmtest: FAIL expected no prior alarm before phase 2\n"); + return 1; + } + print("alarmtest: blocking on read, waiting for real alarm to fire\n"); + + char victimBuffer[4]; + long readResult = userRead((int)victimFd, victimBuffer, sizeof(victimBuffer)); + userClose((int)victimFd); + + if (handlerFired != 1) { + print("alarmtest: FAIL onAlrm handler did not fire exactly once\n"); + return 1; + } + if (readResult != errInterrupted) { + print("alarmtest: FAIL blocking read did not return errInterrupted\n"); + return 1; + } + print("alarmtest: alarm-fired-interrupted-blocking-read-ok\n"); + + /* Phase 3: real firing, default disposition, must terminate a + completely separate process -- alarmdefault (spawned above, before + phase 2) has had its own 1-second alarm counting down this whole + time, so this wait should return almost immediately, not add a + second real second of its own. */ + long childStatus = userWait(child); + print("alarmtest: alarmdefault exit status="); + printDec(childStatus); + print("\n"); + if (childStatus != 128 + sigAlrm) { + print("alarmtest: FAIL expected alarmdefault exit status 128+sigAlrm\n"); + return 1; + } + print("alarmtest: alarm-default-disposition-terminated-ok\n"); + + print("alarmtest: sigalrm-ok\n"); + return 0; +}