第六节 内核分层和主干组件
前面我们已经启用了内存分配器组件 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]
输出正常!