第五节 全局配置与同步支持

在本章的最后,增加几个常用工具组件,为后续的实验工作做一个准备。

  1. 全局配置组件 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

  2. 自旋锁 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

  3. 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