第四节 启用页内存分配器

上一节我们实现了一个 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 页分配器已经成功启用!