From f7f01bdbb8430e015948ac50e7aa16f9a0cd42b4 Mon Sep 17 00:00:00 2001 From: Stefan Lankes Date: Sat, 16 Mar 2024 21:57:17 +0100 Subject: [PATCH 1/2] add async interrupt safe synchronization primitives An asynchronous interface has been added to the interrupt-safe mutex so that interrupt handlers and asynchronous tasks can be synchronized. The mutex was demonstrated by using the shell as an example, where the asynchronous task communicates with the serial interface and its interrupt handler. --- Cargo.lock | 1 + Cargo.toml | 3 +- src/arch/x86_64/kernel/mod.rs | 4 +- src/shell/constants.rs | 11 ++ src/{shell.rs => shell/mod.rs} | 21 ++-- src/shell/shell.rs | 190 +++++++++++++++++++++++++++++++++ src/shell/writer.rs | 44 ++++++++ src/synch/async.rs | 128 ++++++++++++++++++++++ src/synch/mod.rs | 1 + 9 files changed, 389 insertions(+), 14 deletions(-) create mode 100644 src/shell/constants.rs rename src/{shell.rs => shell/mod.rs} (70%) create mode 100644 src/shell/shell.rs create mode 100644 src/shell/writer.rs create mode 100644 src/synch/async.rs diff --git a/Cargo.lock b/Cargo.lock index 2f6e0149c2..400bd056c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -616,6 +616,7 @@ dependencies = [ "hermit-entry", "hermit-macro", "hermit-sync", + "interrupts", "llvm-tools", "lock_api", "log", diff --git a/Cargo.toml b/Cargo.toml index b9ce5d59c8..16f404e704 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,7 +68,7 @@ vga = [] common-os = [] nostd = [] semihosting = ["dep:semihosting"] -shell = ["simple-shell"] +shell = [] [dependencies] hermit-macro = { path = "hermit-macro" } @@ -106,6 +106,7 @@ talc = { version = "4" } time = { version = "0.3", default-features = false } volatile = { version = "0.5.4", features = ["unstable"] } zerocopy = { version = "0.7", features = ["derive"] } +interrupts = "0.1" [dependencies.smoltcp] version = "0.11" diff --git a/src/arch/x86_64/kernel/mod.rs b/src/arch/x86_64/kernel/mod.rs index aef2bc5768..e888f42381 100644 --- a/src/arch/x86_64/kernel/mod.rs +++ b/src/arch/x86_64/kernel/mod.rs @@ -5,13 +5,13 @@ use core::ptr; use core::sync::atomic::{AtomicPtr, AtomicU32, Ordering}; use hermit_entry::boot_info::{BootInfo, PlatformInfo, RawBootInfo}; -use hermit_sync::InterruptSpinMutex; use x86::controlregs::{cr0, cr0_write, cr4, Cr0}; use self::serial::SerialPort; use crate::arch::mm::{PhysAddr, VirtAddr}; use crate::arch::x86_64::kernel::core_local::*; use crate::env::{self, is_uhyve}; +use crate::synch::r#async::AsyncInterruptMutex; #[cfg(feature = "acpi")] pub mod acpi; @@ -52,7 +52,7 @@ pub fn raw_boot_info() -> &'static RawBootInfo { } /// Serial port to print kernel messages -pub(crate) static COM1: InterruptSpinMutex> = InterruptSpinMutex::new(None); +pub(crate) static COM1: AsyncInterruptMutex> = AsyncInterruptMutex::new(None); pub fn get_ram_address() -> PhysAddr { PhysAddr(boot_info().hardware_info.phys_addr_range.start) diff --git a/src/shell/constants.rs b/src/shell/constants.rs new file mode 100644 index 0000000000..cca9056139 --- /dev/null +++ b/src/shell/constants.rs @@ -0,0 +1,11 @@ +pub(crate) const CTRL_L: u8 = 12; +pub(crate) const ENTER: u8 = 13; +pub(crate) const BACKSPACE: u8 = 127; +pub(crate) const CTRL_C: u8 = 3; +pub(crate) const ESCAPE: u8 = 27; + +pub(crate) const CSI: u8 = 91; +pub(crate) const CSI_UP: u8 = 65; +pub(crate) const CSI_DOWN: u8 = 66; +pub(crate) const CSI_RIGHT: u8 = 67; +pub(crate) const CSI_LEFT: u8 = 68; diff --git a/src/shell.rs b/src/shell/mod.rs similarity index 70% rename from src/shell.rs rename to src/shell/mod.rs index 52cab9ac17..daea99241d 100644 --- a/src/shell.rs +++ b/src/shell/mod.rs @@ -1,16 +1,17 @@ +/// This shell implementation is derived form +/// https://github.com/explodingcamera/pogos/tree/main/crates/simple-shell use hermit_sync::Lazy; -use simple_shell::*; -use crate::arch::kernel::COM1; +use self::shell::*; use crate::interrupts::print_statistics; -fn read() -> Option { - COM1.lock().as_mut().map(|s| s.read())? -} +mod constants; +mod shell; +mod writer; static mut SHELL: Lazy> = Lazy::new(|| { - let (print, read) = (|s: &str| print!("{}", s), read); - let mut shell = Shell::new(print, read); + let print = |s: &str| print!("{}", s); + let mut shell = Shell::new(print); shell.commands.insert( "help", @@ -27,7 +28,7 @@ static mut SHELL: Lazy> = Lazy::new(|| { "interrupts", ShellCommand { help: "Shows the number of received interrupts", - func: |_, shell| { + func: |_, _shell| { print_statistics(); Ok(()) }, @@ -38,9 +39,8 @@ static mut SHELL: Lazy> = Lazy::new(|| { "shutdown", ShellCommand { help: "Shutdown HermitOS", - func: |_, shell| { + func: |_, _shell| { crate::shutdown(0); - Ok(()) }, aliases: &["s"], }, @@ -50,6 +50,5 @@ static mut SHELL: Lazy> = Lazy::new(|| { }); pub(crate) fn init() { - // Also supports async crate::executor::spawn(unsafe { SHELL.run_async() }); } diff --git a/src/shell/shell.rs b/src/shell/shell.rs new file mode 100644 index 0000000000..d2a25ee5f3 --- /dev/null +++ b/src/shell/shell.rs @@ -0,0 +1,190 @@ +use alloc::collections::BTreeMap; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; +use core::future::{self, Future}; +use core::task::{ready, Poll}; + +use super::constants::*; +use super::writer::*; +use crate::arch::kernel::COM1; + +#[derive(Clone, Copy)] +pub struct ShellCommand<'a> { + pub help: &'a str, + pub func: fn(&[&str], &mut Shell<'a>) -> Result<(), &'a str>, + pub aliases: &'a [&'a str], +} + +pub struct Shell<'a> { + pub history: Vec, + pub commands: BTreeMap<&'a str, ShellCommand<'a>>, + pub command: String, + pub cursor: usize, + writer: Writer, +} + +impl<'a> Shell<'a> { + pub fn new(write: fn(&str)) -> Self { + Self { + history: Vec::new(), + commands: BTreeMap::new(), + command: String::new(), + cursor: 0, + writer: Writer::new(write), + } + } + + async fn get_char_async(&self) -> u8 { + future::poll_fn(|cx| { + let mut pinned = core::pin::pin!(COM1.async_lock()); + let mut guard = ready!(pinned.as_mut().poll(cx)); + if let Some(Some(c)) = guard.as_mut().map(|s| s.read()) { + Poll::Ready(c) + } else { + cx.waker().wake_by_ref(); + Poll::Pending + } + }) + .await + } + + #[allow(dead_code)] + pub fn with_commands(mut self, mut commands: BTreeMap<&'a str, ShellCommand<'a>>) -> Self { + self.commands.append(&mut commands); + self + } + + pub async fn run_async(&mut self) { + self.print_prompt(); + + loop { + let c = self.get_char_async().await; + match c { + ESCAPE => self.handle_escape_async().await, + _ => self.match_char(c), + } + } + } + + fn match_char(&mut self, b: u8) { + match b { + CTRL_C => self.process_command("exit".to_string()), + CTRL_L => self.handle_clear(), + ENTER => self.handle_enter(), + BACKSPACE => self.handle_backspace(), + c if (32..=126).contains(&c) => { + self.command.insert(self.cursor, c as char); + self.cursor += 1; + + if self.cursor < self.command.len() { + // Print the remaining text + shell_print!(self.writer, "{}", &self.command[self.cursor - 1..]); + // Move cursor to the correct position + shell_print!(self.writer, "\x1b[{}D", self.command.len() - self.cursor); + } else { + shell_print!(self.writer, "{}", c as char); + } + } + _ => {} + } + } + + fn handle_clear(&mut self) { + self.clear_screen(); + self.print_prompt(); + shell_print!(self.writer, "{}", self.command); + self.cursor = self.command.len(); + } + + fn handle_backspace(&mut self) { + if self.cursor > 0 { + self.command.remove(self.cursor - 1); + self.cursor -= 1; + shell_print!(self.writer, "\x08"); // Move cursor left + shell_print!(self.writer, "{}", &self.command[self.cursor..]); // Print the remaining text + shell_print!(self.writer, " "); // Clear last character + shell_print!( + self.writer, + "\x1b[{}D", + self.command.len() - self.cursor + 1 + ); + // Move cursor to the correct position + } + } + + fn handle_enter(&mut self) { + shell_println!(self.writer, ""); + self.process_command(self.command.clone()); + self.history.push(self.command.clone()); + self.command.clear(); + self.cursor = 0; + self.print_prompt(); + } + + async fn handle_escape_async(&mut self) { + if self.get_char_async().await != CSI { + return; + } + let b = self.get_char_async().await; + self._handle_escape(b); + } + + fn _handle_escape(&mut self, b: u8) { + match b { + CSI_UP => {} + CSI_DOWN => {} + CSI_RIGHT => { + if self.cursor < self.command.len() { + shell_print!(self.writer, "\x1b[1C"); + self.cursor += 1; + } + } + CSI_LEFT => { + if self.cursor > 0 { + shell_print!(self.writer, "\x1b[1D"); + self.cursor -= 1; + } + } + _ => {} + } + } + + fn process_command(&mut self, command: String) { + let mut args = command.split_whitespace(); + let command = args.next().unwrap_or(""); + let args = args.collect::>(); + + for (name, shell_command) in &self.commands { + if shell_command.aliases.contains(&command) || name == &command { + return (shell_command.func)(&args, self).unwrap_or_else(|err| { + shell_println!(self.writer, "{}: {}", command, err); + }); + } + } + + if command.is_empty() { + return; + } + + shell_println!(self.writer, "{}: command not found", command); + } + + pub fn print_help_screen(&mut self) { + shell_println!(self.writer, "available commands:"); + for (name, command) in &self.commands { + shell_print!(self.writer, " {:<12}{:<25}", name, command.help); + if !command.aliases.is_empty() { + shell_print!(self.writer, " aliases: {}", command.aliases.join(", ")); + } + shell_println!(self.writer, ""); + } + } + + pub fn print_prompt(&mut self) { + shell_print!(self.writer, "> "); + } + + pub fn clear_screen(&mut self) { + shell_print!(self.writer, "\x1b[2J\x1b[1;1H"); + } +} diff --git a/src/shell/writer.rs b/src/shell/writer.rs new file mode 100644 index 0000000000..5d1aef41bf --- /dev/null +++ b/src/shell/writer.rs @@ -0,0 +1,44 @@ +use core::fmt::Write; + +pub(crate) struct Writer { + print: fn(&str), +} + +impl core::fmt::Write for Writer { + fn write_str(&mut self, s: &str) -> core::fmt::Result { + (self.print)(s); + Ok(()) + } +} + +impl Writer { + pub fn new(print: fn(&str)) -> Self { + Self { print } + } + + pub fn print(&mut self, t: &str) { + self.write_str(t).unwrap(); + } + + pub fn print_args(&mut self, t: core::fmt::Arguments<'_>) { + self.write_fmt(t).unwrap(); + } +} + +macro_rules! shell_print { + ($writer:expr, $fmt:literal$(, $($arg: tt)+)?) => { + $writer.print_args(format_args!($fmt $(,$($arg)+)?)) + } +} + +macro_rules! shell_println { + ($writer:expr, $fmt:literal$(, $($arg: tt)+)?) => {{ + shell_print!($writer, $fmt $(,$($arg)+)?); + $writer.print("\n"); + }}; + () => { + $writer.print("\n"); + } +} + +pub(crate) use {shell_print, shell_println}; diff --git a/src/synch/async.rs b/src/synch/async.rs new file mode 100644 index 0000000000..bd78bace6f --- /dev/null +++ b/src/synch/async.rs @@ -0,0 +1,128 @@ +use core::cell::UnsafeCell; +use core::future; +use core::mem::MaybeUninit; +use core::ops::{Deref, DerefMut}; +use core::sync::atomic::{AtomicBool, Ordering}; +use core::task::Poll; + +use crossbeam_utils::Backoff; + +#[derive(Debug)] +pub(crate) struct AsyncInterruptMutex { + lock: AtomicBool, + interrupt_guard: UnsafeCell>, + data: UnsafeCell, +} + +/// A guard to which the protected data can be accessed +/// +/// When the guard falls out of scope it will release the lock. +#[derive(Debug)] +pub(crate) struct AsyncInterruptMutexGuard<'a, T: ?Sized> { + lock: &'a AtomicBool, + interrupt_guard: &'a UnsafeCell>, + data: &'a mut T, +} + +unsafe impl Sync for AsyncInterruptMutex {} +unsafe impl Send for AsyncInterruptMutex {} + +impl AsyncInterruptMutex { + pub const fn new(data: T) -> AsyncInterruptMutex { + Self { + lock: AtomicBool::new(false), + interrupt_guard: UnsafeCell::new(MaybeUninit::uninit()), + data: UnsafeCell::new(data), + } + } + + #[inline] + fn obtain_lock(&self) { + let backoff = Backoff::new(); + let guard = interrupts::disable(); + while self + .lock + .compare_exchange_weak(false, true, Ordering::Acquire, Ordering::Relaxed) + .is_err() + { + while self.is_locked() { + backoff.snooze(); + } + } + // SAFETY: We have exclusive access through locking `inner`. + unsafe { + self.interrupt_guard.get().write(MaybeUninit::new(guard)); + } + } + + #[inline] + fn is_locked(&self) -> bool { + self.lock.load(Ordering::Relaxed) + } + + pub fn lock(&self) -> AsyncInterruptMutexGuard<'_, T> { + self.obtain_lock(); + AsyncInterruptMutexGuard { + lock: &self.lock, + interrupt_guard: &self.interrupt_guard, + data: unsafe { &mut *self.data.get() }, + } + } + + #[inline] + fn obtain_try_lock(&self) -> bool { + let guard = interrupts::disable(); + let ok = self + .lock + .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) + .is_ok(); + if ok { + // SAFETY: We have exclusive access through locking `inner`. + unsafe { + self.interrupt_guard.get().write(MaybeUninit::new(guard)); + } + } + + ok + } + + pub async fn async_lock(&self) -> AsyncInterruptMutexGuard<'_, T> { + future::poll_fn(|cx| { + if self.obtain_try_lock() { + Poll::Ready(AsyncInterruptMutexGuard { + lock: &self.lock, + interrupt_guard: &self.interrupt_guard, + data: unsafe { &mut *self.data.get() }, + }) + } else { + cx.waker().wake_by_ref(); + Poll::Pending + } + }) + .await + } +} + +impl<'a, T: ?Sized> Deref for AsyncInterruptMutexGuard<'a, T> { + type Target = T; + fn deref<'b>(&'b self) -> &'b T { + &*self.data + } +} + +impl<'a, T: ?Sized> DerefMut for AsyncInterruptMutexGuard<'a, T> { + fn deref_mut<'b>(&'b mut self) -> &'b mut T { + &mut *self.data + } +} + +impl<'a, T: ?Sized> Drop for AsyncInterruptMutexGuard<'a, T> { + /// The dropping of the AsyncInterruptMutexGuard will release the lock it was created from. + fn drop(&mut self) { + // SAFETY: We have exclusive access through locking `inner`. + let guard = unsafe { self.interrupt_guard.get().replace(MaybeUninit::uninit()) }; + // SAFETY: `guard` was initialized when locking. + let _guard = unsafe { guard.assume_init() }; + self.lock.store(false, Ordering::Release); + } +} diff --git a/src/synch/mod.rs b/src/synch/mod.rs index a60e68331f..ca7be88716 100644 --- a/src/synch/mod.rs +++ b/src/synch/mod.rs @@ -1,5 +1,6 @@ //! Synchronization primitives +pub(crate) mod r#async; pub mod futex; #[cfg(feature = "newlib")] pub mod recmutex; From 56692ae612802f509e776491ca33a04f72f6c89a Mon Sep 17 00:00:00 2001 From: Stefan Lankes Date: Sat, 16 Mar 2024 22:15:44 +0100 Subject: [PATCH 2/2] remove clippy warnings --- src/synch/async.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/synch/async.rs b/src/synch/async.rs index bd78bace6f..6cf3352f37 100644 --- a/src/synch/async.rs +++ b/src/synch/async.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + use core::cell::UnsafeCell; use core::future; use core::mem::MaybeUninit; @@ -105,13 +107,13 @@ impl AsyncInterruptMutex { impl<'a, T: ?Sized> Deref for AsyncInterruptMutexGuard<'a, T> { type Target = T; - fn deref<'b>(&'b self) -> &'b T { + fn deref(&self) -> &T { &*self.data } } impl<'a, T: ?Sized> DerefMut for AsyncInterruptMutexGuard<'a, T> { - fn deref_mut<'b>(&'b mut self) -> &'b mut T { + fn deref_mut(&mut self) -> &mut T { &mut *self.data } }