# 内存管理模块

【部分内容已经过时】

## 概述

内存管理模块的职责是完成内存资源的管理。它主要建立在硬件提供的页机制之上，为使用者提供共享内存、页交换、写时复制等高级功能，并方便不同进程之间内存资源的管理。此模块功能相当于ucore lab3。

具体而言，使用者需要实现以下接口：

* 页表（项）
* 页交换算法

即可使用模块提供的以下功能：

* 页交换
* 写时复制
* 共享内存
* 内存管理器

## 实现

此模块作为独立的crate存在，代码位于：`crate/memory`

src目录结构及各文件功能如下：

```
.
├── lib.rs           根模块
├── addr.rs          定义基本数据类型
├── cow.rs           写时复制和共享内存
├── memory_set.rs    内存管理器
├── paging           页表
│   ├── mock_page_table.rs    页表mock实现
│   └── mod.rs                页表接口
└── swap             交换机制
    ├── mod.rs                接口定义和交换框架的实现
    ├── mock_swapper.rs       交换到内存的实现
    ├── fifo.rs               FIFO算法
    └── enhanced_clock.rs     改进时钟算法
```

模块内为页表接口提供了模拟实现，因此就可以为建立在其上的各种算法编写单元测试，使用以下命令即可运行：

```bash
cd crate/memory
rustup override set nightly-2018-09-13
cargo test
```

### 页表接口

位于`src/paging/mod.rs`，定义了两个trait：`PageTable`,`Entry`

```rust
/// （可修改的）页表接口
pub trait PageTable {
    /// 关联类型，指向一个Entry接口的实现
    type Entry: Entry;
    /// 映射一个虚拟页到一个物理帧，返回该页对应的页表项，可做进一步的修改
    fn map(&mut self, addr: VirtAddr, target: PhysAddr) -> &mut Self::Entry;
    /// 取消一个虚拟页的映射
    fn unmap(&mut self, addr: VirtAddr);
    /// 获取一个虚地址在页表中对应的页表项
    /// TODO：目前没考虑非法输入，会导致page fault
    fn get_entry(&mut self, addr: VirtAddr) -> &mut Self::Entry;
    
    /// 以下仅用于mock页表的测试
    fn get_page_slice_mut<'a,'b>(&'a mut self, addr: VirtAddr) -> &'b mut [u8];
    fn read(&mut self, addr: VirtAddr) -> u8;
    fn write(&mut self, addr: VirtAddr, data: u8);
}

/// 页表项接口
pub trait Entry {
    /// 【重要】执行TLB flush使之前的修改生效
    fn update(&mut self);
    
    // 获取状态位信息
    fn accessed(&self) -> bool;
    fn dirty(&self) -> bool;
    fn writable(&self) -> bool;
    fn present(&self) -> bool;
    fn user(&self) -> bool;
    fn execute(&self) -> bool;
    
    // 修改状态位信息
    fn clear_accessed(&mut self);
    fn clear_dirty(&mut self);
    fn set_writable(&mut self, value: bool);
    fn set_present(&mut self, value: bool);
    fn set_user(&mut self, value: bool);
    fn set_execute(&mut self, value: bool);

    // 获取和设置映射的物理帧
    fn target(&self) -> PhysAddr;
    fn set_target(&mut self, target: PhysAddr);

    // 用于写时复制的状态位
    fn writable_shared(&self) -> bool;
    fn readonly_shared(&self) -> bool;
    fn set_shared(&mut self, writable: bool);
    fn clear_shared(&mut self);

    // 用于交换机制的状态位
    fn swapped(&self) -> bool;
    fn set_swapped(&mut self, value: bool);
}

```

读者可查看`MockPageTable`的实现，了解接口的具体使用方法。

### 页表的具体实现

x86\_64/RV32下的实现位于：`kernel/src/arch/../paging.rs`，其实只是套了层壳，诸如map等复杂操作的实现位于x86\_64/riscv库中。

64位下的页表由于层数较多，难以像ucore for x86\_32的实现一样，预先将每个页表节点分配到固定的物理帧上，并建立描述结构。为了方便页表本身的编辑，我们使用**自映射机制**，这样页表中的每个节点都映射到唯一的虚地址。并使用**物理帧分配器**`FrameAllocator`动态分配页表本身占用的物理内存。

关于自映射机制的实现细节，推荐阅读 [Page Tables | Writing an OS in Rust](https://os.phil-opp.com/first-edition) ，并配合[x86\_64库中的实现](https://docs.rs/x86_64/0.2.11/x86_64/structures/paging/index.html)，加深理解。

RISCV32中基本继承了x86\_64的实现方式。不过值得注意的是：**RISCV下的页表规范阻碍了自映射的实现。**&#x539F;因是RISCV页表项中的flags，明确表示它指向的是数据页（VRW），还是下层页表（V）。假如把一个二级页表项，当做一级页表项来解读，就会触发异常，而这是自映射机制中必须的操作。为了绕开这个问题，就要求**在访问一级页表虚地址期间，将它所对应的二级页表项flags置为VRW**。此外，为了访问二级页表本身，还需要再加一个自映射的二级页表项，其flags为VRW。

### 写时复制

TODO

详见`cow.rs`

### 交换机制

位于`swap/mod.rs`，首先定义了以下接口：

* SwapManager：实现具体的页交换算法，本质是一个所有可交换页的优先队列
* Swapper：交换的具体执行者，负责将数据换入/换出到其它存储设备

其它3个文件都是对上述接口的实现。

基于上述接口，定义了实现交换操作的扩展结构：

```rust
SwapExt <T: PageTable, M: SwapManager, S: Swapper>
```

使用时，构造一个SwapExt结构套在原有的PageTable结构外面，例如：

```rust
let pt = MockPageTable::new();
let pt = SwapExt::new(pt, swap_manager, swapper);
```

它可被视为PageTable的一个“装饰器”，由于实现了Deref trait，它“继承“了PageTable的全部方法，同时还增加了以下新方法：

* swap\_in：换入一个指定页
* swap\_out：换出一个指定页
* swap\_out\_any：换出任意一个可交换页
* page\_fault\_handler：发生PageFault后，需要调用此函数，如返回true，说明发生了交换，不是一个异常。

### 内存管理器

位于`memory_set.rs`，定义了一个主要结构`MemorySet`，以及提供底层支持接口`InactivePageTable`。

一个MemorySet负责维护**一个线程所使用的内存资源**，相当于原版ucore中的`mm_struct`。一个MemorySet中包含若干`MemoryArea`结构，它用来描述**一段连续的、属性相同的虚地址空间及其映射**，相当于原版ucore中的`vma_struct`。其中又定义了`MemoryAttr`类型用来描述映射的**属性和访问权限**。

此外，每个MemorySet还包含一个唯一的页表（拥有它的所有权），即`InactivePageTable`，它里面的内存映射保持和各个MemoryArea相同。

由于MemorySet**掌管其控制的内存资源**，因此它实现了析构函数（即Drop特性），会在自己销毁时将内部各段从页表中取消映射，释放物理帧。当一个线程控制块销毁时，它所对应的MemorySet也自动销毁，由此实现内存资源的自动释放。

MemorySet的用法简单示意如下：

```rust
// 构造一个MemorySet
// 在背后会自动分配一个内核栈、创建一个页表（映射了内核空间）
let mut ms = MemorySet::<T>::new();

// 添加不同内存段
// 每次push的同时会在内部页表中建立映射
// 可使用MemoryArea不同的构造函数：
// * new_identity：虚实地址相同
// * new_physical：虚实地址有一个偏移
// * new：动态分配物理内存
ms.push(MemoryArea::new_identity(text_begin, text_end, MemoryAttr::default().execute().readonly(), "text"));
ms.push(MemoryArea::new_identity(data_begin, data_end, MemoryAttr::default(), "data"));
ms.push(MemoryArea::new(bss_begin, bss_end, MemoryAttr::default(), "bss"));

// 使页表生效（设置CR3/satp）
unsafe { ms.activate(); }
```

在内核中的使用场景有两处：

* 为RISCV映射内核：`arch/riscv32/memory.rs::remap_the_kernel()`
* 映射用户程序：`process/context.rs::memory_set_from(elf)`

为了使用MemorySet，需要实现`InactivePageTable` 接口。这里有必要解释一下：既然已经有了`PageTable`接口，为什么还要定义一个新的？这两种页表到底有什么区别？

我们在上面【页表的具体实现】一节中提到，目前的实现使用**自映射机制**完成对页表自身的修改，它带来的一个问题是：要编辑一个页表，必须要求【当前生效页表的自映射项】指向这个待编辑页表。因此：在修改每个页表的前后，都要修改【当前生效页表的自映射项】。换句话说，任何一个**未生效**的页表都是**不可访问**的，必须完成特定操作才能让它**可被访问**。（在RISCV中更甚：任何一个一级页表都是**不可访问**的，必须修改相应页表项中的属性位，才能让它**可被访问**）

因此，我们必须用一个新的接口表示**暂时不可编辑的页表**。它包含一个方法，可以将自己临时转换为可编辑的状态。现在我们可以来看`InactivePageTable`的具体定义了：

```rust
/// 表示一个未生效的，暂时不可编辑的页表
pub trait InactivePageTable {
    /// 关联类型，指向可编辑的页表类
    type Active: PageTable;

    /// 创建一个新的页表，同时映射内核空间
    fn new() -> Self;
    
    /// 创建一个新的页表，除设置自映射外，完全空白。
    /// 在设置第一个内核页表时使用。
    fn new_bare() -> Self;
    
    /// 修改此页表，在函数f中完成具体操作
    fn edit(&mut self, f: impl FnOnce(&mut Self::Active));
    
    /// 使此页表生效
    unsafe fn activate(&self);
    
    /// 临时使此页表生效，然后执行函数f。
    /// 用于完成用户程序的映射后，复制数据进去。
    unsafe fn with(&self, f: impl FnOnce());
    
    /// 返回此页表生效时的CR3/satp。
    /// 用于线程切换。
    fn token(&self) -> usize;

    // 以下接口为MemorySet的依赖操作。
    // 其实和页表无关（注意没有&self），为了简洁，放在这里。
    fn alloc_frame() -> Option<PhysAddr>;
    fn dealloc_frame(target: PhysAddr);
    fn alloc_stack() -> Stack;
}
```

x86\_64/RV32下的实现位于：`kernel/src/arch/../paging.rs`

## 其它部分

作为独立crate存在的内存管理模块，其实更适合被称为**虚存管理模块**。还有一些其他与内存管理相关的工作，已经有现成的crate完成了。

### 物理帧分配

物理帧分配是虚存管理的底层支持之一，对应的接口是`FrameAllocator`。

在原版ucore中，物理内存管理需要从可用的物理空间中分配和回收**不定长**的内存段（= malloc in C, new in C++），这一工作在RustOS中转移到了下面的**堆空间管理**。

在RustOS中的物理内存管理，被简化成在可用的物理空间中分配和回收**固定大小**的物理帧，其本质上是**对N个整数的分配和回收**。我们使用了类似线段树的分层Bitset结构，高效地完成上述工作。其实现位于独立的crate `bit-allocator`。

当然考虑到页表支持大页映射（32位：4M，64位：2M/1G），未来可对数据结构做一些修改。不过这会增加整体的逻辑复杂度。

### 堆空间管理

堆空间管理的任务是：在一片连续的内存空间中，分配和回收不定长的片段，给对象使用。

在Rust的core库中，定义了专门的[Alloc接口](https://doc.rust-lang.org/alloc/alloc/trait.GlobalAlloc.html)。当你实现了global\_allocator之后，就可以使用Box在堆上存储数据，进而可以使用alloc库中提供的丰富数据结构，如Vec、BTreeSet等。

原ucore中物理内存管理部分的诸多算法，如：First Fit、Last Fit、Best Fit，都可以在这个接口下实现（然而目前尚未实现）。

此外还有诸多crate实现了此接口，如：

* [linked\_list\_allocator](https://docs.rs/crate/linked_list_allocator)：blog\_os作者自己造的轮子
* [slab\_allocator](https://docs.rs/crate/slab_allocator)：拓展了上面的算法，Redox使用

目前RustOS就直接使用了第一个轮子。用法非常简单：

```rust
// 定义在lib.rs中
use linked_list_allocator::LockedHeap;
#[global_allocator]
static ALLOCATOR: LockedHeap = LockedHeap::empty();

// 使用前初始化
unsafe { ALLOCATOR.lock().init(heap_start, heap_size); }
```

关于堆管理的实现细节，推荐阅读 [Kernel Heap | Writing an OS in Rust](https://os.phil-opp.com/kernel-heap/)。&#x20;

### 内核栈分配

目前的做法是：直接使用上面的堆空间分配。

## 总结：使用指南

把内存管理相关逻辑独立成crate，一个重要目的就是方便复用。假设你在一个全新的平台上从头用Rust写了一个OS，该怎么用这个库呢？下面就来简单梳理一下：

#### 1. 选择或实现一个物理帧分配器，定义一个全局实例 FRAME\_ALLOCATOR

全局实例应该需要上锁。

#### 2. 为该平台实现页表接口：PageTable，Entry

如果不使用自映射，可能需要额外开辟空间维护页表的物理地址信息。

如果需要动态分配页表本身所占用的物理空间，就需要使用到 FRAME\_ALLOCATOR。

#### 2.1 定义一个PageTable的全局实例 ACTIVE\_TABLE

后面所有的编辑页表操作，都通过它进行。

#### 3. 实现InactivePageTable接口

如果使用自映射，八成就需要访问ACTIVE\_TABLE了。

完成后就可以使用MemorySet管理内存了。

#### 4.1 如需写时复制：在ACTIVE\_TABLE外面套一层 CowExt结构

#### 4.2 如需交换机制：在ACTIVE\_TABLE外面套一层 SwapExt结构

对于Swapper和SwapManager，可以从库中选取，也可以自己实现。

#### 4.3 如使用上面拓展：在缺页异常处理中调用`ACTIVE_TABLE.page_fault_handler()`

#### 5. 选择或实现一个global\_allocator，开启堆空间

## TODO

* [ ] 实现其他堆分配算法：First Fit、Last Fit、Best Fit……
* [ ] 实现其他页交换算法
* [ ] 为物理帧分配提供大页支持
* [ ] 实现延迟分配物理帧
* [ ] 在MemorySet层面提供共享内存和写时复制的接口
* [ ] 探讨尝试更优雅的接口与实现方式


---

# 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://rcore.gitbook.io/rust-os-docs/nei-cun-guan-li-mo-kuai.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.
