Signal Callback
A signal callback system does a couple really important things we are going to need.
First off, it means that any part of the kernel can register a function to be called when a specific interrupt fires. It also means that instead of hardcoding handlers in the IDT, I can have a table of function pointers that can be swapped out at runtime. Part of this is also the ability to handle signals like Ctrl+C, Ctrl+Z and so on.
So instead of:
#![allow(unused)]
fn main() {
// Hardcoded forever
IDT.entries[33].set_handler(keyboard_interrupt_handler);
}
I can have:
#![allow(unused)]
fn main() {
// Registerable at runtime
signal::register(SIGKEYBOARD, my_custom_handler);
}
This sets the foundation for processes later — each process can register its own signal handlers, just like in Linux with signal(SIGINT, my_handler).
The Signal Table
Since there are exactly 256 possible interrupts on x86, we don’t need a dynamic data structure. A fixed array of 256 function pointers does the job cleanly with no heap allocation:
#![allow(unused)]
fn main() {
type SignalHandler = fn(interrupt: u32, frame: &InterruptFrame);
static mut SIGNAL_TABLE: [Option<SignalHandler>; 256] = [None; 256];
}
Option<SignalHandler> means each slot is either None (no handler registered) or Some(fn) (handler registered). The registration API is simple:
#![allow(unused)]
fn main() {
pub fn register(interrupt: u8, handler: SignalHandler) {
unsafe {
SIGNAL_TABLE[interrupt as usize] = Some(handler);
}
}
}
During init, we pre-populate the table with default handlers so nothing is ever unhandled:
#![allow(unused)]
fn main() {
pub fn init() {
register(0, handlers::divide_by_zero);
register(6, handlers::invalid_opcode);
register(8, handlers::double_fault);
register(13, handlers::general_protection_fault);
register(14, handlers::page_fault);
register(33, keyboard::kbhandler);
for i in 0..=255u8 {
if SIGNAL_TABLE[i as usize].is_none() {
register(i, handlers::default_interrupt);
}
}
}
}
Generic Dispatcher
Instead of each IDT entry pointing to its own specific handler, they all point to assembly stubs that push their interrupt number and call a single rust_interrupt_dispatcher. The flow changes from:
IRQ fires → IDT → hardcoded handler
To:
IRQ fires → IDT → generic dispatcher → looks up SIGNAL_TABLE[n] → calls registered handler
The dispatcher in Rust:
#![allow(unused)]
fn main() {
#[no_mangle]
pub extern "C" fn rust_interrupt_dispatcher(interrupt_num: u32, frame: &InterruptFrame) {
unsafe {
if let Some(handler) = SIGNAL_TABLE[interrupt_num as usize] {
handler(interrupt_num, frame);
}
}
}
}
This is a significant improvement — adding a new interrupt handler is now just one register() call rather than touching the IDT directly.