You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Andre Richter 089565762a
Makefiles: Docker USB passthrough in Linux only
Detect if we run on Linux. If so, dockerize Makefile targets that need access to
USB devices. If we run on anything else, attempt to execute natively.

This will enable non-Linux Unixes like macOS to run the chainboot target, given
they install the needed Ruby dependencies.
4 years ago
..
.vscode Switch to rust-analyzer 4 years ago
src Refactor tutorial 05 4 years ago
Cargo.lock Bump to register 0.5.x 4 years ago
Cargo.toml Remove xbuild. Use upstream aarch64 rust-std 🎉 🦀 4 years ago
Makefile Makefiles: Docker USB passthrough in Linux only 4 years ago
README.md Add README diffs 4 years ago
kernel Remove xbuild. Use upstream aarch64 rust-std 🎉 🦀 4 years ago
kernel8.img Remove xbuild. Use upstream aarch64 rust-std 🎉 🦀 4 years ago

README.md

Tutorial 05 - 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 muts 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 primivite. 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. That is also why it is implemented in the same file as the interface itself. In later tutorials, an implementation might move to the _arch once it pulls in arch-specific code that can not be further abstracted.

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 pure Rust!
[1] Chars written: 27
[2] Stopping here.

Diff to previous


diff -uNr 04_zero_overhead_abstraction/src/bsp/raspberrypi/console.rs 05_safe_globals/src/bsp/raspberrypi/console.rs
--- 04_zero_overhead_abstraction/src/bsp/raspberrypi/console.rs
+++ 05_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 impl 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.
+        let mut r = &self.inner;
+        r.lock(|inner| fmt::Write::write_fmt(inner, args))
+    }
+}
+
+impl console::interface::Statistics for QEMUOutput {
+    fn chars_written(&self) -> usize {
+        let mut r = &self.inner;
+        r.lock(|inner| inner.chars_written)
+    }
 }

diff -uNr 04_zero_overhead_abstraction/src/console.rs 05_safe_globals/src/console.rs
--- 04_zero_overhead_abstraction/src/console.rs
+++ 05_safe_globals/src/console.rs
@@ -10,10 +10,22 @@

 /// 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;
 }

diff -uNr 04_zero_overhead_abstraction/src/main.rs 05_safe_globals/src/main.rs
--- 04_zero_overhead_abstraction/src/main.rs
+++ 05_safe_globals/src/main.rs
@@ -95,6 +95,7 @@
 #![feature(format_args_nl)]
 #![feature(naked_functions)]
 #![feature(panic_info_message)]
+#![feature(trait_alias)]
 #![no_main]
 #![no_std]

@@ -108,6 +109,7 @@
 mod panic_wait;
 mod print;
 mod runtime_init;
+mod synchronization;

 /// Early init code.
 ///
@@ -115,8 +117,15 @@
 ///
 /// - Only a single core must be active and running this function.
 unsafe fn kernel_init() -> ! {
+    use console::interface::Statistics;
+
     println!("[0] Hello from pure Rust!");

-    println!("[1] Stopping here.");
+    println!(
+        "[1] Chars written: {}",
+        bsp::console::console().chars_written()
+    );
+
+    println!("[2] Stopping here.");
     cpu::wait_forever()
 }

diff -uNr 04_zero_overhead_abstraction/src/synchronization.rs 05_safe_globals/src/synchronization.rs
--- 04_zero_overhead_abstraction/src/synchronization.rs
+++ 05_safe_globals/src/synchronization.rs
@@ -0,0 +1,91 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+//
+// Copyright (c) 2020 Andre Richter <andre.o.richter@gmail.com>
+
+//! Synchronization primitives.
+
+use core::cell::UnsafeCell;
+
+//--------------------------------------------------------------------------------------------------
+// Public Definitions
+//--------------------------------------------------------------------------------------------------
+
+/// Synchronization interfaces.
+pub mod interface {
+
+    /// Any object implementing this trait guarantees exclusive access to the data contained within
+    /// the Mutex for the duration of the provided closure.
+    ///
+    /// The trait follows the [Rust embedded WG's
+    /// proposal](https://github.com/korken89/wg/blob/master/rfcs/0377-mutex-trait.md) and therefore
+    /// provides some goodness such as [deadlock
+    /// prevention](https://github.com/korken89/wg/blob/master/rfcs/0377-mutex-trait.md#design-decisions-and-compatibility).
+    ///
+    /// # Example
+    ///
+    /// Since the lock function takes an `&mut self` to enable deadlock-prevention, the trait is
+    /// best implemented **for a reference to a container struct**, and has a usage pattern that
+    /// might feel strange at first:
+    ///
+    /// ```
+    /// static MUT: Mutex<RefCell<i32>> = Mutex::new(RefCell::new(0));
+    ///
+    /// fn foo() {
+    ///     let mut r = &MUT; // Note that r is mutable
+    ///     r.lock(|data| *data += 1);
+    /// }
+    /// ```
+    pub trait Mutex {
+        /// The type of encapsulated data.
+        type Data;
+
+        /// Creates a critical section and grants temporary mutable access to the encapsulated data.
+        fn lock<R>(&mut self, f: impl FnOnce(&mut Self::Data) -> R) -> R;
+    }
+}
+
+/// A pseudo-lock for teaching purposes.
+///
+/// Used to introduce [interior mutability].
+///
+/// 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.
+///
+/// [interior mutability]: https://doc.rust-lang.org/std/cell/index.html
+pub struct NullLock<T: ?Sized> {
+    data: UnsafeCell<T>,
+}
+
+//--------------------------------------------------------------------------------------------------
+// Public Code
+//--------------------------------------------------------------------------------------------------
+
+unsafe impl<T: ?Sized> Sync for NullLock<T> {}
+
+impl<T> NullLock<T> {
+    /// Wraps `data` into a new `NullLock`.
+    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>(&mut 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)
+    }
+}