第五节 全局配置与同步支持
在本章的最后,增加几个常用工具组件,为后续的实验工作做一个准备。
-
全局配置组件 axconfig
创建组件 axconfig,主要用来提供全局的配置参数和一些常用的工具函数,它的 lib.rs 就如这样:
// axconfig/src/lib.rs #![no_std] pub const PAGE_SHIFT: usize = 12; pub const PAGE_SIZE: usize = 1 << PAGE_SHIFT; pub const PHYS_VIRT_OFFSET: usize = 0xffff_ffc0_0000_0000; pub const ASPACE_BITS: usize = 39; pub const SIZE_1G: usize = 0x4000_0000; pub const SIZE_2M: usize = 0x20_0000; #[inline] pub const fn align_up(val: usize, align: usize) -> usize { (val + align - 1) & !(align - 1) } #[inline] pub const fn align_down(val: usize, align: usize) -> usize { (val) & !(align - 1) } #[inline] pub const fn align_offset(addr: usize, align: usize) -> usize { addr & (align - 1) } #[inline] pub const fn is_aligned(addr: usize, align: usize) -> bool { align_offset(addr, align) == 0 } #[inline] pub const fn phys_pfn(pa: usize) -> usize { pa >> PAGE_SHIFT }
在我们的实验中,页面 Page 采用最常见的 4096 字节。同时提供一组与对齐相关的工具函数。
后面我们会陆续加入更多的配置参数和工具函数。
可以在真正实现 align 相关函数之前,先写好测试用例,以测试驱动的方式开发该组件功能:
// axconfig/tests/test_align.rs use axconfig::{align_up, align_down, PAGE_SIZE}; #[test] fn test_align_up() { assert_eq!(align_up(23, 16), 32); assert_eq!(align_up(4095, PAGE_SIZE), PAGE_SIZE); assert_eq!(align_up(4096, PAGE_SIZE), PAGE_SIZE); assert_eq!(align_up(4097, PAGE_SIZE), 2*PAGE_SIZE); } #[test] fn test_align_down() { assert_eq!(align_down(23, 16), 16); assert_eq!(align_down(4095, PAGE_SIZE), 0); assert_eq!(align_down(4096, PAGE_SIZE), PAGE_SIZE); assert_eq!(align_down(4097, PAGE_SIZE), PAGE_SIZE); }
上面是组件级的测试用例,直接放在 axconfig/tests 目录下,用于对组件公开接口的测试。
执行
make test
,显示测试成功:Running tests/test_align.rs (target/debug/deps/test_align-087b60d36f414a97)
running 2 tests test test_align_down ... ok test test_align_up ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
自旋锁 SpinRaw
在真正启动多任务和启用中断之前,内核一直处于纯粹的单线程执行状态,没有任何并发,自然也没有同步的问题,当前对于全局变量的修改是安全的。但是 Rust 不清楚这一点,所以我们需要先实现一个最初版本的自旋锁 SpinRaw,用它包装 mutable 全局变量,假装已经有了同步保护,但实际上它目前只是个空壳。
创建组件 spinlock,在模块 raw.rs 中实现 SpinRaw 类型。
// spinlock/src/lib.rs #![no_std] mod raw; pub use raw::{SpinRaw, SpinRawGuard}; // spinlock/src/raw.rs use core::cell::UnsafeCell; use core::ops::{Deref, DerefMut}; pub struct SpinRaw<T> { data: UnsafeCell<T>, } pub struct SpinRawGuard<T> { data: *mut T, } unsafe impl<T> Sync for SpinRaw<T> {} unsafe impl<T> Send for SpinRaw<T> {} impl<T> SpinRaw<T> { #[inline(always)] pub const fn new(data: T) -> Self { Self { data: UnsafeCell::new(data), } } #[inline(always)] pub fn lock(&self) -> SpinRawGuard<T> { SpinRawGuard { data: unsafe { &mut *self.data.get() }, } } }
按照 Rust 要求,标记 SpinRaw 具有 Send 和 Sync 的标记 trait。
实现 lock 方法返回 SpinRawGuard,假装这个 guard 持有了锁,guard 是 RAII 的模式,当它释放即执行 drop 方法时自动解锁。目前没有实际解锁动作,直接忽略对 Drop trait 的实现。
然后我们为 guard 实现 Deref 和 DerefMut 两个 Trait,把它作为智能指针以方便直接访问 SpinRaw 包装变量的方法。
// spinlock/src/raw.rs impl<T> Deref for SpinRawGuard<T> { type Target = T; #[inline(always)] fn deref(&self) -> &T { unsafe { &*self.data } } } impl<T> DerefMut for SpinRawGuard<T> { #[inline(always)] fn deref_mut(&mut self) -> &mut T { unsafe { &mut *self.data } } }
编写测试用例,验证对外公开的组件接口功能:
// spinlock/tests/test_raw.rs use spinlock::SpinRaw; struct Inner { val: usize, } impl Inner { const fn new() -> Self { Self { val: 0 } } fn set(&mut self, v: usize) { self.val = v; } fn get(&self) -> usize { self.val } } static SPIN: SpinRaw<Inner> = SpinRaw::new(Inner::new()); #[test] fn test_lock() { SPIN.lock().set(1); assert_eq!(SPIN.lock().get(), 1); }
执行
make test
测试,显示:Running tests/test_raw.rs (target/debug/deps/test_raw-70e610058ffa9914)
running 1 test test test_lock ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
-
BootOnceCell
类似于上面情况,有些全局变量虽然需要设置,但是仅需要一次,之后就一直是只读状态,这属于延迟初始化的一种。
例如下一章马上要涉及的早期页表根目录 KERNEL_PAGE_TABLE,这个全局变量只需要初始化一次,但是负责初始化的是一个函数,没法在定义时直接调用,只能延迟初始化。那么就可以借助这个 BootOnceCell 来封装 KERNEL_PAGE_TABLE。
创建组件 axsync,在模块 bootcell.rs 中实现类型 BootOnceCell。
// axsync/src/lib.rs #![no_std] mod bootcell; pub use bootcell::BootOnceCell; // axsync/src/bootcell.rs use core::cell::OnceCell; pub struct BootOnceCell<T> { inner: OnceCell<T>, } impl<T> BootOnceCell<T> { pub const fn new() -> Self { Self { inner: OnceCell::new() } } pub fn init(&self, val: T) { let _ = self.inner.set(val); } pub fn get(&self) -> &T { self.inner.get().unwrap() } pub fn is_init(&self) -> bool { self.inner.get().is_some() } } unsafe impl<T> Sync for BootOnceCell<T> {}
注意两点:
- 我们以 Rust 库提供的 OnceCell 为基础进行实现,比较简便。
- 顾名思义,BootOnceCell 的那一次初始化调用必须在 Boot 阶段,即单线程环境下完成,之后就变成了只读变量,这样再启用多线程也没有问题。但如果初始化操作是在启用多线程或中断之后再进行,就是不安全的。一定注意这个调用时机。
BootOnceCell 是对 lazy_static 的替代实现。
对应的测试用例如下:
// axsync/tests/test_bootcell.rs use axsync::BootOnceCell; static TEST: BootOnceCell<usize> = BootOnceCell::new(); #[test] fn test_bootcell() { assert!(!TEST.is_init()); TEST.init(101); assert_eq!(TEST.get(), &101); assert!(TEST.is_init()); }
执行
make test
测试,显示:Running tests/test_bootcell.rs (target/debug/deps/test_bootcell-3224367d8bbd079d)
running 1 test test test_bootcell ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s