OS in Rust
On x86, there are about 20 different CPU exception types.
For interrupts (vs. polling).
| Pros | Cons |
|---|---|
| Fast Response | Can be distruptive |
| Less overall compute | More overall complexity |
____________ ____________
Real Time Clock --> | | Timer -------------> | |
ACPI -------------> | | Keyboard-----------> | | _____
Available --------> | Secondary |----------------------> | Primary | | |
Available --------> | Interrupt | Serial Port 2 -----> | Interrupt |---> | CPU |
Mouse ------------> | Controller | Serial Port 1 -----> | Controller | |_____|
Co-Processor -----> | | Parallel Port 2/3 -> | |
Primary ATA ------> | | Floppy disk -------> | |
Secondary ATA ----> |____________| Parallel Port 1----> |____________|0x20 (command) and 0x21 (data).0xa0 (command) and 0xa1 (data).0x20) CPU exceptions.pic8259/0.10.1/source/src/lib.rs
#![no_std]
use x86_64::instructions::port::Port;
const CMD_INIT: u8 = 0x11;
const CMD_END_OF_INTERRUPT: u8 = 0x20;
const MODE_8086: u8 = 0x01;
struct Pic {
offset: u8,
command: Port<u8>,
data: Port<u8>,
}
impl Pic {
fn handles_interrupt(&self, interupt_id: u8) -> bool {
self.offset <= interupt_id && interupt_id < self.offset + 8
}
unsafe fn end_of_interrupt(&mut self) {
self.command.write(CMD_END_OF_INTERRUPT);
}
unsafe fn read_mask(&mut self) -> u8 {
self.data.read()
}
unsafe fn write_mask(&mut self, mask: u8) {
self.data.write(mask)
}
}
pub struct ChainedPics {
pics: [Pic; 2],
}
impl ChainedPics {
pub const unsafe fn new(offset1: u8, offset2: u8) -> ChainedPics {
ChainedPics {
pics: [
Pic {
offset: offset1,
command: Port::new(0x20),
data: Port::new(0x21),
},
Pic {
offset: offset2,
command: Port::new(0xA0),
data: Port::new(0xA1),
},
],
}
}
pub unsafe fn initialize(&mut self) {
let mut wait_port: Port<u8> = Port::new(0x80);
let mut wait = || wait_port.write(0);
let saved_masks = self.read_masks();
self.pics[0].command.write(CMD_INIT);
wait();
self.pics[1].command.write(CMD_INIT);
wait();
self.pics[0].data.write(self.pics[0].offset);
wait();
self.pics[1].data.write(self.pics[1].offset);
wait();
self.pics[0].data.write(4);
wait();
self.pics[1].data.write(2);
wait();
self.pics[0].data.write(MODE_8086);
wait();
self.pics[1].data.write(MODE_8086);
wait();
self.write_masks(saved_masks[0], saved_masks[1])
}
pub unsafe fn read_masks(&mut self) -> [u8; 2] {
[self.pics[0].read_mask(), self.pics[1].read_mask()]
}
pub unsafe fn write_masks(&mut self, mask1: u8, mask2: u8) {
self.pics[0].write_mask(mask1);
self.pics[1].write_mask(mask2);
}
pub unsafe fn disable(&mut self) {
self.write_masks(u8::MAX, u8::MAX)
}
pub fn handles_interrupt(&self, interrupt_id: u8) -> bool {
self.pics.iter().any(|p| p.handles_interrupt(interrupt_id))
}
pub unsafe fn notify_end_of_interrupt(&mut self, interrupt_id: u8) {
if self.handles_interrupt(interrupt_id) {
if self.pics[1].handles_interrupt(interrupt_id) {
self.pics[1].end_of_interrupt();
}
self.pics[0].end_of_interrupt();
}
}
}pic8259Cargo.toml is growing quite large.src/interrupts.rsChainedPics”, the main thing the crate provides.interrupts::init_idt() ____________ ____________
Real Time Clock --> | | Timer -------------> | |
ACPI -------------> | | Keyboard-----------> | | _____
Available --------> | Secondary |----------------------> | Primary | | |
Available --------> | Interrupt | Serial Port 2 -----> | Interrupt |---> | CPU |
Mouse ------------> | Controller | Serial Port 1 -----> | Controller | |_____|
Co-Processor -----> | | Parallel Port 2/3 -> | |
Primary ATA ------> | | Floppy disk -------> | |
Secondary ATA ----> |____________| Parallel Port 1----> |____________| ____________
Timer -------------> | |
Keyboard-----------> | | _____
-------------------> | Primary | | |
Serial Port 2 -----> | Interrupt |---> | CPU |
Serial Port 1 -----> | Controller | |_____|
Parallel Port 2/3 -> | |
Floppy disk -------> | |
Parallel Port 1----> |____________|0 of the primary PIC.pub constenum to specify the index of a given hardware interrupt in the PIC.repru8 because doing so would’ve been a sensible default.enum and use magic numbers, at all..notify_end_of_interrupt.
cargo fmt but the arguments back on one line.src/interrupts.rs
notify_end_of_interrupt:
command and data ports to_start, and…src/main.rs
loop {}’s gobbled up all available resources.
loop {} the CPU continues to run at full speed even though there’s no work to do.HLT_start and panic in src/main.rsNow that we are able to handle interrupts from external devices, we are finally able to add support for keyboard input. This will allow us to interact with our kernel for the first time.
Like the hardware timer, the keyboard controller is already enabled by default. So when you press a key, the keyboard controller sends an interrupt to the PIC, which forwards it to the CPU. The CPU looks for a handler function in the IDT, but the corresponding entry is empty. Therefore, a double fault occurs.
So let’s add a handler function for the keyboard interrupt. It’s quite similar to how we defined the handler for the timer interrupt; it just uses a different interrupt number:
// in src/interrupts.rs
#[derive(Debug, Clone, Copy)]
#[repr(u8)]
pub enum InterruptIndex {
Timer = PIC_1_OFFSET,
Keyboard, // new
}
lazy_static! {
static ref IDT: InterruptDescriptorTable = {
let mut idt = InterruptDescriptorTable::new();
idt.breakpoint.set_handler_fn(breakpoint_handler);
[…]
// new
idt[InterruptIndex::Keyboard.as_usize()]
.set_handler_fn(keyboard_interrupt_handler);
idt
};
}
extern "x86-interrupt" fn keyboard_interrupt_handler(
_stack_frame: InterruptStackFrame)
{
print!("k");
unsafe {
PICS.lock()
.notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8());
}
}As we see from the graphic above, the keyboard uses line 1 of the primary PIC. This means that it arrives at the CPU as interrupt 33 (1 + offset 32). We add this index as a new Keyboard variant to the InterruptIndex enum. We don’t need to specify the value explicitly, since it defaults to the previous value plus one, which is also 33. In the interrupt handler, we print a k and send the end of interrupt signal to the interrupt controller.
We now see that a k appears on the screen when we press a key. However, this only works for the first key we press. Even if we continue to press keys, no more ks appear on the screen. This is because the keyboard controller won’t send another interrupt until we have read the so-called scancode of the pressed key.
To find out which key was pressed, we need to query the keyboard controller. We do this by reading from the data port of the PS/2 controller, which is the I/O port with the number 0x60:
// in src/interrupts.rs
extern "x86-interrupt" fn keyboard_interrupt_handler(
_stack_frame: InterruptStackFrame)
{
use x86_64::instructions::port::Port;
let mut port = Port::new(0x60);
let scancode: u8 = unsafe { port.read() };
print!("{}", scancode);
unsafe {
PICS.lock()
.notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8());
}
}We use the Port type of the x86_64 crate to read a byte from the keyboard’s data port. This byte is called the scancode and it represents the key press/release. We don’t do anything with the scancode yet, other than print it to the screen:

The above image shows me slowly typing “123”. We see that adjacent keys have adjacent scancodes and that pressing a key causes a different scancode than releasing it. But how do we translate the scancodes to the actual key actions exactly?
There are three different standards for the mapping between scancodes and keys, the so-called scancode sets. All three go back to the keyboards of early IBM computers: the IBM XT, the IBM 3270 PC, and the IBM AT. Later computers fortunately did not continue the trend of defining new scancode sets, but rather emulated the existing sets and extended them. Today, most keyboards can be configured to emulate any of the three sets.
By default, PS/2 keyboards emulate scancode set 1 (“XT”). In this set, the lower 7 bits of a scancode byte define the key, and the most significant bit defines whether it’s a press (“0”) or a release (“1”). Keys that were not present on the original IBM XT keyboard, such as the enter key on the keypad, generate two scancodes in succession: a 0xe0 escape byte and then a byte representing the key. For a list of all set 1 scancodes and their corresponding keys, check out the OSDev Wiki.
To translate the scancodes to keys, we can use a match statement:
// in src/interrupts.rs
extern "x86-interrupt" fn keyboard_interrupt_handler(
_stack_frame: InterruptStackFrame)
{
use x86_64::instructions::port::Port;
let mut port = Port::new(0x60);
let scancode: u8 = unsafe { port.read() };
// new
let key = match scancode {
0x02 => Some('1'),
0x03 => Some('2'),
0x04 => Some('3'),
0x05 => Some('4'),
0x06 => Some('5'),
0x07 => Some('6'),
0x08 => Some('7'),
0x09 => Some('8'),
0x0a => Some('9'),
0x0b => Some('0'),
_ => None,
};
if let Some(key) = key {
print!("{}", key);
}
unsafe {
PICS.lock()
.notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8());
}
}The above code translates keypresses of the number keys 0-9 and ignores all other keys. It uses a match statement to assign a character or None to each scancode. It then uses if let to destructure the optional key. By using the same variable name key in the pattern, we shadow the previous declaration, which is a common pattern for destructuring Option types in Rust.
Now we can write numbers:

Translating the other keys works in the same way. Fortunately, there is a crate named pc-keyboard for translating scancodes of scancode sets 1 and 2, so we don’t have to implement this ourselves. To use the crate, we add it to our Cargo.toml and import it in our lib.rs:
Now we can use this crate to rewrite our keyboard_interrupt_handler:
// in/src/interrupts.rs
extern "x86-interrupt" fn keyboard_interrupt_handler(
_stack_frame: InterruptStackFrame)
{
use pc_keyboard::{layouts, DecodedKey, HandleControl, Keyboard, ScancodeSet1};
use spin::Mutex;
use x86_64::instructions::port::Port;
lazy_static! {
static ref KEYBOARD: Mutex<Keyboard<layouts::Us104Key, ScancodeSet1>> =
Mutex::new(Keyboard::new(ScancodeSet1::new(),
layouts::Us104Key, HandleControl::Ignore)
);
}
let mut keyboard = KEYBOARD.lock();
let mut port = Port::new(0x60);
let scancode: u8 = unsafe { port.read() };
if let Ok(Some(key_event)) = keyboard.add_byte(scancode) {
if let Some(key) = keyboard.process_keyevent(key_event) {
match key {
DecodedKey::Unicode(character) => print!("{}", character),
DecodedKey::RawKey(key) => print!("{:?}", key),
}
}
}
unsafe {
PICS.lock()
.notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8());
}
}We use the lazy_static macro to create a static Keyboard object protected by a Mutex. We initialize the Keyboard with a US keyboard layout and the scancode set 1. The HandleControl parameter allows to map ctrl+[a-z] to the Unicode characters U+0001 through U+001A. We don’t want to do that, so we use the Ignore option to handle the ctrl like normal keys.
On each interrupt, we lock the Mutex, read the scancode from the keyboard controller, and pass it to the add_byte method, which translates the scancode into an Option<KeyEvent>. The KeyEvent contains the key which caused the event and whether it was a press or release event.
To interpret this key event, we pass it to the process_keyevent method, which translates the key event to a character, if possible. For example, it translates a press event of the A key to either a lowercase a character or an uppercase A character, depending on whether the shift key was pressed.
With this modified interrupt handler, we can now write text:

It’s possible to configure some aspects of a PS/2 keyboard, for example, which scancode set it should use. We won’t cover it here because this post is already long enough, but the OSDev Wiki has an overview of possible configuration commands.
This post explained how to enable and handle external interrupts. We learned about the 8259 PIC and its primary/secondary layout, the remapping of the interrupt numbers, and the “end of interrupt” signal. We implemented handlers for the hardware timer and the keyboard and learned about the hlt instruction, which halts the CPU until the next interrupt.
Now we are able to interact with our kernel and have some fundamental building blocks for creating a small shell or simple games.
Timer interrupts are essential for an operating system because they provide a way to periodically interrupt the running process and let the kernel regain control. The kernel can then switch to a different process and create the illusion of multiple processes running in parallel.
But before we can create processes or threads, we need a way to allocate memory for them. The next posts will explore memory management to provide this fundamental building block.