使用Rust在树莓派上编写操作系统 - 04 - 安全的全局变量

概述

  • 引入了伪锁。
  • 本章首次展示了操作系统的同步机制,并使全局数据结构可以被安全的访问。

Rust中可变的全局变量

当我们在第三章中引入全局可用的print!宏时,使了个小手段。在调用core::fmtwrite_fmt()函数时,需要提供一个&mut self参数,而我们的调用之所以能成功,是因为每次调用时该函数时,我们都会创建一个新的QEMUOutput实例。

如果我们想保持某些状态——例如,对写入的字符进行统计——就需要创建一个QEMUOutput的全局实例(在Rust中,使用static关键字)。

然而,我们不能使用静态变量QEMU_OUTPUT调用参数为&mut self的函数。此时,我们需要一个static mut变量,但是,在static mut变量上调用函数修改状态并不安全。Rust编译器认为,此时,它无法阻止多个内核/线程同时改变该数据(该变量是全局的,因此任何代码都可能在任何地方引用它。借用检查器此时将无法继续保证借用安全)。

这个问题的解决方案是将全局封装在一个同步原语中(译注:synchronization primitive,实现同步操作的原子化执行)。在我们的例子中,将使用互斥锁(Mutex即MUTual EXclusion)的一个变体。 Mutexsynchronized.rs中作为一个trait引入,并由该文件中的NullLock实现。为了使代码更适合教学目的,现阶段省略了用于保护并发访问的罗辑结构,因为目前的内核仅在单核上执行且禁用中断,所以目前并不需要并发保护。

NullLock专注于展示Rust的一个核心概念——内部可变性,请务必仔细阅读。此外,我还建议阅读这篇关于Rust引用类型的准确认知模型

如果你想将NullLock与某些实际应用中的互斥锁的实现进行比较,您可以查看spincrateparking_lotcrate中的实现。

测试运行

1
2
3
4
5
6
$ make qemu
[...]

[0] Hello from Rust!
[1] Chars written: 22
[2] Stopping here.

与上一章代码的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
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)
+ }
+}

评论

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×