Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.