Signal Scheduling
Until now, signals have been firing immediately when received. But we actually want a proper queue — a way to schedule signals for later delivery at a controlled point. This is important because some signals shouldn’t be processed in the middle of another operation, and a queue lets us decide when to process them.
The Queue
On a single-threaded kernel without preemption, a fixed-size ring buffer of 32 entries is more than enough. A ring buffer is the classic choice for a fixed-size queue — two cursors chase each other around a circular array:
#![allow(unused)]
fn main() {
const SIGNAL_QUEUE_SIZE: usize = 32;
static mut SIGNAL_QUEUE: [u8; SIGNAL_QUEUE_SIZE] = [0; SIGNAL_QUEUE_SIZE];
static mut QUEUE_HEAD: usize = 0; // next read position
static mut QUEUE_TAIL: usize = 0; // next write position
}
When HEAD == TAIL, the queue is empty. When (TAIL + 1) % SIZE == HEAD, it’s full.
The modulo arithmetic wraps the cursors around automatically.
schedule()
Adds a signal to the queue. Checks if there’s room, writes the interrupt number to
QUEUE_TAIL, then advances the tail:
#![allow(unused)]
fn main() {
pub fn schedule(interrupt: u8) {
unsafe {
if (QUEUE_TAIL + 1) % SIGNAL_QUEUE_SIZE != QUEUE_HEAD {
SIGNAL_QUEUE[QUEUE_TAIL] = interrupt;
QUEUE_TAIL = (QUEUE_TAIL + 1) % SIGNAL_QUEUE_SIZE;
}
}
}
}
If the queue is full, the signal is silently dropped. In a more complete kernel you’d want to handle this more gracefully, but for now it’s fine.
process_pending()
Drains one signal from the queue and calls its registered handler. Checks if the queue
has anything in it, reads from QUEUE_HEAD, advances the head, then looks up and calls
the handler with a captured frame:
#![allow(unused)]
fn main() {
pub fn process_pending() {
unsafe {
if QUEUE_HEAD != QUEUE_TAIL {
let interrupt = SIGNAL_QUEUE[QUEUE_HEAD];
QUEUE_HEAD = (QUEUE_HEAD + 1) % SIGNAL_QUEUE_SIZE;
let frame = capture_registers();
let handler = SIGNAL_TABLE[interrupt as usize];
if let Some(h) = handler {
h(interrupt as u32, &frame);
}
}
}
}
}
Since there’s no real interrupt frame at this point — we’re processing from a queue,
not from an actual hardware interrupt — we call capture_registers() to get the
current CPU state as a best approximation.
Testing
Added sys signal to NPS to prove the queue works end to end:
> sys signal
=== Testing Signal Handler ===
Scheduling interrupt 0 (divide by zero)...
Processing pending signals...
EXCEPTION #0 DIVIDE BY ZERO
EAX: 0x0025AD6C EBX: 0x0024AA1C
ECX: 0x0025AD6C EDX: 0x00000024
ESI: 0x00000000 EDI: 0x00001000
EBP: 0x00000000 ESP: 0x0025AD54
EIP: 0x00000000 EFL: 0x00000000
The signal was queued, then processed — the divide by zero handler fired with a full register dump. Queue in, queue out.