Skip to content

feat: add PS/2 keyboard interrupt driver - #2532

Open
GloriousAlpaca wants to merge 7 commits into
hermit-os:mainfrom
GloriousAlpaca:pr-keyboard-clean
Open

feat: add PS/2 keyboard interrupt driver#2532
GloriousAlpaca wants to merge 7 commits into
hermit-os:mainfrom
GloriousAlpaca:pr-keyboard-clean

Conversation

@GloriousAlpaca

Copy link
Copy Markdown

This feature adds support for the PS2 legacy keyboard in Qemu x86_64.
Currently it does:

  1. A minimized hardware initialization (e.g. flush the output buffer) to use the keyboard, according to the standards for the Intel 8042 chip: Link to OSDev
  2. Install an interrupt handler, which is done in the install_handlers function in interrupt.rs, as it is a hardware interrupt but does not belong in the pci interrupt handlers list (legacy device).
  3. Add a systemcall to pop the last scancode out of the keyboard input ringbuffer (which currently has a length of 256). The Ringbuffer also uses Atomics, to prevent races and deadlocks.
    The systemcall returns 0 if the keyboard feature is disabled.

I have only tested this feature with C programs on a Mac using Qemu.

@mkroening mkroening self-assigned this Jul 6, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark Results

Details
Benchmark Current: 9afbade Previous: 2e23902 Performance Ratio
startup_benchmark Build Time 126.10 s 80.34 s 1.57
startup_benchmark File Size 0.77 MB 0.80 MB 0.97
Startup Time - 1 core 0.94 s (±0.02 s) 0.75 s (±0.02 s) 1.26
Startup Time - 2 cores 0.96 s (±0.04 s) 0.74 s (±0.02 s) 1.31
Startup Time - 4 cores 0.98 s (±0.04 s) 0.74 s (±0.02 s) 1.33
multithreaded_benchmark Build Time 125.62 s 82.11 s 1.53
multithreaded_benchmark File Size 0.83 MB 0.86 MB 0.97
Multithreaded Pi Efficiency - 2 Threads 64.48 % (±8.42 %) 85.89 % (±6.61 %) 0.75
Multithreaded Pi Efficiency - 4 Threads 36.52 % (±4.07 %) 43.43 % (±2.56 %) 0.84
Multithreaded Pi Efficiency - 8 Threads 21.49 % (±1.94 %) 25.76 % (±1.53 %) 0.83
micro_benchmarks Build Time 124.34 s 80.40 s 1.55
micro_benchmarks File Size 0.83 MB 0.86 MB 0.97
Scheduling time - 1 thread 78.04 ticks (±3.56 ticks) 62.65 ticks (±4.06 ticks) 1.25
Scheduling time - 2 threads 54.74 ticks (±17.17 ticks) 34.08 ticks (±4.10 ticks) 1.61
Micro - Time for syscall (getpid) 4.27 ticks (±0.32 ticks) 3.45 ticks (±0.58 ticks) 1.24
Memcpy speed - (built_in) block size 4096 56720.74 MByte/s (±39303.87 MByte/s) 82448.38 MByte/s (±56997.13 MByte/s) 0.69
Memcpy speed - (built_in) block size 1048576 29337.44 MByte/s (±23911.65 MByte/s) 30585.98 MByte/s (±24707.84 MByte/s) 0.96
Memcpy speed - (built_in) block size 16777216 28588.14 MByte/s (±23656.77 MByte/s) 26340.06 MByte/s (±21720.96 MByte/s) 1.09
Memset speed - (built_in) block size 4096 56921.26 MByte/s (±39459.25 MByte/s) 82292.76 MByte/s (±56891.50 MByte/s) 0.69
Memset speed - (built_in) block size 1048576 30051.94 MByte/s (±24326.42 MByte/s) 31323.85 MByte/s (±25145.86 MByte/s) 0.96
Memset speed - (built_in) block size 16777216 29337.79 MByte/s (±24081.68 MByte/s) 27104.68 MByte/s (±22209.94 MByte/s) 1.08
Memcpy speed - (rust) block size 4096 52014.76 MByte/s (±36688.83 MByte/s) 74097.96 MByte/s (±51811.44 MByte/s) 0.70
Memcpy speed - (rust) block size 1048576 29216.27 MByte/s (±23812.96 MByte/s) 30361.60 MByte/s (±24602.37 MByte/s) 0.96
Memcpy speed - (rust) block size 16777216 28537.90 MByte/s (±23603.90 MByte/s) 27625.34 MByte/s (±22806.88 MByte/s) 1.03
Memset speed - (rust) block size 4096 52505.83 MByte/s (±37054.89 MByte/s) 74373.47 MByte/s (±51976.48 MByte/s) 0.71
Memset speed - (rust) block size 1048576 29950.54 MByte/s (±24234.44 MByte/s) 31110.89 MByte/s (±25033.24 MByte/s) 0.96
Memset speed - (rust) block size 16777216 29267.09 MByte/s (±24014.38 MByte/s) 28386.93 MByte/s (±23265.03 MByte/s) 1.03
alloc_benchmarks Build Time 120.37 s 74.76 s 1.61
alloc_benchmarks File Size 0.85 MB 0.87 MB 0.97
Allocations - Allocation success 91.35 % 91.31 % 1.00
Allocations - Deallocation success 100.00 % 100.00 % 1
Allocations - Pre-fail Allocations 61.55 % 61.44 % 1.00
Allocations - Average Allocation time 5654.02 Ticks (±224.98 Ticks) 5860.58 Ticks (±98.43 Ticks) 0.96
Allocations - Average Allocation time (no fail) 6316.33 Ticks (±232.98 Ticks) 6554.81 Ticks (±92.86 Ticks) 0.96
Allocations - Average Deallocation time 1320.59 Ticks (±478.29 Ticks) 1805.01 Ticks (±250.35 Ticks) 0.73
mutex_benchmark Build Time 121.25 s 79.82 s 1.52
mutex_benchmark File Size 0.83 MB 0.86 MB 0.97
Mutex Stress Test Average Time per Iteration - 1 Threads 14.80 ns (±0.75 ns) 12.10 ns (±0.41 ns) 1.22
Mutex Stress Test Average Time per Iteration - 2 Threads 24.52 ns (±8.33 ns) 40.26 ns (±1.68 ns) 0.61

This comment was automatically generated by workflow using github-action-benchmark.

@GloriousAlpaca
GloriousAlpaca force-pushed the pr-keyboard-clean branch 2 times, most recently from 2c424d5 to f6c859b Compare July 6, 2026 16:30

@mkroening mkroening left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR! :)

I was wondering why implement drivers for legacy devices instead of USB keyboards (xhci, usb-oxide, embassy-usb). I guess it is because of simplicity.

It would be great to discuss the high-level application-facing API, since that is the hardest to change once merged.

Comment thread Cargo.toml Outdated
Comment thread Cargo.toml Outdated
Comment thread src/syscalls/system.rs Outdated
Comment thread src/arch/x86_64/kernel/interrupts.rs Outdated
Comment thread src/arch/x86_64/kernel/interrupts.rs Outdated
Comment thread src/arch/x86_64/kernel/pc_keyboard.rs Outdated
Comment thread src/arch/x86_64/kernel/pc_keyboard.rs Outdated
Comment thread src/arch/x86_64/kernel/pc_keyboard.rs Outdated
Comment thread src/arch/x86_64/kernel/pc_keyboard.rs Outdated
Comment on lines +63 to +64
/// Pops a scancode from the keyboard buffer, returning None if the buffer is empty.
pub fn pop_scancode() -> Option<u8> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A scancode can never be zero, right? Returning Option<NonZero<u8>> would be preferable in that case.

Comment thread src/syscalls/system.rs Outdated
Comment on lines +10 to +15
#[cfg(all(target_arch = "x86_64", feature = "keyboard"))]
#[hermit_macro::system]
#[unsafe(no_mangle)]
pub extern "C" fn sys_read_keyboard() -> u8 {
crate::kernel::keyboard::pop_scancode().unwrap_or(0)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not too sure about this API. Is the application supposed to busy loop on this and retrieve one event at a time?

What about doing something similar to Linux's event device (evdev) interface (Linux docs)? Reading from /dev/input/event0 would then fill a user buffer with input events and blocks if no events are there unless opened with O_NONBLOCK.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we don't use a filesystem like Linux I would propose something like this:
pub extern "C" fn sys_read_keyboard(buffer: *mut u8 ,size: usize, nonblock: bool) -> isize
(Should I also rename the systemcall to sys_read_pc_keyboard?)
This would be pretty flexible, we can throw standard errorcodes or return the buffersize in bytes like this.
We can then use a semaphore to block the thread until there are scancodes in the vecdeque.

@GloriousAlpaca
GloriousAlpaca force-pushed the pr-keyboard-clean branch 3 times, most recently from a7180a7 to 153302f Compare July 17, 2026 14:24
Comment thread src/arch/x86_64/kernel/pc_keyboard.rs Outdated
Comment thread src/arch/x86_64/kernel/pc_keyboard.rs Outdated
Comment thread src/arch/x86_64/kernel/pc_keyboard.rs Outdated
Comment thread src/arch/x86_64/kernel/pc_keyboard.rs Outdated
Comment on lines +47 to +53
let scancode = Ps2::read_data();
let mut buffer = KEYBOARD_BUFFER.lock();

if buffer.len() >= BUFFER_SIZE {
buffer.pop_front();
}
buffer.push_back(scancode);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if retaining the keys in the queue is how this is handled best. I'm thinking that maybe adding a timestamp to each key event and discarding it after x seconds is a correct approach. But maybe I'm prematurely optimizing this. It would be interesting to know how other systems are handling this.

Comment thread Cargo.toml
##
## This feature initializes the PS/2 keyboard controller and installs a keyboard interrupt handler.
## It also provides a system call to receive the last scancode from the internal keyboard buffer.
## Note that this is not a complete keyboard driver and not needed for general keyboard support.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
## Note that this is not a complete keyboard driver and not needed for general keyboard support.
## Note that this is not a complete keyboard driver and not needed for serial input/output.

@GloriousAlpaca
GloriousAlpaca force-pushed the pr-keyboard-clean branch 3 times, most recently from 2167837 to 452458e Compare August 11, 2026 13:46
struct Ps2;
impl Ps2 {
pub fn read_status() -> u8 {
unsafe { Port::<u8>::new(PS2_CMD_PORT).read() }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a brief safety comment:

Suggested change
unsafe { Port::<u8>::new(PS2_CMD_PORT).read() }
// SAFETY: Correct port access without safety related side-effects.
unsafe { Port::<u8>::new(PS2_CMD_PORT).read() }

Comment thread src/syscalls/system.rs
#[cfg(all(target_arch = "x86_64", feature = "pc-keyboard"))]
#[hermit_macro::system]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn sys_read_keyboard(buffer: *mut u8, size: usize, nonblock: bool) -> isize {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a doc comment

Comment thread src/syscalls/system.rs
if size == 0 {
return 0;
}
let buffer_slice: &mut [u8] = unsafe { core::slice::from_raw_parts_mut(buffer, size) };

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let buffer_slice: &mut [u8] = unsafe { core::slice::from_raw_parts_mut(buffer, size) };
// SAFETY: We have to trust the user input, because we are a unikernel and if the user wants to crash the program they are free to do so.
let buffer_slice: &mut [u8] = unsafe { core::slice::from_raw_parts_mut(buffer, size) };

Comment thread src/syscalls/system.rs
#[cfg(all(target_arch = "x86_64", feature = "pc-keyboard"))]
#[hermit_macro::system]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn sys_read_keyboard(buffer: *mut u8, size: usize, nonblock: bool) -> isize {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a doc comment

const PS2_CNFG_ENABLE_KEYBOARD_INTERRUPT: u8 = 0x01;
const PS2_BUFFER_FULL: u8 = 0x01;

const BUFFER_SIZE: usize = 256;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const BUFFER_SIZE: usize = 256;
const MAX_INP_BUFFER_SIZE: usize = 256;

Comment on lines +56 to +69
let mut sem = true;
{
let mut buffer = KEYBOARD_BUFFER.lock();

// Pop the oldest scancode if the buffer is full.
if buffer.len() >= BUFFER_SIZE {
buffer.pop_front();
sem = false;
}
buffer.push_back(valid_scancode);
}
if sem {
KEYBOARD_SEMAPHORE.release();
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic is fine, but you could rewrite it to be a bit simpler by returning in the line that is currently sem = false (don't forget the push_back).

Comment thread src/syscalls/system.rs
#[cfg(all(target_arch = "x86_64", feature = "pc-keyboard"))]
#[hermit_macro::system]
#[unsafe(no_mangle)]
pub unsafe extern "C" fn sys_read_keyboard(buffer: *mut u8, size: usize, nonblock: bool) -> isize {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a doc comment

Comment thread src/syscalls/system.rs
if size == 0 {
return 0;
}
let buffer_slice: &mut [u8] = unsafe { core::slice::from_raw_parts_mut(buffer, size) };

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let buffer_slice: &mut [u8] = unsafe { core::slice::from_raw_parts_mut(buffer, size) };
// SAFETY: We have to trust the user input, because we are a unikernel and if the user wants to crash the program they are free to do so.
let buffer_slice: &mut [u8] = unsafe { core::slice::from_raw_parts_mut(buffer, size) };

}

static KEYBOARD_BUFFER: Lazy<InterruptTicketMutex<VecDeque<NonZeroU8>>> =
Lazy::new(|| InterruptTicketMutex::new(VecDeque::with_capacity(BUFFER_SIZE)));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Lazy::new(|| InterruptTicketMutex::new(VecDeque::with_capacity(BUFFER_SIZE)));
Lazy::new(|| InterruptTicketMutex::new(VecDeque::with_capacity(32)));

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants