内存分配
本章代码对应分支:alloc-memory
概要
- 初始化内核堆。
- 判断内存的那些部分是可以供我们分配的。
- 引入 buddy allocator 辅助管理物理内存。
- 实现物理内存的分配与释放。
初始化内核堆
内存可以在 stack 上分配,在我们的 os 中,stack 的大小在 boot/entry.asm
被定为 4 * 4k
。但是如 Vec#[global_allocator]
标签允许我们实现自己的 全局分配器 。这里如果不指定 全局分配器 会导致后续无法使用 Vec
这里我们使用一个现成的适用于本 os 的 全局分配器 :
Cargo.toml
[dependencies]
buddy_system_allocator = "0.1"
lib.rs
#![feature(alloc_error_handler)]
mod memory;
mod consts;
use buddy_system_allocator::LockedHeap;
#[global_allocator]
static HEAP_ALLOCATOR: LockedHeap = LockedHeap::empty();
#[alloc_error_handler]
fn foo(_: core::alloc::Layout) -> ! {
panic!("DO NOTHING alloc_error_handler set by kernel");
}
接下来创建目录和文件,对堆进行初始化:
consts.rs
pub const KERNEL_HEAP_SIZE: usize = 0x00a0_0000;
大小可以自行规定,合适就行,后续(下一章)将根据实际情况修改它
memory/mod.rs
use crate::consts::*;
use crate::HEAP_ALLOCATOR;
pub fn init(dtb: usize) {
use riscv::register::sstatus;
unsafe {
// Allow user memory access
sstatus::set_sum();
}
init_heap();
}
fn init_heap() {
static mut HEAP: [u8; KERNEL_HEAP_SIZE] = [0; KERNEL_HEAP_SIZE];
unsafe {
HEAP_ALLOCATOR
.lock()
.init(HEAP.as_ptr() as usize, KERNEL_HEAP_SIZE);
}
}
这里我们将 sum 位设为 1 。sum(permit supervisor user memory access)位修改 S 模式读、写和指令获取访问虚拟内存的权限。仅当 sum = 1 时则允许在 S 模式下访问属于 U 模式(U = 1)的内存,否则会产生异常 这一步现在还不是必须的,要到用户程序时才能体现出作用
计算可用内存
硬件信息需要通过 device tree 获得,但是略繁琐,所以这里我们暂时直接钦定 MEMORY_END
:
consts.rs
pub const MEMORY_OFFSET: usize = 0x8000_0000;
pub const MEMORY_END: usize = 0x8800_0000;
pub const PAGE_SIZE: usize = 4096;
memory/mod.rs
// Symbols provided by linker script
extern "C" {
fn end();
}
use frame_allocator::{ init as init_frame_allocator, test as test_frame_allocator }; // TODO
pub fn init() {
unsafe {
sstatus::set_sum(); // Allow user memory access
}
init_heap();
let memory_start = (end as usize) + PAGE_SIZE;
let memory_size = MEMORY_END - memory_start;
init_frame_allocator(memory_start, memory_size);
test_frame_allocator();
}
boot/linker.ld
为end
赋值,这个是 kernel 的结束虚拟地址,此时由于尚未启用页表,虚实地址相等。在此之后是 device tree base ,我们为其留出PAGE_SIZE
大小的空间存放
物理内存块的分配与释放
我们将内存整体视为许多连续的“块”,这里我们将每个块的大小定为 4k ,也即 PAGE_SIZE
。那么我们需要一个数据结构来管理这些块:哪些块被分配了、哪些块是空闲的、如何进行分配和释放等。buddy system 就是通过二叉树对内存块进行管理的一种算法。
Cargo.toml
这里提供已经写好的 buddy system 算法 ,请自行下载并放于 crate
目录下:
[dependencies]
buddy-allocator = { path = "crate/buddy-allocator" }
lazy_static = { version = "1.3", features = ["spin_no_std"] }
spin = "0.3"
算法细节可阅读 伙伴分配器的一个极简实现 也可以直接看提供的代码
memory/frame_allocator/mod.rs
这里我们需要创建一个用于分配内存的 BUDDY_ALLOCATOR
全局静态变量。但是他的值需要在运行时才能被确定。这里 lazy_static 便帮我们解决了这个问题。
同时,为了防止多线程时内存被重复分配,需要使用互斥锁(Mutex)管理 BUDDY_ALLOCATOR
。
什么是 Mutex ?我们用一个现实生活中的例子来理解:假设你去超市买了一个笔记本,付款之后你还没来得及把他拿走,这时来了另一个人,也付了钱,买了这个笔记本。那么这个笔记本属于谁呢?这不是我们乐意见到的。为了防止这种情况,在超市买东西的时候,前一个人的结账尚未完成的时候,下一个人是不能够开始结账的。同样的道理适用于内存块的分配。
use buddy_allocator::{ BuddyAllocator, log2_down };
use lazy_static::*;
use spin::Mutex;
use riscv::addr::*;
use crate::consts::*;
// 物理页帧分配器
lazy_static! {
pub static ref BUDDY_ALLOCATOR: Mutex<BuddyAllocator>
= Mutex::new(BuddyAllocator::new());
}
pub fn init(start: usize, lenth: usize) {
BUDDY_ALLOCATOR.lock()
.init(log2_down((start + lenth - MEMORY_OFFSET) / PAGE_SIZE) as u8);
alloc_frames((start - MEMORY_OFFSET - 1) / PAGE_SIZE + 1);
println!("++++init frame allocator succeed!++++");
}
pub fn alloc_frame() -> Option<Frame> {
alloc_frames(1)
}
pub fn alloc_frames(size: usize) -> Option<Frame> {
let ret = BUDDY_ALLOCATOR
.lock()
.alloc(size)
.map(|id| id * PAGE_SIZE + MEMORY_OFFSET);
ret.map(|addr| Frame::of_addr(PhysAddr::new(addr)))
}
pub fn dealloc_frame(target: Frame) {
dealloc_frames(target, 1);
}
pub fn dealloc_frames(target: Frame, size: usize) {
BUDDY_ALLOCATOR
.lock()
.dealloc(target.start_address().as_usize() / PAGE_SIZE - MEMORY_OFFSET / PAGE_SIZE, size);
}
riscv::addr::*
引入了 struct Frame
以及一些相关函数。由于 buddy_allocator::alloc
返回的是内存块编号,类型为 Option<usize>
,所以需要将其转换为物理地址,然后通过 Frame::of_addr
转换为物理帧。同理,在释放内存时需要进行类似的操作。
这里涉及到一个 rust 的语法:闭包。我们举一个例子便能理解他:
- Some(233).map(|x| x + 666) = Some(899)
完成了分配和释放内存的函数,让我们来简单的测试一下他的正确性:
memory/frame_allocator/mod.rs
pub fn test() {
let frame1: Frame = alloc_frame().expect("failed to alloc frame");
println!("test frame_allocator: {:#x}", frame1.start_address().as_usize());
let frame2: Frame = alloc_frames(2).expect("failed to alloc frame");
println!("test frame_allocator: {:#x}", frame2.start_address().as_usize());
let frame3: Frame = alloc_frame().expect("failed to alloc frame");
println!("test frame_allocator: {:#x}", frame3.start_address().as_usize());
dealloc_frame(frame1);
dealloc_frames(frame2, 2);
dealloc_frame(frame3);
}
init.rs
#[no_mangle]
pub fn rust_main() -> ! {
crate::interrupt::init();
crate::clock::init();
crate::memory::init();
loop {}
}
执行 make run
,出现了如下错误:
error[E0152]: duplicate lang item found: `oom`.
--> src/lib.rs:23:1
|
23 | / fn foo(_: core::alloc::Layout) -> ! {
24 | | panic!("DO NOTHING alloc_error_handler set by kernel");
25 | | }
| |_^
|
= note: first defined in crate `buddy_allocator`.
error: aborting due to previous error
For more information about this error, try `rustc --explain E0152`.
error: Could not compile `os`.
这是由于 buddy_allocator 也定义了对内存分配错误的处理函数,这样就和我们之前定义的 alloc_error_handler
冲突了。把我们实现的 alloc_error_handler
删掉即可。再次执行 make run
,编译正确,屏幕输出:
...
++++init frame allocator succeed!++++
test frame_allocator: 0x81000000
test frame_allocator: 0x81002000
test frame_allocator: 0x81001000
100 ticks!
...
仔细观察输出的地址,再思考一下 buddy system 分配内存的过程 这里输出的均为物理地址
预告
本章我们实现了内存分配,下一章我们学习管理已分配的内存的工具:页表。