第五节 启用动态内存分配

本节为内核启用动态内存分配功能,主要分两个步骤:

  1. 向 Rust 声明一个支持 GlobalAlloc Trait 的内存分配器 GlobalAllocator,这个 Trait 是向 Rust 提供动态内存分配服务的标准接口。
  2. 初始化内存分配器,为它指定可以使用的内存地址范围。
global_allocator

如上图,全局内存分配器 GlobalAllocator 实现 GlobalAlloc Trait,它包含两个功能:字节分配和页分配,分别用于响应对应请求。区分两种请求的策略是,请求分配的大小是页大小的倍数且按页对齐,就视作申请页;否则就是按字节申请分配。这两个内部功能可以由各种内存分配算法支持实现,当前是内核启动早期,我们基于上节提供的 early allocator支持两种功能。

全局内存分配器启用时必须指定一组可用的内存地址范围(至少一个)。在内核启动早期,通过 early_init 方法初始化并启用,这就是本节要实现的主要内容;然后在适当时刻,调用 final_init 方法切换到正式的内存分配器,这是第四章将要介绍的内容。

下面来实现全局内存分配器 GlobalAllocator,首先引入一些必须依赖的外部符号:

// axalloc/src/lib.rs
use core::alloc::Layout;
use core::ptr::NonNull;
use spinlock::SpinRaw;
use axconfig::PAGE_SIZE;

extern crate alloc;
use alloc::alloc::GlobalAlloc;

mod early;
use early::EarlyAllocator;

实现全局的内存分配器:

// axalloc/src/lib.rs
#[cfg_attr(not(test), global_allocator)]
static GLOBAL_ALLOCATOR: GlobalAllocator = GlobalAllocator::new();

struct GlobalAllocator {
    early_alloc: SpinRaw<EarlyAllocator>,
}

impl GlobalAllocator {
    pub const fn new() -> Self {
        Self {
            early_alloc: SpinRaw::new(EarlyAllocator::uninit_new()),
        }
    }

    pub fn early_init(&self, start: usize, size: usize) {
        self.early_alloc.lock().init(start, size)
    }
}

分别为字节分配和页分配准备方法:

// axalloc/src/lib.rs
impl GlobalAllocator {
    fn alloc_bytes(&self, layout: Layout) -> *mut u8 {
        if let Ok(ptr) = self.early_alloc.lock().alloc_bytes(layout) {
            ptr.as_ptr()
        } else {
            alloc::alloc::handle_alloc_error(layout)
        }
    }
    fn dealloc_bytes(&self, ptr: *mut u8, layout: Layout) {
        self.early_alloc.lock().dealloc_bytes(
            NonNull::new(ptr).expect("dealloc null ptr"),
            layout
        )
    }
    fn alloc_pages(&self, layout: Layout) -> *mut u8 {
        if let Ok(ptr) = self.early_alloc.lock().alloc_pages(layout) {
            ptr.as_ptr()
        } else {
            alloc::alloc::handle_alloc_error(layout)
        }
    }
    fn dealloc_pages(&self, _ptr: *mut u8, _layout: Layout) {
        unimplemented!();
    }
}

现在如果进行编译,Rust 会提示需要 GlobalAlloc Trait,这个 Trait 是 GlobalAllocator 必须实现的标准接口。

实现 GlobalAlloc Trait 的两个方法 - 分配 alloc 和释放 dealloc,如下:

// axalloc/src/lib.rs
unsafe impl GlobalAlloc for GlobalAllocator {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        if layout.size() % PAGE_SIZE == 0 && layout.align() == PAGE_SIZE {
            self.alloc_pages(layout)
        } else {
            self.alloc_bytes(layout)
        }
    }

    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
        if layout.size() % PAGE_SIZE == 0 && layout.align() == PAGE_SIZE {
            self.dealloc_pages(ptr, layout)
        } else {
            self.dealloc_bytes(ptr, layout)
        }
    }
}

pub fn early_init(start: usize, len: usize) {
    GLOBAL_ALLOCATOR.early_init(start, len)
}

现在准备启用:

上一节开头,我们让 axorigin 向屏幕输出 String 类型,结果报错 - “没有全局内存分配器”。如下在使用 String 类型变量之前,首先对 axalloc 组件进行初始化,启用内存分配功能。

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

extern crate alloc;
use alloc::string::String;
use axhal::ax_println;

#[no_mangle]
pub fn main(_hartid: usize, _dtb: usize) {
    extern "C" {
        fn _skernel();
    }
    // 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);

    let s = String::from("from String");
    ax_println!("\nHello, ArceOS![{}]", s);
}

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

第14~17行:如注释所述,我们使用内核前面 1M 的地址范围来初始化内存分配器,在 RiscV64 架构下这部分内存是确定空闲的。虽然只有 1M,但对于早期启动阶段来说已经足够了,第四章我们将切换到正式的内存分期器,管理所有的 Heap 区域。

在内核启动早期,我们不想直接使用内核后面的内存区域来初始化内存分配器。此时,内核尚未对系统的整体内存分布状况进行调查,还不知道具体有几块内存区域以及它们的大小,如直接使用可能会引入一些不确定因素。所以目前先使用确定的空闲区域初始化一个临时的小型的早期内存分配器,以方便后续引导过程的实现。

现在可以测试了,make run 看看输出结果:

Hello, ArceOS![from String]

验证成功!