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_ACTIVEflag — tells the keyboard handler which mode we’re inLINE_CHARvolatile shared state — the mailbox between handler and consumertake_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.