WARNING本文所有内容均为翻译,原文:Tutorial 04 - Safe Globals;原项目:Operating System development tutorials in Rust on the Raspberry Pi
概述
- 引入了伪锁。
- 本章首次展示了操作系统的同步机制,并使全局数据结构可以被安全的访问。
Rust中可变的全局变量
当我们在第三章中引入全局可用的print!宏时,使了个小手段。在调用core::fmt的write_fmt()函数时,需要提供一个&mut self参数,而我们的调用之所以能成功,是因为每次调用时该函数时,我们都会创建一个新的QEMUOutput实例。
如果我们想保持某些状态——例如,对写入的字符进行统计——就需要创建一个QEMUOutput的全局实例(在Rust中,使用static关键字)。
然而,我们不能使用静态变量QEMU_OUTPUT调用参数为&mut self的函数。此时,我们需要一个static mut变量,但是,在static mut变量上调用函数修改状态并不安全。Rust编译器认为,此时,它无法阻止多个内核/线程同时改变该数据(该变量是全局的,因此任何代码都可能在任何地方引用它。借用检查器此时将无法继续保证借用安全)。
这个问题的解决方案是将全局封装在一个同步原语中(译注:synchronization primitive,实现同步操作的原子化执行)。在我们的例子中,将使用互斥锁(Mutex即MUTual EXclusion)的一个变体。 Mutex在synchronized.rs中作为一个trait引入,并由该文件中的NullLock实现。为了使代码更适合教学目的,现阶段省略了用于保护并发访问的罗辑结构,因为目前的内核仅在单核上执行且禁用中断,所以目前并不需要并发保护。
NullLock专注于展示Rust的一个核心概念——内部可变性,请务必仔细阅读。此外,我还建议阅读这篇关于Rust引用类型的准确认知模型。
如果你想将NullLock与某些实际应用中的互斥锁的实现进行比较,您可以查看spincrate或parking_lotcrate中的实现。
测试运行
$ make qemu[...]
[0] Hello from Rust![1] Chars written: 22[2] Stopping here.与上一章代码的区别
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 = "2018"
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,37 @@ // 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.+ 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)+ } }
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@@ -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 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@@ -107,6 +107,7 @@ #![feature(format_args_nl)] #![feature(global_asm)] #![feature(panic_info_message)]+#![feature(trait_alias)] #![no_main] #![no_std]
@@ -115,6 +116,7 @@ mod cpu; mod panic_wait; mod print;+mod synchronization;
/// Early init code. ///@@ -122,7 +124,15 @@ /// /// - Only a single core must be active and running this function. unsafe fn kernel_init() -> ! {+ use console::interface::Statistics;+ println!("[0] Hello from Rust!");
- panic!("Stopping here.")+ println!(+ "[1] Chars written: {}",+ bsp::console::console().chars_written()+ );++ println!("[2] Stopping here.");+ cpu::wait_forever() }
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-2021 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)+ }+}