> For the complete documentation index, see [llms.txt](https://xy-plus.gitbook.io/rcore-step-by-step/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://xy-plus.gitbook.io/rcore-step-by-step/nei-cun-fen-pei.md).

# 6. 内存分配

> 本章代码对应 commit ：ce95a018b972c80c59d83e8f7eb5d0d2a7f9b6b5

## 概要

上一章我们通过 bbl 启用了分页机制。但是这个由于这个页表过于简陋，所以我们希望能够使用自己写的页表。显然，系统需要一片内存来存放页表。本章我们的目标是将物理内存分配给页表。但是在此之前我们需要先能够管理物理内存，为此我们需要：

1. 判断内存的那些部分是可以供我们分配的。
2. 引入 buddy allocator 辅助管理物理内存。
3. 实现物理内存的分配与释放。

## 设置可分配内存范围

首先，新建 **consts.rs** 保存一些常量：

```rust
pub const KERNEL_OFFSET: usize = 0xC000_0000;

pub const MEMORY_OFFSET: usize = 0x8000_0000;

pub const PAGE_SIZE: usize = 4096;

pub const MAX_DTB_SIZE: usize = 0x2000;

pub const KERNEL_HEAP_SIZE: usize = 0x00a0_0000;
```

在实现内存分配之前，堆的初始化是必须的：

**Cargo.toml**

```rust
[dependencies]
buddy_system_allocator = "0.1"
```

**lib.rs**

```rust
use buddy_system_allocator::LockedHeap;
#[global_allocator]
static HEAP_ALLOCATOR: LockedHeap = LockedHeap::empty();
```

**memory/mod.rs**

```rust
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);
    }
    println!("heap init end");
}
```

现在我们便可以开始计算哪些部分的内存是能够被分配的。

在 **Cargo.toml** 中加入：

```rust
[dependencies]
device_tree = { path = "crate/device_tree-rs" }
```

这时编译出现如下错误：

```
error: `#[alloc_error_handler]` function required, but not found

error: aborting due to previous error

error: Could not compile `os`.

To learn more, run the command again with --verbose.
Makefile:30: recipe for target 'kernel' failed
make: *** [kernel] Error 101
```

### FIX error： `#[alloc_error_handler]` function required, but not found

这需要在`os/lib.rs`中添加一行：

```
#![feature(alloc_error_handler)]
```

表示要定义`alloc_error_handler`这个函数。然后在os/memory/mod.rs中实现这个函数的定义：

```
#[alloc_error_handler]
fn foo(_: core::alloc::Layout) -> ! {
    panic!("DO NOTHING alloc_error_handler set by kernel");
}
```

其实这个函数只是简单地panic整个OS。

### 显示机器和内核的内存信息

通过 `let Some((addr, mem_size)) = device_tree::DeviceTree::dtb_query_memory(dtb)` 即可从 dtb 中读取内存的起始地址和大小。？？？

在内核结束的位置，紧接着就会存放 dtb ：

```rust
let kernel_end = dtb - KERNEL_OFFSET + MEMORY_OFFSET + PAGE_SIZE; // 内核的起始物理地址
let kernel_size = kernel_end - addr; // 内核的终止物理地址
```

> 这里给 dtb 留出了一页的大小存放

## 引入 buddy allocator

创建文件： **memory/frame\_allocator/mod.rs** 。

在 **Cargo.toml** 中添加：

```rust
[dependencies]
bit_field = "0.9"
buddy-allocator = { path = "crate/buddy-allocator" }
lazy_static = { version = "1.3", features = ["spin_no_std"] }
spin = "0.3"
```

**buddy\_allocator** 通过 [buddy system](https://coolshell.cn/articles/10427.html) 算法分配内存，算法细节可自行上网查询。

在 **memory/frame\_allocator/mod.rs** 中添加：

```rust
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());
}

static mut KERNEL_END: usize = 0;

pub fn init(start: usize, lenth: usize) {
    unsafe {
        KERNEL_END = start;
    }
    let mut bu = BUDDY_ALLOCATOR.lock()
        .init(log2_down(lenth / PAGE_SIZE) as u8);
    println!("++++init frame allocator succeed!++++");
}
```

> 对详细实现过程感兴趣的读者请自行上 [GitHub](https://github.com/xy-plus/buddy-allocator) 阅读代码，这里不做过多介绍

这里我们需要创建一个用于分配内存的 **BUDDY\_ALLOCATOR** 全局静态变量。但是他的值需要在运行时才能被确定。这里 [lazy\_static](http://autumnai.github.io/collenchyma/lazy_static/) 便帮我们解决了这个问题。

什么是 **Mutex** ？我们用一个现实生活中的例子来理解：假设你去超市买了一个笔记本，付款之后你还没来得及把他拿走，这时来了另一个人，也付了钱，买了这个笔记本。那么这个笔记本属于谁呢？这不是我们乐意见到的。为了防止这种情况，在超市买东西的时候，前一个人的结账尚未完成的时候，下一个人是不能够开始结账的。同样的道理适用于内存块的分配，这里的内存块就可以类比于超市的笔记本，互斥锁（Mutex）就使得内存在分配时不会收到外界干扰。

> 更详细的互斥锁内容将在以后的章节介绍

`init` 根据内存大小初始化了 **buddy\_allocator** ，同时修改了内核结束地址，以便后续物理内存的分配和释放时使用。

## 物理内存块的分配与释放

有了上面的这些工作，内存分配和释放的实现变得十分简洁：

```rust
// in memory/frame_allocator/mod.rs

use riscv::addr::*;

pub fn alloc_frame() -> Option<Frame> {
        alloc_frames(1)
}

pub fn alloc_frames(size: usize) -> Option<Frame> {
    unsafe {
        let ret = BUDDY_ALLOCATOR
            .lock()
            .alloc(size)
            .map(|id| id * PAGE_SIZE + KERNEL_END);
        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) {
    unsafe {
        BUDDY_ALLOCATOR
            .lock()
            .dealloc(target.number() - KERNEL_END / 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** 中加入：

```rust
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_frame().expect("failed to alloc frame");
    println!("test frame_allocator: {:#x}", frame2.start_address().as_usize());
    dealloc_frame(frame1);
    let frame3: Frame = alloc_frame().expect("failed to alloc frame");
    println!("test frame_allocator: {:#x}", frame3.start_address().as_usize());
    dealloc_frame(frame2);
    dealloc_frame(frame3);
}
```

然后修改 **memory/mod.rs** 的 `init()` ：

```rust
pub mod frame_allocator;

use frame_allocator::{ init as init_frame_allocator, test as test_frame_allocator };
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();
    if let Some((addr, mem_size)) = device_tree::DeviceTree::dtb_query_memory(dtb) {
        assert_eq!(addr, MEMORY_OFFSET);
        let KERNEL_END = dtb - KERNEL_OFFSET + MEMORY_OFFSET + PAGE_SIZE;
        let KERNEL_SIZE = KERNEL_END - addr;
        init_frame_allocator(KERNEL_END, KERNEL_SIZE);
    } else {
        panic!("failed to query memory");
    }
    test_frame_allocator();
}
```

执行 **make run** ，出现了如下错误：

```
error[E0152]: duplicate lang item found: `oom`.
  --> src/memory/mod.rs:49:1
   |
49 | / fn foo(_: core::alloc::Layout) -> ! {
50 | |     panic!("DO NOTHING alloc_error_handler set by kernel");
51 | | }
   | |_^
   |
   = 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冲突了。把我们的老代码`src/memory/mod.rs：47-51 lines`删除掉即可。再次执行`make run`，编译正确，且运行的屏幕部分显示内容为：

```rust
++++init frame allocator succeed!++++
test frame_allocator: 0x80c01000
test frame_allocator: 0x80c02000
test frame_allocator: 0x80c01000
```

注意到 `0x80c02000 - 0x80c01000 = 0x1000 = 4096` ，每次分配的内存恰好就为一个页面的大小（PAGE\_SIZE）。

> 这里输出的均为物理地址

## 预告

本章我们实现了内存分配，下一章我们将利用 frame\_allocator 创建页表。
