第四节 启用页内存分配器
上一节我们实现了一个 bitmap 位分配器,其中每个位可以对应管理一个页。本节就对其进行封装,实现一个正式的页内存分配器,用于在内核启动的后期取代早期页分配器的功能,管理内核所有的空闲内存。
首先是封装上节的 bitmap 位分配器,实现可以与 axalloc 组件的框架相适应的页内存分配器 BitmapPageAllocator:
//axalloc/src/bitmap.rs
use core::ptr::NonNull;
use crate::{AllocError, AllocResult};
use axconfig::PAGE_SIZE;
use bitmap_allocator::{BitAlloc1M, BitAlloc};
use alloc::alloc::Layout;
pub struct BitmapPageAllocator {
base: usize,
inner: BitAlloc1M,
}
impl BitmapPageAllocator {
pub const fn new() -> Self {
Self { base: 0, inner: BitAlloc1M::DEFAULT }
}
pub fn init(&mut self, start: usize, size: usize) {
let end = axconfig::align_down(start + size, PAGE_SIZE);
let start = axconfig::align_up(start, PAGE_SIZE);
self.base = start;
let total_pages = (end - start) / PAGE_SIZE;
self.inner.insert(0..total_pages);
}
}
// axalloc/Cargo.toml
[dependencies]
bitmap_allocator = { path = "../bitmap_allocator" }
第 8~11 行:定义 BitmapPageAllocator,其中 inner
就是被封装的 bitmap 位分配器。
第 17~23 行:把内存地址范围换算成以页为单位,每个页对应 1 位,然后就可以利用内嵌的位分配管理器来标记页的分配情况。默认所有位初始都是 0,表示页面不可分配。第 22 行的 insert 设置了一系列连续的 1,标记这些位对应的页面是可以分配的。
然后为 BitmapPageAllocator 实现分配和释放的方法:
impl BitmapPageAllocator {
pub fn alloc_pages(&mut self, layout: Layout) -> AllocResult<NonNull<u8>> {
if layout.align() % PAGE_SIZE != 0 {
return Err(AllocError::InvalidParam);
}
let align_pow2 = layout.align() / PAGE_SIZE;
if !align_pow2.is_power_of_two() {
return Err(AllocError::InvalidParam);
}
let num_pages = layout.size() / PAGE_SIZE;
let align_log2 = align_pow2.trailing_zeros() as usize;
match num_pages.cmp(&1) {
core::cmp::Ordering::Equal => self.inner.alloc().map(|idx| idx * PAGE_SIZE + self.base),
core::cmp::Ordering::Greater => self
.inner
.alloc_contiguous(num_pages, align_log2)
.map(|idx| idx * PAGE_SIZE + self.base),
_ => return Err(AllocError::InvalidParam),
}
.map(|pos| NonNull::new(pos as *mut u8).unwrap())
.ok_or(AllocError::NoMemory)
}
pub fn dealloc_pages(&mut self, pos: usize, num_pages: usize) {
let idx = (pos - self.base) / PAGE_SIZE;
for i in 0..num_pages {
self.inner.dealloc(idx+i)
}
}
}
第 12~19 行:分配的关键,如果申请一页,就直接调用位管理器的 alloc() 把对应位置的位清零,标记已分配;申请连续的多页时,则调用 alloc_contiguous() 把对应的连续位集合清零,标记它们已经分配。
第 23~27 行:释放回收页面时,先算出页面对应的第一个位的索引,从它开始逐个设置 1,标记它们已经被回收,恢复可分配状态。
目前 bitmap 页分配器已经准备好,下面把它集成到全局分配器框架中:
// axalloc/src/lib.rs
use axsync::BootOnceCell;
mod bitmap;
use bitmap::BitmapPageAllocator;
struct GlobalAllocator {
early_alloc: SpinRaw<EarlyAllocator>,
page_alloc: SpinRaw<BitmapPageAllocator>,
finalized: BootOnceCell<bool>,
}
impl GlobalAllocator {
pub const fn new() -> Self {
Self {
early_alloc: SpinRaw::new(EarlyAllocator::uninit_new()),
page_alloc: SpinRaw::new(BitmapPageAllocator::new()),
finalized: BootOnceCell::new(),
}
}
}
第 9~10 行:在 GlobalAllocator 中增加 page_alloc 和 finalized,前者是正式的页内存分配器,后者区分是否处于内核的早期启动阶段,早期使用 early 分配器,后期则替换为正式的内存分配器。
基于 finalized 标记的阶段,扩展 GlobalAllocator 框架分配和释放页面的方法:
// axalloc/src/lib.rs
impl GlobalAllocator {
fn alloc_pages(&self, layout: Layout) -> *mut u8 {
let ret = if self.finalized.is_init() {
self.page_alloc.lock().alloc_pages(layout)
} else {
self.early_alloc.lock().alloc_pages(layout)
};
if let Ok(ptr) = ret {
ptr.as_ptr()
} else {
alloc::alloc::handle_alloc_error(layout)
}
}
fn dealloc_pages(&self, ptr: *mut u8, layout: Layout) {
if self.finalized.is_init() {
self.page_alloc.lock().dealloc_pages(ptr as usize, layout.size()/PAGE_SIZE)
} else {
unimplemented!()
};
}
}
第 4 行:在分配方法中,根据成员 finalized 确定当前是什么阶段,早期使用 early_alloc,后期使用 page_alloc。
第 17 行:类似的,在回收的方法中同样需要检查 finalized,只有后期才需要基于 page_alloc 实现。
然后,实现 final_init() 函数,它负责启用正式的内存分配器:
// axalloc/src/lib.rs
pub fn final_init(start: usize, len: usize) {
GLOBAL_ALLOCATOR.final_init(start, len)
}
impl GlobalAllocator {
pub fn final_init(&self, start: usize, size: usize) {
self.page_alloc.lock().init(start, size);
self.finalized.init(true);
}
}
第 8~9 行:final_init() 需要完成两项具体工作,初始化 page_alloc 页内存分配器,再标识 finalized 为 true,说明进入正式的内存分配器的使用阶段。从上面的 alloc_pages 和 dealloc_pages 方法可以看到,设置该标识之后,早期内存分配器被禁用,切换到正式内存分配器。
实际上本节只是处理了页内存分配器,下节会扩展 final_init() 等方法,继续处理字节内存分配器。
万事俱备,现在就来调用 final_init(),实施内存分配器的切换:
// axruntime/src/lib.rs
pub extern "C" fn rust_main(hartid: usize, dtb: usize) -> ! {
... ...
let phys_memory_size = dtb_info.memory_size;
info!("Initialize kernel page table...");
remap_kernel_memory(dtb_info);
info!("Initialize formal allocators ...");
for r in free_regions(phys_memory_size) {
axalloc::final_init(phys_to_virt(r.paddr), r.size);
}
unsafe { main(); }
axhal::terminate();
}
第 4 行:预先保存物理内存的大小,因为按照 Rust 语法,remap_kernel_memory() 会 move 那个 dtb_info 变量。
第 10~12 行:遍历所有的空闲内存区域,对每一个调用 final_init,由此启用正式的内存分配器。
最后,在 axorigin 应用组件中申请页面,验证正式的内存分配器的功能:
// axorigin/src/main.rs
#![no_std]
#![no_main]
use axstd::{String, println, time, PAGE_SIZE};
#[no_mangle]
pub fn main(_hartid: usize, _dtb: usize) {
let now = time::Instant::now();
println!("\nNow: {}", now);
let s = String::from("from String");
println!("Hello, ArceOS![{}]", s);
try_alloc_pages();
let d = now.elapsed();
println!("Elapsed: {}.{:06}", d.as_secs(), d.subsec_micros());
}
fn try_alloc_pages() {
use core::alloc::Layout;
extern crate alloc;
const NUM_PAGES: usize = 300;
let layout = Layout::from_size_align(NUM_PAGES*PAGE_SIZE, PAGE_SIZE).unwrap();
let p = unsafe { alloc::alloc::alloc(layout) };
println!("Allocate pages: [{:?}].", p);
unsafe { alloc::alloc::dealloc(p, layout) };
println!("Release pages ok!");
}
第 21~31 行:申请 300 个页面,然后释放。早期内存分配器的最大分配能力只有 1M,承担不了申请 300 个页面的任务(300*4K),如果分配成功,可以从侧面证明,内核已经切换到分配能力更强的正式内存分配器。
另外,应用 axorigin 调用了 PAGE_SIZE 这个常量,我们需要扩展一下 axstd,引出该常量。
// axstd/Cargo.toml
[dependencies]
axconfig = { path = "../axconfig" }
// axstd/src/lib.rs
pub use axconfig::PAGE_SIZE;
现在来测试一下我们的最新成果,make run
,看到 axorigin 正常申请了 300 个页面然后释放:
ArceOS is starting ...
Now: 0.114120 Hello, ArceOS![from String] Allocate pages: [0xffffffc08026c000]. Release pages ok! Elapsed: 0.006793
正式的 bitmap 页分配器已经成功启用!