第六节 内核分层和主干组件

前面我们已经启用了内存分配器组件 axalloc,对它的初始化工作临时放在了应用组件 axorigin 中。显然这是不合适的,内存分配器作为内核的关键组成部分应当划分到系统层,并在系统层就被初始化。但在此之前,我们将重新分析和规划整个系统的框架和分层。

回顾第一章的第三节,我们把系统简单分成了两层,应用层包含应用组件 axorigin,系统层仅包含硬件抽象组件 axhal。

实际上对于系统层,又可以进一步划分为硬件体系无关与硬件体系相关的两个层次。硬件体系相关的工作已经由 axhal 承担,现在我们再增加一级硬件体系无关的层次,该层的核心组件 axruntime 专门用来对通用的、硬件体系无关的各类组件进行组织和初始化。

此外,在应用层与系统层之间,常规上还需要一层应用接口库,负责封装屏蔽系统内部复杂性,方便应用的开发。其中关键组件 axstd,顾名思义,功能上相当于 Rust 官方 STD 库的作用。

这样系统就形成了如下的四层结构,每一层都由一个核心组件负责串联:

主干组件层次

自底向上四个层次的核心组件分别是 axhal、axruntime、axstd 和 axorigin,它们构成了框架主干,在系统中是必须存在和不可替代的;除主干之外的其它组件都称为功能组件,往往是可选和可配置的,某些功能可能存在多个候选组件。由主干组件负责对功能组件进行接入、初始化和管理。

另外一个需要注意的问题:启动过程中,各层次主干组件的调用关系是自底向上,基于 extern ABI 的形式;而运行过程则是自顶向下调用,预先通过 Cargo.toml 中的 dependencies 建立依赖链。至于为何采用这样的设计,请回顾第一章第三节关于循环依赖的问题。

下面我们就将引入 axstd 和 axruntime 这两个新组件,并分别针对启动和运行两个过程对系统框架进行相应的调整。

启动过程的调整(自底向上)

先来改造 axhal,它的 rust_entry 中需要以 extern ABI 方式调用 axruntime 的 rust_main 入口:

// axhal/src/riscv64.rs
mod lang_items;
mod boot;
pub mod console;
mod paging;

unsafe extern "C" fn rust_entry(hartid: usize, dtb: usize) {
    extern "C" {
        fn rust_main(hartid: usize, dtb: usize);
    }
    rust_main(hartid, dtb);
}

建立组件 axruntime 并实现它的主入口函数 rust_main,该函数未来将会包含内核启动的各个主要过程:

// axruntime/src/lib.rs
#![no_std]

pub use axhal::ax_println as println;

#[no_mangle]
pub extern "C" fn rust_main(_hartid: usize, _dtb: usize) -> ! {
    extern "C" {
        fn _skernel();
        fn main();
    }

    println!("\nArceOS is starting ...");
    // We reserve 2M memory range [0x80000000, 0x80200000) for SBI,
    // but it only occupies ~194K. Split this range in half,
    // requisition the higher part(1M) for early heap.
    axalloc::early_init(_skernel as usize - 0x100000, 0x100000);
    unsafe { main(); }
    loop {}
}

第 4 行:引入 axhal 定义的标准输出宏 ax_println,并且把它 re-export 出去,后面 axstd 将继续把它暴露给应用。

第 17 行:把对 axalloc 的初始化从应用 axorigin 中转移到 rust_main 中。

第 18 行:调用 axorigin 的入口 main。

然后是对应调整 axorigin 的 main 函数:

// axorigin/src/main.rs
#![no_std]
#![no_main]

use axstd::{String, println};

#[no_mangle]
pub fn main(_hartid: usize, _dtb: usize) {
    let s = String::from("from String");
    println!("\nHello, ArceOS![{}]", s);
}

第 5 行:应用只与接口库 axstd 交互,使用它提供的类型、方法及宏,所以我们将让 axstd 公开这些声明。

运行过程的调整(自顶向下)

建立从 axorigin -> axstd -> axruntime -> axhal 的依赖关系:

# axorigin/Cargo.toml
[dependencies]
axstd = { path = "../axstd" }

# axstd/Cargo.toml
[dependencies]
axruntime = { path = "../axruntime" }
axhal = { path = "../axhal" }

# axruntime/Cargo.toml
[dependencies]
axhal = { path = "../axhal" }
axalloc = { path = "../axalloc" }

第 12 行:axruntime 负责初始化 axalloc,建立对它的依赖。

应用接口库组件 axstd 的实现:

// axstd/src/lib.rs
#![no_std]

extern crate alloc;
pub use alloc::string::String;
pub use axruntime::println;

第 5 行:直接 re-export Rust 的 alloc 库中定义的 String 类型。

第 6 行:把 axruntime 声明的 println 宏公开给应用层调用。

上述 5 和 6 行的目的都是尽可能简化应用开发,让开发者获得类似于在 Linux/Windows 上开发 Rust 应用的体验。

代码调整完毕,但还需要更新一下测试方面的设置。新增 axruntime 和 axstd 组件后,make test 在遇到这两个组件时,会报错。

我们对内核的测试主要针对各个功能组件,所以屏蔽 axruntime 和 axstd 组件,以减少测试过程中不必要的干扰:

# Makefile
test:
	cargo test --workspace --exclude "axorigin" --exclude "axruntime" --exclude "axstd" -- --nocapture

看一下当前根目录下的 Cargo.toml 内容:

[workspace]
resolver = "2"

members = [
    "axorigin", "axhal", "axconfig", "spinlock", "axsync", "page_table", "axalloc",
    "axruntime", "axstd",
]

[profile.release]
lto = true

现在可以执行测试,看我们最近对内核的修改是否影响了之前的功能。

执行 make test:测试全部通过!

最后验证整个内核在调整整体框架后的功能,执行 make run,看结果:

ArceOS is starting ...

Hello, ArceOS![from String]

输出正常!