fec4f9b6f2
- Remove the panic version of the GPIO and UART driver. While they were a neat idea, it proved tedious to drag them along different tutorials where the virtual memory situation kept on changing. Actually, not much is lost, since the benefit was only of theoretical nature until now, since everything is still single-threaded with NullLocks. It is still possible to re-introduce them later. - Refactor driver bringup starting with tutorial 14. Instantiating the drivers only when we are already capable of using the remapped MMIO address makes the kernel a lot more robust, and the drivers need not care whether their MMIO addresses are good to use already or not. - Use console and irq_manager references from the generic kernel code. This improves decoupling from the BSP, and is needed as a basis for tutorial 14. |
2 years ago | |
---|---|---|
.. | ||
.vscode | 2 years ago | |
src | 2 years ago | |
tests | 3 years ago | |
Cargo.lock | 2 years ago | |
Cargo.toml | 2 years ago | |
Makefile | 2 years ago | |
README.md | 2 years ago | |
build.rs | 2 years ago |
README.md
Tutorial 04 - Safe Globals
tl;dr
- A pseudo-lock is introduced.
- It is a first showcase of OS synchronization primitives and enables safe access to a global data structure.
Mutable globals in Rust
When we introduced the globally usable print!
macros in tutorial 03, we cheated a bit. Calling
core::fmt
's write_fmt()
function, which takes an &mut self
, was only working because on each
call, a new instance of QEMUOutput
was created.
If we would want to preserve some state, e.g. statistics about the number of characters written, we
need to make a single global instance of QEMUOutput
(in Rust, using the static
keyword).
A static QEMU_OUTPUT
, however, would not allow to call functions taking &mut self
. For that, we
would need a static mut
, but calling functions that mutate state on static mut
s is unsafe. The
Rust compiler's reasoning for this is that it can then not prevent anymore that multiple
cores/threads are mutating the data concurrently (it is a global, so everyone can reference it from
anywhere. The borrow checker can't help here).
The solution to this problem is to wrap the global into a synchronization primitive. In our case, a
variant of a MUTual EXclusion primitive. Mutex
is introduced as a trait in synchronization.rs
,
and implemented by the NullLock
in the same file. In order to make the code lean for teaching
purposes, it leaves out the actual architecture-specific logic for protection against concurrent
access, since we don't need it as long as the kernel only executes on a single core with interrupts
disabled.
The NullLock
focuses on showcasing the Rust core concept of interior mutability. Make sure to
read up on it. I also recommend to read this article about an accurate mental model for Rust's
reference types.
If you want to compare the NullLock
to some real-world mutex implementations, you can check out
implemntations in the spin crate or the parking lot crate.
Test it
$ make qemu
[...]
[0] Hello from Rust!
[1] Chars written: 22
[2] Stopping here.
Diff to previous
diff -uNr 03_hacky_hello_world/Cargo.toml 04_safe_globals/Cargo.toml
--- 03_hacky_hello_world/Cargo.toml
+++ 04_safe_globals/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "mingo"
-version = "0.3.0"
+version = "0.4.0"
authors = ["Andre Richter <andre.o.richter@gmail.com>"]
edition = "2021"
diff -uNr 03_hacky_hello_world/src/bsp/raspberrypi/console.rs 04_safe_globals/src/bsp/raspberrypi/console.rs
--- 03_hacky_hello_world/src/bsp/raspberrypi/console.rs
+++ 04_safe_globals/src/bsp/raspberrypi/console.rs
@@ -4,7 +4,7 @@
//! BSP console facilities.
-use crate::console;
+use crate::{console, synchronization, synchronization::NullLock};
use core::fmt;
//--------------------------------------------------------------------------------------------------
@@ -12,25 +12,64 @@
//--------------------------------------------------------------------------------------------------
/// A mystical, magical device for generating QEMU output out of the void.
-struct QEMUOutput;
+///
+/// The mutex protected part.
+struct QEMUOutputInner {
+ chars_written: usize,
+}
+
+//--------------------------------------------------------------------------------------------------
+// Public Definitions
+//--------------------------------------------------------------------------------------------------
+
+/// The main struct.
+pub struct QEMUOutput {
+ inner: NullLock<QEMUOutputInner>,
+}
+
+//--------------------------------------------------------------------------------------------------
+// Global instances
+//--------------------------------------------------------------------------------------------------
+
+static QEMU_OUTPUT: QEMUOutput = QEMUOutput::new();
//--------------------------------------------------------------------------------------------------
// Private Code
//--------------------------------------------------------------------------------------------------
+impl QEMUOutputInner {
+ const fn new() -> QEMUOutputInner {
+ QEMUOutputInner { chars_written: 0 }
+ }
+
+ /// Send a character.
+ fn write_char(&mut self, c: char) {
+ unsafe {
+ core::ptr::write_volatile(0x3F20_1000 as *mut u8, c as u8);
+ }
+
+ self.chars_written += 1;
+ }
+}
+
/// Implementing `core::fmt::Write` enables usage of the `format_args!` macros, which in turn are
/// used to implement the `kernel`'s `print!` and `println!` macros. By implementing `write_str()`,
/// we get `write_fmt()` automatically.
///
+/// The function takes an `&mut self`, so it must be implemented for the inner struct.
+///
/// See [`src/print.rs`].
///
/// [`src/print.rs`]: ../../print/index.html
-impl fmt::Write for QEMUOutput {
+impl fmt::Write for QEMUOutputInner {
fn write_str(&mut self, s: &str) -> fmt::Result {
for c in s.chars() {
- unsafe {
- core::ptr::write_volatile(0x3F20_1000 as *mut u8, c as u8);
+ // Convert newline to carrige return + newline.
+ if c == '\n' {
+ self.write_char('\r')
}
+
+ self.write_char(c);
}
Ok(())
@@ -41,7 +80,39 @@
// Public Code
//--------------------------------------------------------------------------------------------------
+impl QEMUOutput {
+ /// Create a new instance.
+ pub const fn new() -> QEMUOutput {
+ QEMUOutput {
+ inner: NullLock::new(QEMUOutputInner::new()),
+ }
+ }
+}
+
/// Return a reference to the console.
-pub fn console() -> impl console::interface::Write {
- QEMUOutput {}
+pub fn console() -> &'static dyn console::interface::All {
+ &QEMU_OUTPUT
}
+
+//------------------------------------------------------------------------------
+// OS Interface Code
+//------------------------------------------------------------------------------
+use synchronization::interface::Mutex;
+
+/// Passthrough of `args` to the `core::fmt::Write` implementation, but guarded by a Mutex to
+/// serialize access.
+impl console::interface::Write for QEMUOutput {
+ fn write_fmt(&self, args: core::fmt::Arguments) -> fmt::Result {
+ // Fully qualified syntax for the call to `core::fmt::Write::write_fmt()` to increase
+ // readability.
+ self.inner.lock(|inner| fmt::Write::write_fmt(inner, args))
+ }
+}
+
+impl console::interface::Statistics for QEMUOutput {
+ fn chars_written(&self) -> usize {
+ self.inner.lock(|inner| inner.chars_written)
+ }
+}
+
+impl console::interface::All for QEMUOutput {}
diff -uNr 03_hacky_hello_world/src/console.rs 04_safe_globals/src/console.rs
--- 03_hacky_hello_world/src/console.rs
+++ 04_safe_globals/src/console.rs
@@ -12,12 +12,24 @@
/// Console interfaces.
pub mod interface {
+ use core::fmt;
+
/// Console write functions.
- ///
- /// `core::fmt::Write` is exactly what we need for now. Re-export it here because
- /// implementing `console::Write` gives a better hint to the reader about the
- /// intention.
- pub use core::fmt::Write;
+ pub trait Write {
+ /// Write a Rust format string.
+ fn write_fmt(&self, args: fmt::Arguments) -> fmt::Result;
+ }
+
+ /// Console statistics.
+ pub trait Statistics {
+ /// Return the number of characters written.
+ fn chars_written(&self) -> usize {
+ 0
+ }
+ }
+
+ /// Trait alias for a full-fledged console.
+ pub trait All: Write + Statistics {}
}
//--------------------------------------------------------------------------------------------------
@@ -27,6 +39,6 @@
/// Return a reference to the console.
///
/// This is the global console used by all printing macros.
-pub fn console() -> impl interface::Write {
+pub fn console() -> &'static dyn interface::All {
bsp::console::console()
}
diff -uNr 03_hacky_hello_world/src/main.rs 04_safe_globals/src/main.rs
--- 03_hacky_hello_world/src/main.rs
+++ 04_safe_globals/src/main.rs
@@ -109,6 +109,7 @@
#![feature(asm_const)]
#![feature(format_args_nl)]
#![feature(panic_info_message)]
+#![feature(trait_alias)]
#![no_main]
#![no_std]
@@ -117,6 +118,7 @@
mod cpu;
mod panic_wait;
mod print;
+mod synchronization;
/// Early init code.
///
@@ -124,7 +126,12 @@
///
/// - Only a single core must be active and running this function.
unsafe fn kernel_init() -> ! {
- println!("Hello from Rust!");
+ use console::console;
- panic!("Stopping here.")
+ println!("[0] Hello from Rust!");
+
+ println!("[1] Chars written: {}", console().chars_written());
+
+ println!("[2] Stopping here.");
+ cpu::wait_forever()
}
diff -uNr 03_hacky_hello_world/src/print.rs 04_safe_globals/src/print.rs
--- 03_hacky_hello_world/src/print.rs
+++ 04_safe_globals/src/print.rs
@@ -13,8 +13,6 @@
#[doc(hidden)]
pub fn _print(args: fmt::Arguments) {
- use console::interface::Write;
-
console::console().write_fmt(args).unwrap();
}
diff -uNr 03_hacky_hello_world/src/synchronization.rs 04_safe_globals/src/synchronization.rs
--- 03_hacky_hello_world/src/synchronization.rs
+++ 04_safe_globals/src/synchronization.rs
@@ -0,0 +1,77 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+//
+// Copyright (c) 2020-2022 Andre Richter <andre.o.richter@gmail.com>
+
+//! Synchronization primitives.
+//!
+//! # Resources
+//!
+//! - <https://doc.rust-lang.org/book/ch16-04-extensible-concurrency-sync-and-send.html>
+//! - <https://stackoverflow.com/questions/59428096/understanding-the-send-trait>
+//! - <https://doc.rust-lang.org/std/cell/index.html>
+
+use core::cell::UnsafeCell;
+
+//--------------------------------------------------------------------------------------------------
+// Public Definitions
+//--------------------------------------------------------------------------------------------------
+
+/// Synchronization interfaces.
+pub mod interface {
+
+ /// Any object implementing this trait guarantees exclusive access to the data wrapped within
+ /// the Mutex for the duration of the provided closure.
+ pub trait Mutex {
+ /// The type of the data that is wrapped by this mutex.
+ type Data;
+
+ /// Locks the mutex and grants the closure temporary mutable access to the wrapped data.
+ fn lock<R>(&self, f: impl FnOnce(&mut Self::Data) -> R) -> R;
+ }
+}
+
+/// A pseudo-lock for teaching purposes.
+///
+/// In contrast to a real Mutex implementation, does not protect against concurrent access from
+/// other cores to the contained data. This part is preserved for later lessons.
+///
+/// The lock will only be used as long as it is safe to do so, i.e. as long as the kernel is
+/// executing single-threaded, aka only running on a single core with interrupts disabled.
+pub struct NullLock<T>
+where
+ T: ?Sized,
+{
+ data: UnsafeCell<T>,
+}
+
+//--------------------------------------------------------------------------------------------------
+// Public Code
+//--------------------------------------------------------------------------------------------------
+
+unsafe impl<T> Send for NullLock<T> where T: ?Sized + Send {}
+unsafe impl<T> Sync for NullLock<T> where T: ?Sized + Send {}
+
+impl<T> NullLock<T> {
+ /// Create an instance.
+ pub const fn new(data: T) -> Self {
+ Self {
+ data: UnsafeCell::new(data),
+ }
+ }
+}
+
+//------------------------------------------------------------------------------
+// OS Interface Code
+//------------------------------------------------------------------------------
+
+impl<T> interface::Mutex for NullLock<T> {
+ type Data = T;
+
+ fn lock<R>(&self, f: impl FnOnce(&mut Self::Data) -> R) -> R {
+ // In a real lock, there would be code encapsulating this line that ensures that this
+ // mutable reference will ever only be given out once at a time.
+ let data = unsafe { &mut *self.data.get() };
+
+ f(data)
+ }
+}