169 lines
6.9 KiB
C
169 lines
6.9 KiB
C
#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;
|
|
}
|