Skip to content

kernel: SIGALRM + per-process interval timer (SYS_RTC_ALARM)#337

Open
bboe wants to merge 11 commits intomainfrom
sigalrm-rtc-alarm
Open

kernel: SIGALRM + per-process interval timer (SYS_RTC_ALARM)#337
bboe wants to merge 11 commits intomainfrom
sigalrm-rtc-alarm

Conversation

@bboe
Copy link
Copy Markdown
Owner

@bboe bboe commented May 8, 2026

Summary

  • New per-process interval timer driven by SYS_RTC_ALARM (34h). EBX = ms_until_first (0 = cancel), ECX = ms_interval (0 = one-shot); returns EAX = ms remaining on prior alarm.
  • Expiration delivers SIGALRM (signum 14) via the existing SIGINT user-handler plumbing — vDSO sigreturn trampoline, 52-byte sigcontext, EFLAGS sanitization, IRQ-tail dispatch. The dispatchers and SIGNAL_TAIL_CHECK (renamed from SIGINT_TAIL_CHECK) take EAX = handler, EDX = signum from the caller, so adding a third signal in the future is a per-signal global + a macro branch.
  • Default action for SIGALRM is terminate (Linux signal(7)-aligned). Kill banner is ^A (SIGINT keeps ^C; corrupt-sigcontext kills print ^?).
  • rtc_sleep_ms, fd_read_console, and MIDI_IOCTL_DRAIN return EINTR when interrupted by either signal — same cooperative-interruption pattern that already existed for SIGINT.
  • libc adds alarm() (POSIX seconds) and alarm_ms() (BBoeOS ms-with-interval extension). tools/libc/include/signal.h gets SIGALRM.
  • cc.py gains signal() and alarm_ms() builtins for kernel-side/test C code; tests/bboeos.h mirrors the declarations for clang's host-side typecheck.

Motivation: lets userland (eventually tools/doom/opl_bboeos.c) schedule async OPL writes via Chocolate Doom's native opl_queue.c priority queue instead of the current 6-byte midi-ring shim. Doom integration is a follow-up PR — kept out of scope here.

Test Plan

Six new end-to-end QEMU-boot smoke tests in tests/programs/:

  • alarm_oneshot — handler runs exactly once after 50 ms.
  • alarm_repeat — 20 ms repeating alarm fires 8-11 times in 200 ms.
  • alarm_cancelalarm_ms(0,0) cancels a pending one-shot.
  • alarm_default_kill — arming SIGALRM with no handler kills the program; ^A banner distinguishes it from SIGINT's ^C.
  • alarm_coalesce — 1 ms repeat with 10 ms handler ⇒ ~10 fires per 100 ms (not 100), proving pending_sigalrm coalesces.
  • alarm_nesting — SIGINT handler busy-waits while SIGALRM becomes pending; signal_resume_after_handler's redelivery branch dispatches SIGALRM before resuming main.
  • alarm_during_sleeprtc_sleep_ms(500) interrupted by 50 ms alarm returns CF=1 / EINTR after ~50 ms.

Local CI matrix run on the branch:

  • test_unit (254/254), test_archive (13/13), test_kernel_archive (12/12), test_asm (20/20), test_bboefs (6/6), test_cc_bits (86/86), test_cc_compatibility (43/43)
  • test_programs_bbfs --slow (40/40), test_programs_ext2 --slow (71/71), test_programs_floppy (37/37)
  • test_floppy_boot, test_ps2, test_draw
  • test_doom_music_qemu / test_doom_sound_qemu — skipped locally (no wads/doom1.wad in env). The change doesn't touch the Doom port; CI will exercise these.

🤖 Generated with Claude Code

@bboe bboe force-pushed the sigalrm-rtc-alarm branch 4 times, most recently from f84a719 to 95f980e Compare May 8, 2026 21:01
bboe and others added 11 commits May 8, 2026 20:00
…lag)

Cosmetic prep for SIGALRM sharing the IRQ-tail macro and the nesting
flag — both will need to handle two signals.  This commit just
renames; the macro body still walks pending_sigint only and the
nesting flag still blocks just SIGINT (the only signal that exists).
Later tasks add SIGALRM and extend the macro body to walk both
pending bits.

Renames:
  - in_sigint_handler -> in_signal_handler (entry.asm global +
    program_enter init + signal.c references)
  - SIGINT_TAIL_CHECK -> SIGNAL_TAIL_CHECK (macro definition in
    irq_tail.inc + every call site: entry.asm x 3, syscall.asm x 1,
    ps2.c x 1, fdc.c x 1)
  - File-header generalizations in irq_tail.inc and signal.c from
    "SIGINT" to "Signal" (the dispatch surface is signal-agnostic
    even when only SIGINT exists).

No behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Symbol-only addition, no behavior change.  SYS_RTC_ALARM goes at
the start of the RTC group (30h) and the existing four entries
shift up by one (DATETIME 31h, MILLIS 32h, SLEEP 33h, UPTIME 34h)
so the group stays alphabetical-monotonic.  SIGALRM uses the
POSIX-aligned signum.  signal.h gets the matching SIGALRM macro
for userland.  cc.py's NAMED_CONSTANTS and tests/bboeos.h gain
matching SIG_DFL / SIG_IGN / SIGALRM / SIGINT entries so test
programs can reference the names symbolically.

The SYS_RTC_MILLIS literal in tools/libc/syscall.c's gettimeofday
shifts to 0x32 to match.

No other code uses these symbols by hex literal yet — later tasks
wire them in symbolically (NASM resolves SYS_RTC_* by name).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sigalrm_handler / pending_sigalrm / alarm_deadline / alarm_interval
join the existing SIGINT globals.  program_enter resets all of them
so alarms do not survive exec (POSIX setitimer behavior).  No reader
or writer yet — later tasks wire in the IRQ 0 deadline check, the
SYS_RTC_ALARM dispatch, and the macro/dispatcher generalization.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The kill path now prints a signal-specific banner — '^C' for SIGINT,
'^A' for SIGALRM, '^?' for the corrupt-sigcontext validation failure
in signal_resume_after_handler.  Callers must load EDX before
jumping; SIGINT_TAIL_CHECK and the redelivery branch now do so
explicitly.  No behavior change yet for SIGINT (banner unchanged).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both dispatchers now take EAX = handler, EDX = signum from the
caller (SIGNAL_TAIL_CHECK or the redelivery branch).  signal_dispatch_user
writes EDX into sigcontext + 4 (the signum slot was reserved for this
exactly), stashes the handler in EBP across the iret-frame reads
(EAX gets clobbered by the [esp + 32/40/44] loads), and clears the
pending bit corresponding to the signum.

signal_resume_after_handler's redelivery branch walks both pending
bits in priority order (SIGINT first), preserving the existing
SIGINT-only behavior when no SIGALRM is ever raised.

The hardcoded SIGINT constant and [sigint_handler] reference are
gone from signal.c — the file is now signal-agnostic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The IRQ-tail macro now checks pending_sigint first (lower signum =
higher priority) and falls through to pending_sigalrm.  When
pending_sigalrm is never set (current state — Task 8 wires the IRQ 0
deadline check next), behavior is byte-identical to the old
SIGINT-only path.

Loads EDX = signum and EAX = handler before jumping to the dispatcher;
matches the contract Tasks 4-5 set.  SIG_IGN now clears the right
pending bit based on EDX.

Also updates signal.c: alias-block comment lists pending_sigalrm and
sigalrm_handler as additional kernel globals referenced from inline
asm; "the SIGINT preempted it" and "SIGINT can deliver again"
generalized to signal-agnostic phrasing matching the dispatchers'
new contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EBX = SIGALRM (14) is now a legal signum; the handler slot is
routed to sigalrm_handler vs sigint_handler based on EBX.  Handler
validation rules unchanged (SIG_DFL / SIG_IGN / [PROGRAM_BASE,
KERNEL_VIRT_BASE)).  No way to actually fire a SIGALRM yet — that
needs SYS_RTC_ALARM (next commit) and the IRQ 0 deadline check.

cc.py gains a signal() builtin so cc.py-compiled programs (tests,
shell, edit) can call it directly; matching declaration in
tests/bboeos.h for clang -fsyntax-only.  docs/syscalls.md sys_signal
row updated to reflect the broadened signum set.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EBX = ms_until_first (0 = cancel), ECX = ms_interval (0 = one-shot).
Returns EAX = ms remaining on prior alarm (or 0).  CF always clear —
no error path; any EBX/ECX combination is legal.

Stores the deadline as a system_ticks value (system_ticks runs at
1 kHz so the conversion is identity).  IRQ 0 will fire SIGALRM
when system_ticks crosses alarm_deadline (next commit).

cc.py gains an alarm_ms() builtin; tests/bboeos.h gains the matching
declaration for clang -fsyntax-only.  docs/syscalls.md gets an
rtc_alarm row.  alarm_cancel test exercises the cancel path
(EBX=0 returns ms remaining on previously-armed alarm).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After inc system_ticks, check alarm_deadline; if armed and reached,
set pending_sigalrm and either re-arm (alarm_interval != 0) or clear
(one-shot).  Coalescing matches POSIX: a second fire while
pending_sigalrm is already 1 is dropped.  Constant-time addition
preserves the O(1) ISR latency invariant.

SIGNAL_TAIL_CHECK at the end of the ISR delivers to the user
handler if one is installed and we interrupted CPL=3 — that's the
existing IRQ-tail path, unchanged.

End-to-end SIGALRM smoke tests (oneshot, repeat, default_kill,
coalesce, nesting) cover the user-facing alarm contract from
this point forward.  docs/architecture.md gains a SIGALRM section.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ns EINTR

libc gains alarm() (POSIX seconds) and alarm_ms() (BBoeOS ms+interval
extension).  Both wrap SYS_RTC_ALARM (30h).  alarm() returns prior
remaining seconds (rounded up); alarm_ms() exposes the kernel's
native ms+interval shape — used by test programs and (eventually)
Doom's OPL queue.

rtc_sleep_ms is now interruptible: pending_sigint and pending_sigalrm
short-circuit the busy-wait loop with CF=1.  SYS_RTC_SLEEP propagates
as AL=ERROR_INTERRUPTED so libc's eventual sleep() wrapper can
surface EINTR (and a future signal-aware sleep retry loop can do the
right thing).  docs/syscalls.md rtc_sleep row mentions the EINTR
return path; alarm_during_sleep test arms a 50 ms alarm, calls
sleep(500), and asserts the sleep returned around 50 ms with CF=1
and the handler having run exactly once.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both blocking syscall paths checked only pending_sigint; extend the
check to pending_sigalrm so an alarm fired during read(0, ...) or
MIDI_IOCTL_DRAIN unblocks promptly with ERROR_INTERRUPTED.

CHANGELOG entry summarizes the user-visible surface added by the
SIGALRM feature: SYS_RTC_ALARM (34h), SIGALRM signal, EINTR-aware
blocking syscalls, libc alarm()/alarm_ms() wrappers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bboe bboe force-pushed the sigalrm-rtc-alarm branch from 95f980e to fda1fc5 Compare May 9, 2026 03:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant