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

get_line

We all know readline from C — it collects text from STDIN and saves it to a buffer, returning when the user presses Enter. We’re implementing the same thing for our kernel, but there’s a fundamental caveat that’s worth understanding.

How it Works

The design is straightforward. A static flag GET_LINE_ACTIVE tells the keyboard handler to route input to get_line instead of the shell. A shared LINE_CHAR variable acts as a one-character mailbox between the interrupt handler and get_line. The main loop uses hlt to sleep between characters:

#![allow(unused)]
fn main() {
pub fn get_line(buf: &mut [u8]) -> usize {
    // Set active flag — keyboard handler now routes to LINE_CHAR
    GET_LINE_ACTIVE = true;
    let mut pos = 0;
    loop {
        hlt();  // sleep until next interrupt
        if let Some(c) = take LINE_CHAR {
            match c {
                b'\n' => { GET_LINE_ACTIVE = false; return pos; }
                8     => { if pos > 0 { pos -= 1; backspace(); } }
                _     => { if pos < buf.len() { buf[pos] = c; pos += 1; echo(c); } }
            }
        }
    }
}
}

The keyboard handler checks the flag and routes accordingly:

#![allow(unused)]
fn main() {
if GET_LINE_ACTIVE {
    LINE_CHAR = Some(ascii);  // goes to get_line
} else {
    shell::handle_input(ascii);  // goes to NPS
}
}

The Volatile Problem

Because GET_LINE_ACTIVE and LINE_CHAR are shared between the interrupt handler and the main loop, the compiler might cache their values in registers and never re-read from memory. This causes kbhandler to always see GET_LINE_ACTIVE = false even after get_line sets it to true.

The fix is volatile reads and writes — they force the compiler to always go to memory:

#![allow(unused)]
fn main() {
// Writing
core::ptr::write_volatile(core::ptr::addr_of_mut!(GET_LINE_ACTIVE), true);

// Reading  
let active = core::ptr::read_volatile(core::ptr::addr_of!(GET_LINE_ACTIVE));
}

Same treatment for LINE_CHAR. Without this, the whole thing silently breaks.

The Deadlock Problem

Here’s the catch. Our NPS shell calls get_line from inside cmd_getline, which is called from handle_char, which is called from kbhandler — which runs inside an interrupt handler.

The flow looks like this:

keyboard interrupt fires
→ cli (interrupts disabled)
→ kb_pic_handler (assembly)
→ kbhandler (Rust)
→ shell::handle_input
→ cmd_getline
→ get_line
→ hlt ← waiting for a keyboard interrupt that can NEVER fire
because cli disabled interrupts at the top!

This causes the deadlock. The kernel freezes waiting for an interrupt that will never arrive.

The KFS_5 Fix

The proper solution is a scheduler. Instead of hlt, get_line should put the current process to sleep and yield CPU time to other processes. When the keyboard interrupt fires and a complete line is ready, the scheduler wakes the sleeping process back up.

The infrastructure is already in place:

  • GET_LINE_ACTIVE flag — tells the keyboard handler which mode we’re in
  • LINE_CHAR volatile shared state — the mailbox between handler and consumer
  • take_char() / is_getline_active() API — clean interface for checking state

All that’s missing is the sleep/wake mechanism — a process_sleep() call instead of hlt, and a process_wake() call from the keyboard handler when Enter is pressed. That’s KFS_5’s job.