# 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 创建页表。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://xy-plus.gitbook.io/rcore-step-by-step/nei-cun-fen-pei.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
