Ankylosauridae OS

在这里插入图片描述

文章目录

【结构框架】

├── bootloader
│   ├── rustsbi-k210.bin
│   └── rustsbi-qemu.bin
├── LICENSE
├── os
│   ├── build.rs
│   ├── Cargo.lock
│   ├── Cargo.toml
│   ├── Makefile
│   └── src
│       ├── config.rs(修改:新增一些内存管理的相关配置)
│       ├── console.rs
│       ├── entry.asm
│       ├── lang_items.rs
│       ├── link_app.S
│       ├── linker-k210.ld(修改:将跳板页引入内存布局)
│       ├── linker-qemu.ld(修改:将跳板页引入内存布局)
│       ├── loader.rs(修改:仅保留获取应用数量和数据的功能)
│       ├── main.rs(修改)
│       ├── mm(新增:内存管理的 mm 子模块)
│       │   ├── address.rs(物理/虚拟 地址/页号的 Rust 抽象)
│       │   ├── frame_allocator.rs(物理页帧分配器)
│       │   ├── heap_allocator.rs(内核动态内存分配器)
│       │   ├── memory_set.rs(引入地址空间 MemorySet 及逻辑段 MemoryArea 等)
│       │   ├── mod.rs(定义了 mm 模块初始化方法 init)
│       │   └── page_table.rs(多级页表抽象 PageTable 以及其他内容)
│       ├── sbi.rs
│       ├── sync
│       │   ├── mod.rs
│       │   └── up.rs
│       ├── syscall
│       │   ├── fs.rs(修改:基于地址空间的 sys_write 实现)
│       │   ├── mod.rs
│       │   └── process.rs
│       ├── task
│       │   ├── context.rs(修改:构造一个跳转到不同位置的初始任务上下文)
│       │   ├── mod.rs(修改,详见文档)
│       │   ├── switch.rs
│       │   ├── switch.S
│       │   └── task.rs(修改,详见文档)
│       ├── timer.rs
│       └── trap
│           ├── context.rs(修改:在 Trap 上下文中加入了更多内容)
│           ├── mod.rs(修改:基于地址空间修改了 Trap 机制,详见文档)
│           └── trap.S(修改:基于地址空间修改了 Trap 上下文保存与恢复汇编代码)
├── README.md
├── rust-toolchain
├── tools
│   ├── kflash.py
│   ├── LICENSE
│   ├── package.json
│   ├── README.rst
│   └── setup.py
└── user
    ├── build.py(移除)
    ├── Cargo.toml
    ├── Makefile
    └── src
        ├── bin
        │   ├── 00power_3.rs
        │   ├── 01power_5.rs
        │   ├── 02power_7.rs
        │   └── 03sleep.rs
        ├── console.rs
        ├── lang_items.rs
        ├── lib.rs
        ├── linker.ld(修改:将所有应用放在各自地址空间中固定的位置)
        └── syscall.rs

【以往实验目标】

LibOS目标:让APP与硬件隔离,简化应用访问硬件的难度和复杂性

BatchOS目标:让APP与OS隔离,加强系统安全,提高执行效率

multiprog & time-sharing OS目标:让APP有效共享CPU,提高系统总体性能和效率

【本实验目标】

简化编程,APP不用考虑其运行时的起始执行地址

  • 与编译器达成共识,给每个APP设定一个固定起始执行地址

隔离APP访问的内存地址空间

  • APP的内存地址空间划界,不能越界访问OS和其他APP

【总体思路】

  • 编译:应用程序和内核独立编译,合并为一个镜像
  • 编译:不同应用程序可采用统一的起始地址
  • 构造:系统调用服务,任务的管理与初始化
  • 构造:建立基于页表机制的虚存空间
  • 运行:特权级切换,任务与OS相互切换
  • 运行:切换地址空间,跨地址空间访问数据

应用程序运行的角度看:就是存在一个从“0”地址开始的非常大的可读/可写/可执行的地址空间(Address Space)

操作系统的角度看:每个应用被局限在分配给它的物理内存空间中运行,无法读写其它应用和操作系统所在的内存空间

【Rust中的动态内存分配】

目前为止,如果将当前的操作系统内核也看成一个应用,那么其中所有的变量都是被静态分配在内存中的。我们希望能在操作系统中提供动态申请和释放内存的能力,这样就可以加强操作系统对各种以内存为基础的资源分配与管理。内核中支持动态内存分配,以能使用 Rust 核心库中各种灵活的动态数据结构,如 Vec、HashMap 等,且不用考虑这些数据结构的动态内存释放的繁琐操作,充分利用 Rust 语言保证的内存安全能力。

需要操作系统需要有如下功能

  • 初始时提供一块大内存空间作为初始的“堆”。在没有分页机制情况下,这块空间是物理内存空间,否则就是虚拟内存空间。
  • 提供在堆上分配和释放内存的函数接口。这样函数调用方通过分配内存函数接口得到地址连续的空闲内存块进行读写,也能通过释放内存函数接口回收内存,以备后续的内存分配请求。
  • 提供空闲空间管理的连续内存分配算法。相关算法能动态地维护一系列空闲和已分配的内存块,从而有效地管理空闲块。

可以理解为我们实现了动态内存分配后就可以使用rust的动态数据结构了,原来都是静态分配所以用不了。

【静态内存分配与动态内存分配】

【静态分配】

在编译器编译程序时已经知道这些变量所占的字节大小,于是给它们分配一块固定的内存将它们存储其中,这样变量在栈帧/数据段中的位置就被固定了下来。这些变量是被 静态分配 (Static Allocation) 的,这一过程来源于我们在程序中对变量的声明,在编译期由编译器完成。(事实上,我们在程序中能够 直接 看到的变量都是被静态分配在栈或者全局数据段上的,它们大小在编译期已知)

举例说明->静态分配不能解决的问题:比如,需要将一个文件读到内存进行处理,而且必须将文件一次性完整读进来处理。我们可以选择声明一个栈上的数组(局部变量)或者数据段中的数组(全局变量),作为缓冲区来暂存文件的内容。但我们在编程的时候并不知道待处理的文件大小,只能根据经验将缓冲区的大小设置为某一固定常数。在程序真正运行的时候,如果待处理的文件很小,那么缓冲区多出的部分被浪费掉了;如果待处理的文件很大,应用则无法正常运行。

【动态分配】

应用放置了一个大小可以随着应用的运行动态增减的内存空间 – 堆(Heap)。同时,应用还要能够将这个堆管理起来,即支持在运行的时候从里面分配一块空间来存放变量,而在变量的生命周期结束之后,这块空间需要被回收以待后面的使用。(我们通过返回的指针变量来 间接 访问堆空间上的数据)

动态内存分配的缺点:它背后运行着连续内存分配算法,相比静态分配会带来一些额外的开销。如果动态分配非常频繁,可能会产生很多无法使用的内存碎片,甚至可能会成为应用的性能瓶颈。

【内存碎片】

应用进行多次不同大小的内存分配和释放操作后,会产生内存空间的浪费,即存在无法被应用使用的空闲内存碎片。

内碎片:已被分配出去(属于某个在运行的应用)内存区域,占有这些区域的应用并不使用这块区域,操作系统也无法利用这块区域。

外碎片:还没被分配出去(不属于任何在运行的应用)内存空闲区域,由于太小而无法分配给提出申请内存空间的应用

【Rust的堆数据结构】

首先是一类 智能指针 (Smart Pointer) 。智能指针和 Rust 中的其他两类指针:裸指针 *const T/*mut T引用 &T/&mut T 一样,都指向地址空间中的另一个区域并包含它的位置信息。但它们携带的信息数量不等。

  • 裸指针 *const T/*mut T 基本等价于 C/C++ 里面的普通指针 T* ,它自身的内容仅仅是一个地址。它最为灵活,但是也最不安全
  • 引用 &T/&mut T 实质上只是一个地址范围,但是 Rust 编译器会在编译的时候进行比较严格的 借用检查 (Borrow Check) ,来确保在编译期就解决掉很多内存不安全问题。
  • 智能指针不仅包含它指向区域的地址范围,还含有一些额外的信息,它不仅可以作为一个媒介来访问它指向的数据,还能在这个过程中起到管理和控制的功能。
【Rust动态内存分配相关的智能指针】
  • Box<T> 在创建时会在堆上分配一个类型为 T 的变量,它自身也只保存在堆上的那个变量的位置,当 Box<T> 被回收的时候,它指向的那个变量(位于堆上)也会被回收。

    Box<T> 可以对标 C++ 的 std::unique_ptr

  • Rc<T> 是一个单线程上使用的引用计数类型,它提供了多所有权支持,即可同时存在多个智能指针指向同一个堆上变量的 Rc<T> ,它们都可以拿到指向变量的不可变引用来访问这同一个变量。它同时也是一个引用计数,事实上在堆上的另一个位置维护了这个变量目前被引用的次数 N ,即存在 N 个 Rc<T> 智能指针。当这个计数变为零之后,这个智能指针变量本身以及被引用的变量都会被回收。Arc<T>Rc<T> 功能相同,只是**Arc<T> 可以在多线程**上使用。

  • RefCell<T>借用检查 在运行时进行。对于 RefCell,如果违反借用规则,程序会编译通过,但会在运行时 panic 并退出。使用 RefCell<T> 的好处是,可在其自身是不可变的情况下修改其内部的值。

  • Mutex<T> 是一个互斥锁,在多线程中使用。它可以保护里层的堆上的变量同一时间只有一个线程能对它进行操作,从而避免数据竞争。Mutex<T> 时常和 Arc<T> 配套使用,变成 Arc<Mutex<T>> 这种经典组合结构,让最里层基于泛型 T 数据结构的变量可以在线程间安全传递。

【Rust动态内存分配相关的容器类型】

基于上述智能指针,可形成更强大的 集合 (Collection) 或称 容器 (Container) 类型,它们负责管理一组数目可变的元素:

  • 向量 Vec<T> 类似于 C++ 中的 std::vector
  • 键值对容器 BTreeMap<K, V> 类似于 C++ 中的 std::map
  • 有序集合 BTreeSet<T> 类似于 C++ 中的 std::set
  • 链表 LinkedList<T> 类似于 C++ 中的 std::list
  • 双端队列 VecDeque<T> 类似于 C++ 中的 std::deque
  • 变长字符串 String 类似于 C++ 中的 std::string

../_images/rust-containers.png

【其他语言动态内存分配】
  • C 语言仅支持 malloc/free 这一对操作,它们必须恰好成对使用,否则就会出现各种内存错误。
  • Python/Java 通过 引用计数 (Reference Counting) 对所有的对象进行运行时的动态管理,一套 垃圾回收 (GC, Garbage Collection) 机制会被自动定期触发,每次都会检查所有的对象,如果其引用计数为零则可以将该对象占用的内存从堆上回收以待后续其他的对象使用。完全杜绝了内存安全问题,但是性能开销则很大,而且 GC 触发的时机和每次 GC 的耗时都是无法预测的,还使得软件的执行性能不够确定。
  • C++智能指针(shared_ptr、unique_ptr、weak_ptr、auto_ptr等)和 资源获取即初始化 (RAII, 指将一个使用前必须获取的资源的生命周期绑定到一个变量上,变量释放时,对应的资源也一并释放) 风格都是致力于解决内存安全问题。但这些编程方式是“建议”而不是“强制”。

在动态内存分配方面,Rust 和 C++ 很像,事实上 Rust 有意从 C++ 借鉴了这部分优秀特性,并强制Rust编程人员遵守借用规则 。在 Rust 中,不限于堆内存,将某种资源的生命周期与一个变量绑定的这种 RAII 的思想无处不在,甚至这种资源可能只是另外一种类型的变量。

【在内核中支持动态内存分配】

Rust语言在 alloc crate 中设定了一套简洁规范的接口,只要实现了这套接口,内核就可以很方便地支持动态内存分配了。当我们使用 Rust 标准库 std 的时候可以不用关心这个 crate ,因为标准库内已经已经实现了一套堆管理算法,并将 alloc 的内容包含在 std 名字空间之下让开发者可以直接使用。然而操作系统内核运行在禁用标准库(即 no_std )的裸机平台上,核心库 core 也并没有动态内存分配的功能,这个时候就要考虑利用 alloc 库定义的接口来实现基本的动态内存分配器

其需要实现在本节开始介绍的一系列功能:初始化堆、分配/释放内存块的函数接口、连续内存分配算法

alloc 库需要我们提供给它一个 全局的动态内存分配器 ,它会利用该分配器来管理堆空间,从而使得与堆相关的智能指针或容器数据结构可以正常工作。具体而言,我们的动态内存分配器需要实现它提供的 GlobalAlloc Trait,这个 Trait 有两个必须实现的抽象接口

// alloc::alloc::GlobalAlloc
pub unsafe fn alloc(&self, layout: Layout) -> *mut u8;
pub unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout);

它们类似 C 语言中的 malloc/free ,分别代表堆空间的分配和回收,也同样使用一个裸指针(也就是地址)作为分配的返回值和回收的参数。两个接口中都有一个 alloc::alloc::Layout 类型的参数, 它指出了分配的需求,分为两部分,分别是所需空间的大小 size ,以及返回地址的对齐要求 align 。这个对齐要求必须是一个 2 的幂次,单位为字节数,限制返回的地址必须是 align 的倍数。

【引用伙伴分配器 -> os/Cargo.toml】

将我们的动态内存分配器类型实例化为一个全局变量,并使用 #[global_allocator] 语义项标记即可。由于该分配器的实现比较复杂,我们这里直接使用一个已有的伙伴分配器(buddy_system_allocator)实现

# os/Cargo.toml
[package]
name = "os"
version = "0.1.0"
authors = ["Yifan Wu <shinbokuow@163.com>"]
edition = "2018"

[dependencies]
bitflags = "1.2.1"
buddy_system_allocator = "0.6"
lazy_static = { version = "1.4.0", features = ["spin_no_std"] }
log = "0.4"
riscv = { git = "https://github.com/rcore-os/riscv", features = ["inline-asm"] }
spin = "0.9"
lock_api = "=0.4.6"
xmas-elf = "0.7.0"

Cargo.toml 又被称为清单( manifest ),文件格式是 TOML,每一个清单文件都由以下部分组成:

  • [package] — 定义项目( package )的元信息
  • name — 名称
  • version — 版本
  • authors — 开发作者
  • edition — Rust 版本
  • [dependencies] — 项目依赖包,我们的项目可以引用在 crates.ioGitHub 上的依赖包,也可以引用存放在本地文件系统中的依赖包。
    • crates.io引入依赖包:默认下,Cargo 就从 crates.io 上下载依赖包,只需要一个包名和版本号即可:符合 "x.y.z" 的形式,其中 x 被称为主版本major, y 被称为小版本 minor ,而 z 被称为补丁 patch,可以看出从左到右,版本的影响范围逐步降低,补丁的更新是无关痛痒的,并不会造成 API 的兼容性被破坏。
    • 从其他注册服务引入依赖包:引入 git 仓库中的库作为依赖包,你至少需要提供一个仓库的地址:由于没有指定版本,Cargo 会假定我们使用 mastermain 分支的最新 commit 。可以使用 revtagbranch 来指定想要拉取的版本。
    • 通过路径引入本地依赖包:Cargo 支持通过路径的方式来引入本地的依赖包
【实例化全局动态内存分配器 -> os/src/mm/heap_allocator.rs】
// os/src/mm/heap_allocator.rs
//! The global allocator
use crate::config::KERNEL_HEAP_SIZE;
use buddy_system_allocator::LockedHeap;

#[global_allocator]
/// heap allocator instance
static HEAP_ALLOCATOR: LockedHeap = LockedHeap::empty();

#[alloc_error_handler]
/// panic when heap allocation error occurs
pub fn handle_alloc_error(layout: core::alloc::Layout) -> ! {
    panic!("Heap allocation error, layout = {:?}", layout);
}

/// heap space ([u8; KERNEL_HEAP_SIZE])
static mut HEAP_SPACE: [u8; KERNEL_HEAP_SIZE] = [0; KERNEL_HEAP_SIZE];

/// initiate heap allocator
pub fn init_heap() {
    unsafe {
        HEAP_ALLOCATOR
            .lock()
            .init(HEAP_SPACE.as_ptr() as usize, KERNEL_HEAP_SIZE);
    }
}
  • 第 3 行:确定内核堆大小为0x30_0000

  • 第 4 行:引入伙伴系统buddy_system_allocator

  • 第 6 行:允许自定义配置全局分配器

  • 第 8 行:直接将 buddy_system_allocator 中提供的 LockedHeap 实例化成一个全局变量,并使用 alloc 要求的 #[global_allocator] 语义项进行标记。注意 LockedHeap 已经实现了 GlobalAlloc 要求的抽象接口了。

    个人理解:由于我们的系统没有使用标准库所以是没有堆的内存分配方法的,但是rust给我们留了相应的堆的内存分配接口为GlobalAlloc,我们通过实例化第三方已经实现了GlobalAlloc抽象接口的内存分配器,并将它绑定到对应的GlobalAlloc供我们使用。在这里插入图片描述

  • 第 10 行:内存分配处理特性。

    对于no_std的应用只有在同时具有#[global_allocator] 属性和#[alloc_error_handler] 属性下才能使用标准库的alloc crate

  • 第 12 - 14 行:处理动态内存分配失败的情形,在这种情况下我们直接 panic 。

  • 第 17 行:对内核堆空间进行初始化。这块内存是一个 static mut 且被零初始化的字节数组,位于内核的 .bss 段中。

  • 第 20 - 26 行:在使用任何 alloc 中提供的堆数据结构之前,我们需要先调用 init_heap 函数来给我们的全局分配器一块内存用于分配LockedHeap 也是一个被互斥锁 Mutex<T> 保护的类型,在对它任何进行任何操作之前都要先获取锁以避免其他线程同时对它进行操作导致数据竞争。然后,调用 init 方法告知它能够用来分配的空间的起始地址和大小即可。

Rust中的动态内存分配小结(堆的建立):其目的是为了让我们能够使用Rust核心库中各种灵活的动态数据结构,但由于没有标准库std的支持,所以我们需要手动完成动态内存的分配也就是堆的建立。而Rust在初始就想到了这个问题,所以Rust语言在 alloc crate 中设定了一套简洁规范的接口,只要实现了这套接口,内核就可以很方便地支持动态内存分配了。由于动态内存分配较为困难,所以本系统使用的是一个已有的伙伴分配器(buddy_system_allocator)实现,具体流程如下所示:

  1. 将伙伴分配器的项目依赖包导入进去。
  2. 对伙伴分配器实例化。
  3. 指定伙伴分配器与Rust预留的动态内存分配接口对应。
  4. 增加堆分配错误处理函数。
  5. 增加初始化函数,赋予其内存使其可以在这段内存上通过伙伴分配器对其进行分配。(堆建立完成)

【实现 SV39 多级页表机制】

【虚拟地址和物理地址】

【内存控制相关的CSR(控制与状态)寄存器】

默认情况下 MMU 未被使能,此时无论 CPU 处于哪个特权级,访存的地址都将直接被视作物理地址。 可以通过修改 S 特权级satp CSR 来启用分页模式,此后 S 和 U 特权级的访存地址会被视为虚拟地址,经过 MMU 的地址转换获得对应物理地址,再通过它来访问物理内存

satp 的字段分布:

../_images/satp.png

  • MODE :控制 CPU 使用哪种页表实现。当 MODE 设置为 0 的时候,代表所有访存都被视为物理地址;而设置为 8 的时候,SV39 分页机制被启用,所有 S/U 特权级的访存被视为一个 39 位的虚拟地址,它们需要先经过 MMU 的地址转换流程,如果顺利的话,则会变成一个 56 位的物理地址来访问物理内存;否则则会触发异常,这体现了分页机制的内存保护能力。
  • ASID :表示地址空间标识符,这里还没有涉及到进程的概念,我们不需要管这个地方;
  • PPN :存的是根页表所在的物理页号。这样,给定一个虚拟页号,CPU 就可以从三级页表的根页表开始一步步的将其映射到一个物理页号。
【地址格式与组成】

../_images/sv39-va-pa.png

分页管理的单个页面的大小设置为4KiB, 也就是说虚拟/物理地址区间 [0,4KiB) 为第 0 个虚拟页面/物理页帧,而 [4KiB,8KiB) 为第 1 个,以此类推。4K 需要用 12 位字节地址来表示。因此虚拟地址和物理地址都被分成两部分:它们的低 12 位,即 [11:0] 被称为 页内偏移 (Page Offset) ,它描述一个地址指向的字节在它所在页面中的相对位置。而虚拟地址的高 27 位,即 [38:12] 为它的虚拟页号 VPN,同理物理地址的高 44 位,即 [55:12] 为它的物理页号 PPN,页号可以用来定位一个虚拟/物理地址属于哪一个虚拟页面/物理页帧

前面定位属于哪个页,后面为页内偏移指向确定的一个地址

地址转换是以页为单位进行的,在地址转换的前后地址的页内偏移部分不变。可以认为 MMU 只是从虚拟地址中取出 27 位虚拟页号,在页表中查到其对应的物理页号(如果存在的话),最后将得到的44位的物理页号与虚拟地址的12位页内偏移依序拼接到一起就变成了56位的物理地址

通过虚拟页号在页表中找到对应的物理页号,则真正的物理地址为物理页号所指向的基地址加页内偏移。

RISC-V 64 架构中虚拟地址为何只有 39 位?

在 64 位架构上虚拟地址长度确实应该和位宽一致为 64 位,但是在启用 SV39 分页模式下,只有低 39 位是真正有意义的。SV39 分页模式规定 64 位虚拟地址的 [63:39] 这 25 位必须和第 38 位相同,否则 MMU 会直接认定它是一个不合法的虚拟地址。通过这个检查之后 MMU 再取出低 39 位尝试将其转化为一个 56 位的物理地址。

也就是说,所有 2^64 个虚拟地址中,只有最低的 256GiB (当第 38 位为 0 时)以及最高的 256GiB (当第 38 位为 1 时)是可能通过 MMU 检查的。

【地址相关的数据结构抽象与类型定义 -> os/src/mm/address.rs】

地址转换的核心任务在于如何维护虚拟页号到物理页号的映射——也就是页表。不过在具体实现它之前,我们先将地址和页号的概念抽象为 Rust 中的类型,借助 Rust 的类型安全特性来确保它们被正确实现。

//! Implementation of physical and virtual address and page number.
// os/src/mm/address.rs
use super::PageTableEntry;
use crate::config::{PAGE_SIZE, PAGE_SIZE_BITS};
use core::fmt::{self, Debug, Formatter};

/// physical address
#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub struct PhysAddr(pub usize);

/// virtual address
#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub struct VirtAddr(pub usize);

/// physical page number
#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub struct PhysPageNum(pub usize);

/// virtual page number
#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub struct VirtPageNum(pub usize);

/// Debugging

impl Debug for VirtAddr {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.write_fmt(format_args!("VA:{:#x}", self.0))
    }
}
impl Debug for VirtPageNum {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.write_fmt(format_args!("VPN:{:#x}", self.0))
    }
}
impl Debug for PhysAddr {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.write_fmt(format_args!("PA:{:#x}", self.0))
    }
}
impl Debug for PhysPageNum {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.write_fmt(format_args!("PPN:{:#x}", self.0))
    }
}

/// T: {PhysAddr, VirtAddr, PhysPageNum, VirtPageNum}
/// T -> usize: T.0
/// usize -> T: usize.into()

impl From<usize> for PhysAddr {
    fn from(v: usize) -> Self {
        Self(v)
    }
}
impl From<usize> for PhysPageNum {
    fn from(v: usize) -> Self {
        Self(v)
    }
}
impl From<usize> for VirtAddr {
    fn from(v: usize) -> Self {
        Self(v)
    }
}
impl From<usize> for VirtPageNum {
    fn from(v: usize) -> Self {
        Self(v)
    }
}
impl From<PhysAddr> for usize {
    fn from(v: PhysAddr) -> Self {
        v.0
    }
}
impl From<PhysPageNum> for usize {
    fn from(v: PhysPageNum) -> Self {
        v.0
    }
}
impl From<VirtAddr> for usize {
    fn from(v: VirtAddr) -> Self {
        v.0
    }
}
impl From<VirtPageNum> for usize {
    fn from(v: VirtPageNum) -> Self {
        v.0
    }
}

impl VirtAddr {
    pub fn floor(&self) -> VirtPageNum {
        VirtPageNum(self.0 / PAGE_SIZE)
    }
    pub fn ceil(&self) -> VirtPageNum {
        VirtPageNum((self.0 - 1 + PAGE_SIZE) / PAGE_SIZE)
    }
    pub fn page_offset(&self) -> usize {
        self.0 & (PAGE_SIZE - 1)
    }
    pub fn aligned(&self) -> bool {
        self.page_offset() == 0
    }
}
impl From<VirtAddr> for VirtPageNum {
    fn from(v: VirtAddr) -> Self {
        assert_eq!(v.page_offset(), 0);
        v.floor()
    }
}
impl From<VirtPageNum> for VirtAddr {
    fn from(v: VirtPageNum) -> Self {
        Self(v.0 << PAGE_SIZE_BITS)
    }
}
impl PhysAddr {
    pub fn floor(&self) -> PhysPageNum {
        PhysPageNum(self.0 / PAGE_SIZE)
    }
    pub fn ceil(&self) -> PhysPageNum {
        PhysPageNum((self.0 - 1 + PAGE_SIZE) / PAGE_SIZE)
    }
    pub fn page_offset(&self) -> usize {
        self.0 & (PAGE_SIZE - 1)
    }
    pub fn aligned(&self) -> bool {
        self.page_offset() == 0
    }
}
impl From<PhysAddr> for PhysPageNum {
    fn from(v: PhysAddr) -> Self {
        assert_eq!(v.page_offset(), 0);
        v.floor()
    }
}
impl From<PhysPageNum> for PhysAddr {
    fn from(v: PhysPageNum) -> Self {
        Self(v.0 << PAGE_SIZE_BITS)
    }
}

impl VirtPageNum {
    pub fn indexes(&self) -> [usize; 3] {
        let mut vpn = self.0;
        let mut idx = [0usize; 3];
        for i in (0..3).rev() {
            idx[i] = vpn & 511;
            vpn >>= 9;
        }
        idx
    }
}

impl PhysPageNum {
    pub fn get_pte_array(&self) -> &'static mut [PageTableEntry] {
        let pa: PhysAddr = (*self).into();
        unsafe { core::slice::from_raw_parts_mut(pa.0 as *mut PageTableEntry, 512) }
    }
    pub fn get_bytes_array(&self) -> &'static mut [u8] {
        let pa: PhysAddr = (*self).into();
        unsafe { core::slice::from_raw_parts_mut(pa.0 as *mut u8, 4096) }
    }
    pub fn get_mut<T>(&self) -> &'static mut T {
        let pa: PhysAddr = (*self).into();
        unsafe { (pa.0 as *mut T).as_mut().unwrap() }
    }
}

pub trait StepByOne {
    fn step(&mut self);
}
impl StepByOne for VirtPageNum {
    fn step(&mut self) {
        self.0 += 1;
    }
}

#[derive(Copy, Clone)]
/// a simple range structure for type T
pub struct SimpleRange<T>
where
    T: StepByOne + Copy + PartialEq + PartialOrd + Debug,
{
    l: T,
    r: T,
}
impl<T> SimpleRange<T>
where
    T: StepByOne + Copy + PartialEq + PartialOrd + Debug,
{
    pub fn new(start: T, end: T) -> Self {
        assert!(start <= end, "start {:?} > end {:?}!", start, end);
        Self { l: start, r: end }
    }
    pub fn get_start(&self) -> T {
        self.l
    }
    pub fn get_end(&self) -> T {
        self.r
    }
}
impl<T> IntoIterator for SimpleRange<T>
where
    T: StepByOne + Copy + PartialEq + PartialOrd + Debug,
{
    type Item = T;
    type IntoIter = SimpleRangeIterator<T>;
    fn into_iter(self) -> Self::IntoIter {
        SimpleRangeIterator::new(self.l, self.r)
    }
}
/// iterator for the simple range structure
pub struct SimpleRangeIterator<T>
where
    T: StepByOne + Copy + PartialEq + PartialOrd + Debug,
{
    current: T,
    end: T,
}
impl<T> SimpleRangeIterator<T>
where
    T: StepByOne + Copy + PartialEq + PartialOrd + Debug,
{
    pub fn new(l: T, r: T) -> Self {
        Self { current: l, end: r }
    }
}
impl<T> Iterator for SimpleRangeIterator<T>
where
    T: StepByOne + Copy + PartialEq + PartialOrd + Debug,
{
    type Item = T;
    fn next(&mut self) -> Option<Self::Item> {
        if self.current == self.end {
            None
        } else {
            let t = self.current;
            self.current.step();
            Some(t)
        }
    }
}

/// a simple range structure for virtual page number
pub type VPNRange = SimpleRange<VirtPageNum>;
  • 第 8 行:通过derive特征派生语法实现默认的Copy, Clone, Ord, PartialOrd, Eq, PartialEq特征代码。

  • 第 9 行:实现了物理地址的 Rust 类型声明。

  • 第 13 行:实现了虚拟地址的 Rust 类型声明。

  • 第 17 行:实现了物理页号的 Rust 类型声明。

  • 第 21 行:实现了虚拟页号的 Rust 类型声明。

    它们都是 Rust 的元组式结构体,可以看成 usize 的一种简单包装。我们刻意将它们各自抽象出不同的类型而不是都使用与RISC-V 64硬件直接对应的 usize 基本类型,就是为了在 Rust 编译器的帮助下,通过多种方便且安全的 类型转换 (Type Conversion) 来构建页表。

  • 第 25 - 44 行:为四个类型实现了Debug的方法。

  • 第 50 - 69 行:为四个类型实现了From的方法。传入一个usize生成对应的类型。

  • 第 70 - 89 行:为四个类型实现了From的反法。传入对应的类型得到一个usize类型的值。

    类型转换之 From 和 Into

    当我们为类型 U 实现了 From<T> Trait 之后,可以使用 U::from(_: T) 来从一个 T 类型的实例来构造一个 U 类型的实例

    当我们为类型 U 实现了 Into<T> Trait 之后,对于一个 U 类型的实例 u ,可以使用 u.into() 来将其转化为一个类型为 T 的实例。

    当我们为 U 实现了 From<T> 之后,Rust 会自动为 T 实现 Into<U> Trait,因为它们两个本来就是在做相同的事情。因此我们只需相互实现 From 就可以相互 From/Into 了。

  • 第 91 行:为虚拟地址实现四个方法。

  • 第 92 - 94 行:对于虚拟页号为虚拟地址除以每页的大小,floor下取整

  • 第 95 - 97 行:对于虚拟页号为虚拟地址除以每页的大小,ceil上取整

  • 第 98 - 100 行:获取页内偏移,也就是只取低12位(&4095)的值。

  • 第 101 - 103 行:页内偏移的校准,是否存在页内偏移。

  • 第 105 - 110 行:实现虚拟地址到虚拟页号的转变,但是虚拟地址需要保证它与页面大小对齐才能通过右移转换为虚拟页号。为下取整。

  • 第 111 - 115 行:从虚拟页号到虚拟地址的转换只需左移 12 位即可。

  • 第 116 - 140 行:与91到115行一样,只不过转变为物理地址和物理页号。

  • 第 142 - 152 行:VirtPageNumindexes 可以取出虚拟页号的三级页索引,并按照从高到低的顺序返回。注意它里面包裹的 usize 可能有 27 位,也有可能有 64−12=52 位,但这里我们是用来在多级页表上进行遍历,因此只取出低 27 位。

  • 第 154 - 167 行:构造了可变引用来直接访问物理页号对应的物理页帧,不同的引用类型对应于物理页帧上的一种不同的内存布局

    • 第 155 - 158 行:实现了get_pte_array方法,返回的是一个页表项定长数组的可变引用,也就是512个usize类型的struct,可以用来修改多级页表中的一个节点。
    • 第 159 - 162 行:实现了get_bytes_array方法,返回的是一个字节数组的可变引用,也就是4096个字节,可以以字节为粒度对物理页帧上的数据进行访问。
    • 第 163 - 166 行:实现了get_mut 这个泛型函数,可以获取一个恰好放在一个物理页帧开头的类型为 T 的数据的可变引用。
  • 第 178 - 186 行:声明泛型SimpleRange结构体,里面具有lr两个成员。

  • 第 187 - 201 行:为泛型SimpleRange结构体实现方法。

    • 第 191 - 194 行:实现了new方法,l为开始地址,r为结束地址。
    • 第 195 - 197 行:实现了get_start方法,返回l
    • 第 198 - 200 行:实现了get_end方法,返回r
  • 第 245 行:声明虚拟页号区间结构体SimpleRange

【页表项的数据结构抽象与类型定义 -> os/src/mm/page_table.rs】

在页表中以虚拟页号作为索引不仅能够查到物理页号,还能查到一组保护位,它控制了应用对地址空间每个虚拟页面的访问权限。物理页号和全部的标志位以某种固定的格式保存在一个结构体中,它被称为页表项 (PTE, Page Table Entry) ,是利用虚拟页号在页表中查到的结果。下图为 SV39 分页模式下的页表项:

../_images/sv39-pte.png

  • [0] :V(Valid) 仅当位 V 为 1 时,页表项才是合法的
  • [3:1] :R(Read)/W(Write)/X(eXecute),分别控制索引到这个页表项的对应虚拟页面是否允许读/写/执行
  • [5]:U(User) 控制索引到这个页表项的对应虚拟页面是否在 CPU 处于 U 特权级的情况下是否被允许访问
  • [7]:A(Accessed) 处理器记录自从页表项上的这一位被清零之后,页表项的对应虚拟页面是否被访问过
  • [8]:D(Dirty) 处理器记录自从页表项上的这一位被清零之后,页表项的对应虚拟页面是否被修改过
  • [53:10] :这 44 位是物理页号。
//! Implementation of [`PageTableEntry`] and [`PageTable`].
// os/src/mm/page_table.rs

use super::{frame_alloc, FrameTracker, PhysPageNum, StepByOne, VirtAddr, VirtPageNum};
use alloc::vec;
use alloc::vec::Vec;
use bitflags::*;

bitflags! {
    /// page table entry flags
    pub struct PTEFlags: u8 {
        const V = 1 << 0;
        const R = 1 << 1;
        const W = 1 << 2;
        const X = 1 << 3;
        const U = 1 << 4;
        const G = 1 << 5;
        const A = 1 << 6;
        const D = 1 << 7;
    }
}

#[derive(Copy, Clone)]
#[repr(C)]
/// page table entry structure
pub struct PageTableEntry {
    pub bits: usize,
}

impl PageTableEntry {
    pub fn new(ppn: PhysPageNum, flags: PTEFlags) -> Self {
        PageTableEntry {
            bits: ppn.0 << 10 | flags.bits as usize,
        }
    }
    pub fn empty() -> Self {
        PageTableEntry { bits: 0 }
    }
    pub fn ppn(&self) -> PhysPageNum {
        (self.bits >> 10 & ((1usize << 44) - 1)).into()
    }
    pub fn flags(&self) -> PTEFlags {
        PTEFlags::from_bits(self.bits as u8).unwrap()
    }
    pub fn is_valid(&self) -> bool {
        (self.flags() & PTEFlags::V) != PTEFlags::empty()
    }
    pub fn readable(&self) -> bool {
        (self.flags() & PTEFlags::R) != PTEFlags::empty()
    }
    pub fn writable(&self) -> bool {
        (self.flags() & PTEFlags::W) != PTEFlags::empty()
    }
    pub fn executable(&self) -> bool {
        (self.flags() & PTEFlags::X) != PTEFlags::empty()
    }
}

/// page table structure
pub struct PageTable {
    root_ppn: PhysPageNum,
    frames: Vec<FrameTracker>,
}

/// Assume that it won't oom when creating/mapping.
impl PageTable {
    pub fn new() -> Self {
        let frame = frame_alloc().unwrap();
        PageTable {
            root_ppn: frame.ppn,
            frames: vec![frame],
        }
    }
    /// Temporarily used to get arguments from user space.
    pub fn from_token(satp: usize) -> Self {
        Self {
            root_ppn: PhysPageNum::from(satp & ((1usize << 44) - 1)),
            frames: Vec::new(),
        }
    }
    fn find_pte_create(&mut self, vpn: VirtPageNum) -> Option<&mut PageTableEntry> {
        let mut idxs = vpn.indexes();
        let mut ppn = self.root_ppn;
        let mut result: Option<&mut PageTableEntry> = None;
        for (i, idx) in idxs.iter_mut().enumerate() {
            let pte = &mut ppn.get_pte_array()[*idx];
            if i == 2 {
                result = Some(pte);
                break;
            }
            if !pte.is_valid() {
                let frame = frame_alloc().unwrap();
                *pte = PageTableEntry::new(frame.ppn, PTEFlags::V);
                self.frames.push(frame);
            }
            ppn = pte.ppn();
        }
        result
    }
    fn find_pte(&self, vpn: VirtPageNum) -> Option<&PageTableEntry> {
        let idxs = vpn.indexes();
        let mut ppn = self.root_ppn;
        let mut result: Option<&PageTableEntry> = None;
        for (i, idx) in idxs.iter().enumerate() {
            let pte = &ppn.get_pte_array()[*idx];
            if i == 2 {
                result = Some(pte);
                break;
            }
            if !pte.is_valid() {
                return None;
            }
            ppn = pte.ppn();
        }
        result
    }
    #[allow(unused)]
    pub fn map(&mut self, vpn: VirtPageNum, ppn: PhysPageNum, flags: PTEFlags) {
        let pte = self.find_pte_create(vpn).unwrap();
        assert!(!pte.is_valid(), "vpn {:?} is mapped before mapping", vpn);
        *pte = PageTableEntry::new(ppn, flags | PTEFlags::V);
    }
    #[allow(unused)]
    pub fn unmap(&mut self, vpn: VirtPageNum) {
        let pte = self.find_pte_create(vpn).unwrap();
        assert!(pte.is_valid(), "vpn {:?} is invalid before unmapping", vpn);
        *pte = PageTableEntry::empty();
    }
    pub fn translate(&self, vpn: VirtPageNum) -> Option<PageTableEntry> {
        self.find_pte(vpn).copied()
    }
    pub fn token(&self) -> usize {
        8usize << 60 | self.root_ppn.0
    }
}

/// translate a pointer to a mutable u8 Vec through page table
pub fn translated_byte_buffer(token: usize, ptr: *const u8, len: usize) -> Vec<&'static mut [u8]> {
    let page_table = PageTable::from_token(token);
    let mut start = ptr as usize;
    let end = start + len;
    let mut v = Vec::new();
    while start < end {
        let start_va = VirtAddr::from(start);
        let mut vpn = start_va.floor();
        let ppn = page_table.translate(vpn).unwrap().ppn();
        vpn.step();
        let mut end_va: VirtAddr = vpn.into();
        end_va = end_va.min(VirtAddr::from(end));
        if end_va.page_offset() == 0 {
            v.push(&mut ppn.get_bytes_array()[start_va.page_offset()..]);
        } else {
            v.push(&mut ppn.get_bytes_array()[start_va.page_offset()..end_va.page_offset()]);
        }
        start = end_va.into();
    }
    v
}
  • 第 9 - 21 行:实现了页表项中的标志位 PTEFlagsbitflags 是一个 Rust 中常用来比特标志位的 crate 。它提供了一个 bitflags! 宏,如上面的代码段所展示的那样,可以将一个 u8 封装成一个标志位的集合类型,支持一些常见的集合运算。需要在 cargo 中引入 bitflags 包。

  • 第 23 行:我们让编译器自动为 PageTableEntry 实现 Copy/Clone Trait,来让这个类型以值语义赋值/传参的时候不会发生所有权转移,而是拷贝一份新的副本。从这一点来说 PageTableEntry 就和 usize 一样,因为它也只是后者的一层简单包装,并解释了 usize 各个比特段的含义。

  • 第 26 - 28 行:定义了PTE的结构体。

  • 第 31 - 35 行:为PTE实现了new方法,将ppnPTEFlags按照上图结合起来。

  • 第 36 - 38 行:为PTE实现了清空方法。

  • 第 39 - 41 行:为PTE实现了获取ppn的方法,获取44位的值。

  • 第 42 - 44 行:为PTE实现了获取PTEFlags的方法。

  • 第 45 - 47 行:依据PTEFlagsV位判断是否可用。

  • 第 48 - 50 行:依据PTEFlagsR位判断是否可读。

  • 第 51 - 53 行:依据PTEFlagsW位判断是否可写。

  • 第 54 - 56 行:依据PTEFlagsE位判断是否可执行。

  • 第 60 - 63 行:创建页表的结构

    • 第 61 行:每个应用的地址空间都对应一个不同的多级页表,这也就意味这不同页表的起始地址(即页表根节点的地址)是不一样的。因此 PageTable 要保存它根节点的物理页号 root_ppn 作为页表唯一的区分标志
    • 第 62 行:向量 framesFrameTracker 的形式保存了页表所有的节点(包括根节点)所在的物理页帧。即将这些 FrameTracker 的生命周期进一步绑定到 PageTable 下面。当 PageTable 生命周期结束后,向量 frames 里面的那些 FrameTracker 也会被回收,也就意味着存放多级页表节点的那些物理页帧被回收了。
  • 第 67 - 73 行:为PageTable实现了new方法,通过 new 方法新建一个 PageTable 的时候,它只需有一个根节点。为此我们需要分配一个物理页帧 FrameTracker 并挂在向量 frames 下,然后更新根节点的物理页号 root_ppn

  • 第 75 - 80 行:from_token 可以临时创建一个专用来手动查页表的 PageTable ,它仅有一个从传入的 satp token 中得到的多级页表根节点的物理页号,它的 frames 字段为空,也即不实际控制任何资源;

  • 第 81 - 99 行:PageTable::find_pte_create 在多级页表找到一个虚拟页号对应的页表项的可变引用方便后续的读写。如果在遍历的过程中发现有节点尚未创建则会新建一个节点。

    • 第 82 行:获取虚拟页号的三级页索引。
    • 第 83 行:获取当前应用的根页索引。
    • 第 84 行:初始化result
    • 第 85 行:创建迭代器迭代三级页索引的每一项。
    • 第 86 行:获取当前页表所对应页索引的页表项。
    • 第 87 - 90 行:判断是否取到第0级索引,取到的话则返回该页表项。
    • 第 91 行:判断该页表项是否为空,不存在的话就新建一个节点,更新作为下级节点指针的页表项,并将新分配的物理页帧移动到向量 frames 中方便后续的自动回收。
    • 第 92 行:新建一个物理页帧,返回的是一个物理页号。
    • 第 93 行:更新页表项,物理页号为刚建的,还要将标志位 V 置 1,也就是存放当前页表里的页表项所对应的物理页号中存放的下一个页表。
    • 第 94 行:将新建的节点(包括根节点)所在的物理页帧保存进当前物理页上。
    • 第 96 行:切换到下一个页表上。

    三级页表的查找:首先查找 根页表[第三级索引],找到则切换页表,下一级页表号为 根页表[第三级索引] 里的值,没有则创建页表,将新创建的页表的页表号存放到 根页表[第三级索引] 里面,新增该应用的整个物理页数组,切换到下一级页表。重复三次后保存最后一次的页表项传出了,则该页表项对应的应该为物理地址。

  • 第 100 - 116 行:实现了PageTable::find_pte ,其与 find_pte_create 的不同在于当找不到合法叶子节点的时候不会新建叶子节点而是直接返回 None 即查找失败。它不会尝试对页表本身进行修改,但是注意它返回的参数类型是页表项的可变引用,也即它允许我们修改页表项。

  • 第 118 - 122 行:通过 map 方法来在多级页表中插入一个键值对,注意这里将物理页号 ppn 和页表项标志位 flags 作为不同的参数传入。也就是先找到虚拟页表对应的最后一个页表项,再创建一个物理页帧,将该物理页帧所对应的物理页号存到最后一个页表项的物理页号里。

  • 第 124 - 128 行:通过 unmap 方法来删除一个键值对,在调用时仅需给出作为索引的虚拟页号即可。也就是将虚拟页表对应的最后一个页表项中的值清空。

  • 第 129 - 131 行:translate 调用 find_pte 来实现,如果能够找到页表项,那么它会将页表项拷贝一份并返回,否则就返回一个 None

  • 第 138 - 158 行:page_table 提供了将应用地址空间中一个缓冲区转化为在内核空间中能够直接访问的形式的辅助函数。参数中的 token 是某个应用地址空间的 token , ptrlen 则分别表示该地址空间中的一段缓冲区的起始地址和长度(注:这个缓冲区的应用虚拟地址范围是连续的)。 translated_byte_buffer 会以向量的形式返回一组可以在内核空间中直接访问的字节数组切片

【多级页表】

按需分配策略:有多少合法的虚拟页号,我们就维护一个多大的映射,并为此使用多大的内存用来保存映射。应用的地址空间最开始均不合法,这样的页表自然不需要占用任何内存。而在后面,内核在该应用的页表上也就是一对对映射顺次被插入进来,自然页表所占据的内存大小也就逐渐增加。

../_images/sv39-full.png

SV39 分页机制等价于一颗字典树。SV39 模式中我们采用三级页表,即将 27 位的虚拟页号分为三个等长的部分,第 26-18 位为三级索引 VPN2 ,第17-9 位为二级索引 VPN1 ,第 8-0 位为一级索引 VPN0 。

我们也将页表分为三级页表,二级页表,一级页表。每个页表都用 9 位索引的,因此有 512 个页表项,而每个页表项都是 8 字节,因此每个页表大小都为 512×8=4KiB 。正好是一个物理页的大小。我们可以把一个页表放到一个物理页中,并用一个物理页号来描述它。事实上,三级页表的每个页表项中的物理页号可描述一个二级页表;二级页表的每个页表项中的物理页号可描述一个一级页表;一级页表中的页表项内容则和我们刚才提到的页表项一样,其内容包含物理页号,即描述一个要映射到的物理页。

假设我们有虚拟地址 (VPN2,VPN1,VPN0,offset) :

  • 我们首先会记录装载「当前所用的三级页表的物理页」的页号到 satp 寄存器中;
  • 把 VPN2 作为偏移在第三级页表的物理页中找到第二级页表的物理页号;
  • 把 VPN1 作为偏移在第二级页表的物理页中找到第一级页表的物理页号;
  • 把 VPN0 作为偏移在第一级页表的物理页中找到要访问位置的物理页号;
  • 物理页号对应的物理页基址(即物理页号左移12位)加上 offset 就是虚拟地址对应的物理地址。

这样处理器通过这种多次转换,终于从虚拟页号找到了一级页表项,从而得出了物理页号和虚拟地址所对应的物理地址。这种页表实现被称为 多级页表 (Multi-Level Page-Table) 。由于 SV39 中虚拟页号被分为三级 页索引 (Page Index) ,因此这是一种三级页表。

../_images/pte-rwx.png

刚才我们提到若页表项满足 R,W,X 都为 0,表明这个页表项指向下一级页表。在这里三级和二级页表项的 R,W,X 为 0 应该成立,因为它们指向了下一级页表。

【快表(TLB)】

物理内存的访问速度要比 CPU 的运行速度慢很多。如果我们按照页表机制循规蹈矩的一步步走,将一个虚拟地址转化为物理地址需要访问 3 次物理内存,得到物理地址后还需要再访问一次物理内存,才能完成访存。这无疑很大程度上降低了系统执行效率。

我们使用MMU中的 快表(TLB, Translation Lookaside Buffer) 来作为虚拟页号到物理页号的映射的页表缓存。当我们要进行一个地址转换时,会有很大可能对应的地址映射在近期已被完成过,所以我们可以先到 TLB 缓存里面去查一下,如果有的话我们就可以直接完成映射,而不用访问那么多次内存了。

在一个多任务系统中,可能同时存在多个任务处于运行/就绪状态,它们各自的多级页表在内存中共存,那么 MMU 应该如何知道当前做地址转换的时候要查哪一个页表呢?

satp CSR 布局其中的 PPN 字段指的就是多级页表根节点所在的物理页号。因此,每个应用的地址空间就可以用包含了它多级页表根节点所在物理页号的 satp CSR 代表。在我们切换任务的时候, satp 也必须被同时切换。

如果修改了 satp 寄存器,说明内核切换到了一个与先前映射方式完全不同的页表。此时快表里面存储的映射已经失效了,我们需要刷新整个TLB。

【物理页帧管理】

物理页帧既可以用来实际存放应用/内核的数据/代码,也能够用来存储应用/内核的多级页表。当Bootloader把操作系统内核加载到物理内存中后,物理内存上已经有一部分用于放置内核的代码和数据。我们需要将剩下的空闲内存以单个物理页帧为单位管理起来,当需要存放应用数据或扩展应用的多级页表时分配空闲的物理页帧,并在应用出错或退出的时候回收应用占有的所有物理页帧。

【可用物理页的分配与回收 -> os/src/mm/frame_allocator.rs】
  • 物理内存起始地址:0x80000000
  • 可用物理内存起始地址:os/src/linker.ldekernel 地址
  • 物理内存结束地址:在 config 子模块中0x80800000

物理内存包括:

  • 应用/内核的数据/代码/堆/栈
  • 空闲的空间

管理物理内存:

  • 物理内存上已经有一部分用于放置内核的代码和数据
  • 需要将剩下的空闲内存以单个物理页帧为单位管理起来
    • 当需要存应用数据或扩展应用的多级页表时分配空闲的物理页帧
    • 在应用出错或退出的时候回收应用占的所有物理页帧

我们用一个左闭右开的物理页号区间来表示可用的物理内存:

  • 区间的左端点应该是 ekernel 的物理地址以上取整方式转化成的物理页号;
  • 区间的右端点应该是 MEMORY_END 以下取整方式转化成的物理页号。
//! Implementation of [`FrameAllocator`] which 
//! controls all the frames in the operating system.
// os/src/mm/frame_allocator.rs

use super::{PhysAddr, PhysPageNum};
use crate::config::MEMORY_END;
use crate::sync::UPSafeCell;
use alloc::vec::Vec;
use core::fmt::{self, Debug, Formatter};
use lazy_static::*;

/// manage a frame which has the same lifecycle as the tracker
pub struct FrameTracker {
    pub ppn: PhysPageNum,
}

impl FrameTracker {
    pub fn new(ppn: PhysPageNum) -> Self {
        // page cleaning
        let bytes_array = ppn.get_bytes_array();
        for i in bytes_array {
            *i = 0;
        }
        Self { ppn }
    }
}

impl Debug for FrameTracker {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.write_fmt(format_args!("FrameTracker:PPN={:#x}", self.ppn.0))
    }
}

impl Drop for FrameTracker {
    fn drop(&mut self) {
        frame_dealloc(self.ppn);
    }
}

trait FrameAllocator {
    fn new() -> Self;
    fn alloc(&mut self) -> Option<PhysPageNum>;
    fn dealloc(&mut self, ppn: PhysPageNum);
}

/// an implementation for frame allocator
pub struct StackFrameAllocator {
    current: usize,
    end: usize,
    recycled: Vec<usize>,
}

impl StackFrameAllocator {
    pub fn init(&mut self, l: PhysPageNum, r: PhysPageNum) {
        self.current = l.0;
        self.end = r.0;
    }
}
impl FrameAllocator for StackFrameAllocator {
    fn new() -> Self {
        Self {
            current: 0,
            end: 0,
            recycled: Vec::new(),
        }
    }
    fn alloc(&mut self) -> Option<PhysPageNum> {
        if let Some(ppn) = self.recycled.pop() {
            Some(ppn.into())
        } else if self.current == self.end {
            None
        } else {
            self.current += 1;
            Some((self.current - 1).into())
        }
    }
    fn dealloc(&mut self, ppn: PhysPageNum) {
        let ppn = ppn.0;
        // validity check
        if ppn >= self.current || self.recycled.iter().any(|v| *v == ppn) {
            panic!("Frame ppn={:#x} has not been allocated!", ppn);
        }
        // recycle
        self.recycled.push(ppn);
    }
}

type FrameAllocatorImpl = StackFrameAllocator;

lazy_static! {
    /// frame allocator instance through lazy_static!
    pub static ref FRAME_ALLOCATOR: UPSafeCell<FrameAllocatorImpl> =
        unsafe { UPSafeCell::new(FrameAllocatorImpl::new()) };
}

/// initiate the frame allocator using `ekernel` and `MEMORY_END`
pub fn init_frame_allocator() {
    extern "C" {
        fn ekernel();
    }
    FRAME_ALLOCATOR.exclusive_access().init(
        PhysAddr::from(ekernel as usize).ceil(),
        PhysAddr::from(MEMORY_END).floor(),
    );
}

/// allocate a frame
pub fn frame_alloc() -> Option<FrameTracker> {
    FRAME_ALLOCATOR
        .exclusive_access()
        .alloc()
        .map(FrameTracker::new)
}

/// deallocate a frame
fn frame_dealloc(ppn: PhysPageNum) {
    FRAME_ALLOCATOR.exclusive_access().dealloc(ppn);
}

#[allow(unused)]
/// a simple test for frame allocator
pub fn frame_allocator_test() {
    let mut v: Vec<FrameTracker> = Vec::new();
    for i in 0..5 {
        let frame = frame_alloc().unwrap();
        info!("{:?}", frame);
        v.push(frame);
    }
    v.clear();
    for i in 0..5 {
        let frame = frame_alloc().unwrap();
        info!("{:?}", frame);
        v.push(frame);
    }
    drop(v);
    info!("frame_allocator_test passed!");
}
  • 第 13 - 15 行:将一个物理页帧的生命周期绑定到一个 FrameTracker 变量上,当一个 FrameTracker 被创建的时候,我们需要从 FRAME_ALLOCATOR 中分配一个物理页帧。

  • 第 17 - 26 行:将分配来的物理页帧的物理页号作为参数传给 FrameTrackernew 方法来创建一个 FrameTracker 实例。由于这个物理页帧之前可能被分配过并用做其他用途,我们在这里直接将这个物理页帧上的所有字节清零。

  • 第 34 - 38 行:当一个 FrameTracker 生命周期结束被编译器回收的时候,我们需要将它控制的物理页帧回收到 FRAME_ALLOCATOR 中,当一个 FrameTracker 实例被回收的时候,它的 drop 方法会自动被编译器调用,通过之前实现的 frame_dealloc 我们就将它控制的物理页帧回收以供后续使用了。

  • 第 40 - 44 行:声明一个 FrameAllocator Trait 来描述一个物理页帧管理器需要提供哪些功能。创建一个物理页帧管理器的实例,以及以物理页号为单位进行物理页帧的分配和回收。

  • 第 47 - 51 行:实现了一种栈式物理页帧管理策略,,

    • 第 48 - 49 行:从未被分配出去过的物理页号区间 [ current , end ) 也就是可用的物理页号
    • 第 50 行:recycled 以后入先出的方式保存了被回收的物理页号
  • 第 53 - 58 行:为StackFrameAllocator实现了初始化方法。 调用 init 方法将自身的 [current,end) 初始化为可用物理页号区间。

  • 第 59 - 86 行:为StackFrameAllocator实现了FrameAllocator这个Trait

    • 第 60 - 66 行:创建一个StackFrameAllocator,只需将可用区间两端均设为 0 , 然后创建一个新的Vec空间。
    • 第 67 - 76 行:创建一个物理页帧
      • 第 68 行:首先判断recycled里面是否有之前回收的物理页号。
      • 第 69 行:有的话则将其类型转换为物理页号传输出去。
      • 第 70 行:其次判断物理内存是否分配完了。
      • 第 71 行:有的话则返回空。
      • 第 73 行:将可用的物理页号起始数减一。
      • 第 74 行:分配刚减一的物理页号出去。
    • 第 77 - 85 行:回收指定的物理页帧,在回收 dealloc 的时候,我们需要检查回收页面的合法性,然后将其压入 recycled 栈中
      • 第 80 行:判断传入的ppn是否正确,由于该页面之前一定被分配出去过,因此它的物理页号一定 <current ;并且该页面没有正处在回收状态,即它的物理页号不能在栈 recycled 中找到。
      • 第 84 行:将其压入 recycled 栈中。
  • 第 88 行:创建 StackFrameAllocator 的全局实例 FrameAllocatorImpl,并

  • 第 90 - 94 行:在正式分配物理页帧之前将 FRAME_ALLOCATOR 初始化。

    • 第 92 行:这里我们使用 UPSafeCell<T> 来包裹栈式物理页帧分配器使得其具有内部可变性。每次对该分配器进行操作之前,我们都需要先通过 FRAME_ALLOCATOR.exclusive_access() 拿到分配器的可变借用。
  • 第 97 - 105 行:物理页号起始于ekernel并终止于MEMORY_END,通过这两个地址启动帧分配器。通过init方法初始化可用区间两端。我们调用物理地址 PhysAddrfloor/ceil 方法分别下/上取整获得可用的物理页号区间。

  • 第 108 - 113 行:公开给其他内核模块调用的分配物理页帧的接口,其返回类型并不是 FrameAllocator 要求的物理页号 PhysPageNum ,而是将其进一步包装为一个 FrameTracker

  • 第 116 - 118 行:公开给其他内核模块调用的释放物理页帧的接口。

物理页帧管理小结:物理页帧(物理页)的大小为4KiB,每个物理页都有其所对应的物理页号,对物理页的管理就是对物理页号的管理。物理页号从ekernel地址开始到MEMORY_END地址结束。物理页的总共数量就为这段地址以4KiB大小进行划分的物理页数量。对物理页的管理用到了三个变量,可以分配的物理页的起始页号,可以分配的物理页的最大页号,用完回收的物理页号,对物理页进行alloc先从用完回收的物理页号里分配没有再从可以分配的物理页的起始页号里分配,也就是获取一个物理页号,进行dealloc就是将传入的物理页号放入recycled(用完回收的物理页号)中。

实现 SV39 多级页表机制小结:现在我们已经实现了物理内存,物理内存以4KiB大小的物理页帧进行划分。我们也实现了页表,页表的大小和物理页帧的大小一样也为4KiB。每个页表都含有512个物理页表项,每个页表项8字节,页表项由44位物理页号和10位标志位组成。物理页号中存的是下一张页表的位置,从而迭代后可以找到最终的物理页。虚拟地址为39位,其第12位为页内偏移,剩下27位分成三级页表,每级9位。

在这里插入图片描述

【内核与应用的地址空间】

【实现地址空间抽象】

【逻辑段:一段连续地址的虚拟内存 -> os/src/mm/memory_set.rs】

逻辑段:我们以逻辑段 MapArea 为单位,描述一段连续地址的虚拟内存。就是指地址区间中的一段实际可用(即 MMU 通过查多级页表可以正确完成地址转换)的地址连续的虚拟地址区间,该区间内包含的所有虚拟页面都以一种相同的方式映射到物理页帧,具有可读/可写/可执行等属性。

//! Implementation of [`MapArea`] and [`MemorySet`].
// os/src/mm/memory_set.rs

use super::{frame_alloc, FrameTracker};
use super::{PTEFlags, PageTable, PageTableEntry};
use super::{PhysAddr, PhysPageNum, VirtAddr, VirtPageNum};
use super::{StepByOne, VPNRange};
use crate::config::{MEMORY_END, PAGE_SIZE, TRAMPOLINE, TRAP_CONTEXT, USER_STACK_SIZE};
use alloc::collections::BTreeMap;
use alloc::sync::Arc;
use alloc::vec::Vec;
use lazy_static::*;
use riscv::register::satp;
use spin::Mutex;

extern "C" {
    fn stext();
    fn etext();
    fn srodata();
    fn erodata();
    fn sdata();
    fn edata();
    fn sbss_with_stack();
    fn ebss();
    fn ekernel();
    fn strampoline();
}

lazy_static! {
    /// a memory set instance through lazy_static! managing kernel space
    pub static ref KERNEL_SPACE: Arc<Mutex<MemorySet>> =
        Arc::new(Mutex::new(MemorySet::new_kernel()));
}

/// memory set structure, controls virtual-memory space
pub struct MemorySet {
    page_table: PageTable,
    areas: Vec<MapArea>,
}

impl MemorySet {
    pub fn new_bare() -> Self {
        Self {
            page_table: PageTable::new(),
            areas: Vec::new(),
        }
    }
    pub fn token(&self) -> usize {
        self.page_table.token()
    }
    /// Assume that no conflicts.
    pub fn insert_framed_area(
        &mut self,
        start_va: VirtAddr,
        end_va: VirtAddr,
        permission: MapPermission,
    ) {
        self.push(
            MapArea::new(start_va, end_va, MapType::Framed, permission),
            None,
        );
    }
    fn push(&mut self, mut map_area: MapArea, data: Option<&[u8]>) {
        map_area.map(&mut self.page_table);
        if let Some(data) = data {
            map_area.copy_data(&mut self.page_table, data);
        }
        self.areas.push(map_area);
    }
    /// Mention that trampoline is not collected by areas.
    fn map_trampoline(&mut self) {
        self.page_table.map(
            VirtAddr::from(TRAMPOLINE).into(),
            PhysAddr::from(strampoline as usize).into(),
            PTEFlags::R | PTEFlags::X,
        );
    }
    /// Without kernel stacks.
    pub fn new_kernel() -> Self {
        let mut memory_set = Self::new_bare();
        // map trampoline
        memory_set.map_trampoline();
        // map kernel sections
        info!(".text [{:#x}, {:#x})", stext as usize, etext as usize);
        info!(".rodata [{:#x}, {:#x})", srodata as usize, erodata as usize);
        info!(".data [{:#x}, {:#x})", sdata as usize, edata as usize);
        info!(
            ".bss [{:#x}, {:#x})",
            sbss_with_stack as usize, ebss as usize
        );
        info!("mapping .text section");
        memory_set.push(
            MapArea::new(
                (stext as usize).into(),
                (etext as usize).into(),
                MapType::Identical,
                MapPermission::R | MapPermission::X,
            ),
            None,
        );
        info!("mapping .rodata section");
        memory_set.push(
            MapArea::new(
                (srodata as usize).into(),
                (erodata as usize).into(),
                MapType::Identical,
                MapPermission::R,
            ),
            None,
        );
        info!("mapping .data section");
        memory_set.push(
            MapArea::new(
                (sdata as usize).into(),
                (edata as usize).into(),
                MapType::Identical,
                MapPermission::R | MapPermission::W,
            ),
            None,
        );
        info!("mapping .bss section");
        memory_set.push(
            MapArea::new(
                (sbss_with_stack as usize).into(),
                (ebss as usize).into(),
                MapType::Identical,
                MapPermission::R | MapPermission::W,
            ),
            None,
        );
        info!("mapping physical memory");
        memory_set.push(
            MapArea::new(
                (ekernel as usize).into(),
                MEMORY_END.into(),
                MapType::Identical,
                MapPermission::R | MapPermission::W,
            ),
            None,
        );
        memory_set
    }
    /// Include sections in elf and trampoline and TrapContext and user stack,
    /// also returns user_sp and entry point.
    pub fn from_elf(elf_data: &[u8]) -> (Self, usize, usize) {
        let mut memory_set = Self::new_bare();
        // map trampoline
        memory_set.map_trampoline();
        // map program headers of elf, with U flag
        let elf = xmas_elf::ElfFile::new(elf_data).unwrap();
        let elf_header = elf.header;
        let magic = elf_header.pt1.magic;
        assert_eq!(magic, [0x7f, 0x45, 0x4c, 0x46], "invalid elf!");
        let ph_count = elf_header.pt2.ph_count();
        let mut max_end_vpn = VirtPageNum(0);
        for i in 0..ph_count {
            let ph = elf.program_header(i).unwrap();
            if ph.get_type().unwrap() == xmas_elf::program::Type::Load {
                let start_va: VirtAddr = (ph.virtual_addr() as usize).into();
                let end_va: VirtAddr = ((ph.virtual_addr() + ph.mem_size()) as usize).into();
                let mut map_perm = MapPermission::U;
                let ph_flags = ph.flags();
                if ph_flags.is_read() {
                    map_perm |= MapPermission::R;
                }
                if ph_flags.is_write() {
                    map_perm |= MapPermission::W;
                }
                if ph_flags.is_execute() {
                    map_perm |= MapPermission::X;
                }
                let map_area = MapArea::new(start_va, end_va, MapType::Framed, map_perm);
                max_end_vpn = map_area.vpn_range.get_end();
                memory_set.push(
                    map_area,
                    Some(&elf.input[ph.offset() as usize..(ph.offset() + ph.file_size()) as usize]),
                );
            }
        }
        // map user stack with U flags
        let max_end_va: VirtAddr = max_end_vpn.into();
        let mut user_stack_bottom: usize = max_end_va.into();
        // guard page
        user_stack_bottom += PAGE_SIZE;
        let user_stack_top = user_stack_bottom + USER_STACK_SIZE;
        memory_set.push(
            MapArea::new(
                user_stack_bottom.into(),
                user_stack_top.into(),
                MapType::Framed,
                MapPermission::R | MapPermission::W | MapPermission::U,
            ),
            None,
        );
        // map TrapContext
        memory_set.push(
            MapArea::new(
                TRAP_CONTEXT.into(),
                TRAMPOLINE.into(),
                MapType::Framed,
                MapPermission::R | MapPermission::W,
            ),
            None,
        );
        (
            memory_set,
            user_stack_top,
            elf.header.pt2.entry_point() as usize,
        )
    }
    pub fn activate(&self) {
        let satp = self.page_table.token();
        unsafe {
            satp::write(satp);
            core::arch::asm!("sfence.vma");
        }
    }
    pub fn translate(&self, vpn: VirtPageNum) -> Option<PageTableEntry> {
        self.page_table.translate(vpn)
    }
}

/// map area structure, controls a contiguous piece of virtual memory
pub struct MapArea {
    vpn_range: VPNRange,
    data_frames: BTreeMap<VirtPageNum, FrameTracker>,
    map_type: MapType,
    map_perm: MapPermission,
}

impl MapArea {
    pub fn new(
        start_va: VirtAddr,
        end_va: VirtAddr,
        map_type: MapType,
        map_perm: MapPermission,
    ) -> Self {
        let start_vpn: VirtPageNum = start_va.floor();
        let end_vpn: VirtPageNum = end_va.ceil();
        Self {
            vpn_range: VPNRange::new(start_vpn, end_vpn),
            data_frames: BTreeMap::new(),
            map_type,
            map_perm,
        }
    }
    pub fn map_one(&mut self, page_table: &mut PageTable, vpn: VirtPageNum) {
        let ppn: PhysPageNum;
        match self.map_type {
            MapType::Identical => {
                ppn = PhysPageNum(vpn.0);
            }
            MapType::Framed => {
                let frame = frame_alloc().unwrap();
                ppn = frame.ppn;
                self.data_frames.insert(vpn, frame);
            }
        }
        let pte_flags = PTEFlags::from_bits(self.map_perm.bits).unwrap();
        page_table.map(vpn, ppn, pte_flags);
    }
    #[allow(unused)]
    pub fn unmap_one(&mut self, page_table: &mut PageTable, vpn: VirtPageNum) {
        #[allow(clippy::single_match)]
        match self.map_type {
            MapType::Framed => {
                self.data_frames.remove(&vpn);
            }
            _ => {}
        }
        page_table.unmap(vpn);
    }
    pub fn map(&mut self, page_table: &mut PageTable) {
        for vpn in self.vpn_range {
            self.map_one(page_table, vpn);
        }
    }
    #[allow(unused)]
    pub fn unmap(&mut self, page_table: &mut PageTable) {
        for vpn in self.vpn_range {
            self.unmap_one(page_table, vpn);
        }
    }
    /// data: start-aligned but maybe with shorter length
    /// assume that all frames were cleared before
    pub fn copy_data(&mut self, page_table: &mut PageTable, data: &[u8]) {
        assert_eq!(self.map_type, MapType::Framed);
        let mut start: usize = 0;
        let mut current_vpn = self.vpn_range.get_start();
        let len = data.len();
        loop {
            let src = &data[start..len.min(start + PAGE_SIZE)];
            let dst = &mut page_table
                .translate(current_vpn)
                .unwrap()
                .ppn()
                .get_bytes_array()[..src.len()];
            dst.copy_from_slice(src);
            start += PAGE_SIZE;
            if start >= len {
                break;
            }
            current_vpn.step();
        }
    }
}

#[derive(Copy, Clone, PartialEq, Debug)]
/// map type for memory set: identical or framed
pub enum MapType {
    Identical,
    Framed,
}

bitflags! {
    /// map permission corresponding to that in pte: `R W X U`
    pub struct MapPermission: u8 {
        const R = 1 << 1;
        const W = 1 << 2;
        const X = 1 << 3;
        const U = 1 << 4;
    }
}

#[allow(unused)]
pub fn remap_test() {
    let mut kernel_space = KERNEL_SPACE.lock();
    let mid_text: VirtAddr = ((stext as usize + etext as usize) / 2).into();
    let mid_rodata: VirtAddr = ((srodata as usize + erodata as usize) / 2).into();
    let mid_data: VirtAddr = ((sdata as usize + edata as usize) / 2).into();
    assert!(!kernel_space
        .page_table
        .translate(mid_text.floor())
        .unwrap()
        .writable());
    assert!(!kernel_space
        .page_table
        .translate(mid_rodata.floor())
        .unwrap()
        .writable());
    assert!(!kernel_space
        .page_table
        .translate(mid_data.floor())
        .unwrap()
        .executable());
    info!("remap_test passed!");
}
  • 第 16 - 27 行:引入各个段的标识符。

  • 第 31 - 32 行:Arc<T>是用于共享所有权,而Mutex<T>是一个支持跨线程安全共享可变变量的容器。再此通过lazy方式创建了内核空间

  • 第 36 - 39 行:地址空间结构体的声明。

    • page_table:地址空间的多级页表PageTable 下挂着所有多级页表的节点所在的物理页帧。(包含该程序寻找物理地址所需的所有物理页帧)
    • areas:逻辑段 MapArea 的向量。MapArea 下则挂着对应逻辑段中的数据所在的物理页帧。
  • 第 41 - 221 行:为地址空间实现了许多的方法

    • 第 42 - 47 行:新建一个空的地址空间

    • 第 52 - 62 行:insert_framed_area 方法调用 push ,可以在当前地址空间插入一个 Framed 方式映射到物理内存的逻辑段。注意该方法的调用者要保证同一地址空间内的任意两个逻辑段不能存在交集。

    • 第 63 - 69 行:push 方法可以在当前地址空间插入一个新的逻辑段 map_area ,如果它是以 Framed 方式映射到物理内存,还可以可选地在那些被映射到的物理页帧上写入一些初始化数据 data

      • 第 64 行:在当前地址空间的多级页表下添加新的逻辑段的物理页。
      • 第 65 行:判断是否携带了初始的数据。
      • 第 68 行:将新添加的逻辑段插入到地址空间的areas中,表面注册成功。
    • 第 71 - 77 行:创建跳板机制,跳板所映射的物理地址也就是__alltraps所在的地址。所以每个应用各自地址空间的最高虚拟页面上存放的就是__alltraps

    • 第 79 - 142 行: new_kernel 将映射跳板和地址空间中最低 256GiB 中的内核逻辑段,new_kernel 可以生成内核的地址空间。在 new_kernel 中,我们从低地址到高地址依次创建 5 个逻辑段并通过 push 方法将它们插入到内核地址空间中,

      • 第 80 行:创建一个新的地址空间。
      • 第 82 行:映射跳板。
      • 第 92 -100 行:创建.text逻辑段并插入到地址空间中,具有可读和可执行的权限。
      • 第 102 -110 行:创建.rodata逻辑段并插入到地址空间中,具有可读的权限。
      • 第 112 -120 行:创建.data逻辑段并插入到地址空间中,具有可读和可写的权限。
      • 第 122 -130 行:创建.bss逻辑段并插入到地址空间中,具有可读和可写的权限。
      • 第 132 -140 行:创建物理内存区间,为ekernel到内存空间结束的大小,具有可读和可写的权限。
    • 第 145 - 210 行: from_elf 分析应用的 ELF 文件格式的内容,解析出各数据段并生成对应的地址空间。

      • 第 146 行:新建一个新的地址空间

      • 第 148 行:我们将跳板插入到应用地址空间

      • 第 150 行:我们使用外部 crate xmas_elf 来解析传入的应用 ELF 数据并可以轻松取出各个部分。

      • 第 151 行:取出elf文件的头部信息

      • 第 152 行:取出elf文件的magic部分

      • 第 153 行:判断magic是否是合法的elf文件

      • 第 154 行:我们可以直接得到 program header 的数目,然后遍历所有的 program header 并将合适的区域加入到应用地址空间中。

      • 第 155 行:给结束的虚拟页号增加个标记max_end_vpn

      • 第 157 行:获取elf文件的各个段。

      • 第 158 行:我们确认 program header 的类型是 LOAD ,这表明它有被内核加载的必要,此时不必理会其他类型的 program header

      • 第 159 - 160 行:通过 ph.virtual_addr()ph.mem_size() 来计算这一区域在应用地址空间中的位置。

      • 第 161 行:设置MapPermission 类型状态为U模式

      • 第 162 行:通过 ph.flags() 来确认这一区域访问方式的限制

      • 第 163 - 171 行:将获得的访问区域的限制转换为MapPermission 类型。(也就是在原本区域类型的限制上增加U模式)

      • 第 172 行:创建逻辑段 map_area

      • 第 173 行:刷新最大虚拟页号

      • 第 174 - 177 行:将新创建的逻辑段push 到应用地址空间。当前 program header 数据被存放的位置可以通过 ph.offset()ph.file_size() 来找到。

        第156 - 179主要是把一些内核需要加载的ELF段将其打包并加载进去,通过虚拟地址转换为物理地址组成逻辑段,再将这个逻辑段存入地址空间中。

      • 第 180 行:在此开始处理用户栈。注意在前面加载各个 program header 的时候,我们就已经维护了 max_end_vpn 记录目前涉及到的最大的虚拟页号,只需紧接着在它上面再放置一个保护页面和用户栈即可。

      • 第 181 行:将目前记录的最大的虚拟页号转换为虚拟地址

      • 第 182 - 184 行:将紧接着的虚拟地址加一张页大小后设置为栈底。

      • 第 185 行:栈顶为栈底加8K

      • 第 186 - 194 行:依据栈顶和栈底创建新的逻辑段并将其加入到地址空间中。

      • 第 196 - 204 行:则在应用地址空间中映射次高页面来存放 Trap 上下文。

      • 第 205 - 209 行:返回的时候,我们不仅返回应用地址空间 memory_set ,也同时返回用户栈虚拟地址 user_stack_top 以及从解析 ELF 得到的该应用入口点地址,它们将被我们用来创建应用的任务控制块。

      from_elf函数主要用来解析elf文件,它完全依据应用地址空间的布局进行虚拟地址的转换将所需的ELF文件内容保存到内存的物理地址上并将其转换为虚拟地址存入对应APP的虚拟地址空间。一个应用程序拥有一个地址空间,一个地址空间拥有好多个逻辑段,每一个ELF文件的段都被分成了一个相应的逻辑段。ELF逻辑段和用户栈之间具有1页的保留。之后是8K的用户栈。以上组成了用户地址空间的低地址。用户地址空间的高地址从上往下分别为跳板和陷入式上下文,两者分别对应相应的逻辑段。

      个人理解:解析ELF文件就是创建相应应用的地址空间,按照应用地址空间布局完善其内容。

      • 第 212 行:PageTable::token会按照 satp CSR 格式要求 构造一个无符号 64 位无符号整数使得其分页模式为 SV39 ,且将当前多级页表的根节点所在的物理页号填充进去

      • 第 214 行:我们将刚才构造的satp这个值写入当前 CPUsatp CSR从这一刻开始 SV39 分页模式就被启用了,而且 MMU使用内核地址空间的多级页表进行地址转换

      • 第 215 行:为了确保 MMU 的地址转换能够及时与 satp 的修改同步,我们需要立即使用 sfence.vma 指令将快表清空,这样 MMU 就不会看到快表中已经过期的键值对了。

  • 第 224 - 229 行:声明逻辑段的结构体MapArea

    • 第 225 行:VPNRange 描述一段虚拟页号的连续区间,表示该逻辑段在地址区间中的起始和终值位置。
    • 第 226 行:当逻辑段采用 MapType::Framed 方式映射到物理内存的时候, data_frames 是一个保存了该逻辑段内的每个虚拟页面和它被映射到的物理页帧 FrameTracker 的一个键值对容器 BTreeMap 中,这些物理页帧被用来存放实际内存数据而不是作为多级页表中的中间节点。将这些物理页帧的生命周期绑定到它所在的逻辑段 MapArea 下,当逻辑段被回收之后这些之前分配的物理页帧也会自动地同时被回收。
    • 第 227 行:MapType 描述该逻辑段内的所有虚拟页面映射到物理页帧的同一种方式,使用的是枚举的方式。
    • 第 228 行:MapPermission 表示控制该逻辑段的访问方式,它是页表项标志位 PTEFlags 的一个子集,仅保留 U/R/W/X 四个标志位,因为其他的标志位仅与硬件的地址转换机制细节相关,这样的设计能避免引入错误的标志位。
  • 第 231 - 306 行:为逻辑段MapArea实现了许多的方法

    • 第 232 - 237 行:new 方法可以新建一个逻辑段结构体,传入迭代器 vpn_range 中;
      • 第 238 行:将传入的起始地址下取整。
      • 第 239 行:将传入的结束地址上取整。
      • 第 241 行:将起始虚拟页号和终值虚拟页号传入迭代器 vpn_range 中,为当前逻辑段的页号范围。
    • 第 247 - 261 行:实现了单个虚拟页面进行映射。(就是给了个虚拟页和页表,将虚拟页对应到物理页上并写入页表中)
      • 第 250 行:选择进行恒等方式映射。
      • 第 251 行:当以恒等映射 Identical 方式映射的时候,物理页号就等于虚拟页号;
      • 第 253 行:选择进行随机方式映射。
      • 第 254 行:创建一个物理页。
      • 第 255 行:物理页号为对应新创建的物理页。
      • 第 256 行:将虚拟页与新创建的物理页组成映射值保存到逻辑段的data_frames中。
      • 第 259 行:获取控制段位值。
      • 第 260 行:通过虚拟页号边链接边创建物理页号,实现三级页表一级一级的内部map
    • 第 263 - 272 行:调用 PageTableunmap 接口删除以传入的虚拟页号为键的键值对即可。然而,当以 Framed 映射的时候,不要忘记同时将虚拟页面被映射到的物理页帧 FrameTrackerdata_frames 中移除,这样这个物理页帧才能立即被回收以备后续分配。
    • 第 273 - 277 行:将当前逻辑段到物理内存的映射从传入的该逻辑段所属的地址空间的多级页表中加入,也就是遍历逻辑段中的所有虚拟页面,并以每个虚拟页面为单位依次在多级页表中进行键值对的插入。
    • 第 279 - 283 行:将当前逻辑段到物理内存的映射从传入的该逻辑段所属的地址空间的多级页表中删除,也就是遍历逻辑段中的所有虚拟页面,并以每个虚拟页面为单位依次在多级页表中进行键值对的删除。
    • 第 286 - 305 行:copy_data 方法将切片 data 中的数据拷贝到当前逻辑段实际被内核放置在的各物理页帧上,从而在地址空间中通过该逻辑段就能访问这些数据。调用它的时候需要满足:切片 data 中的数据大小不超过当前逻辑段的总大小,且切片中的数据会被对齐到逻辑段的开头,然后逐页拷贝到实际的物理页帧。
      • 第 288 行:声明开始复制的data地址为0。
      • 第 289 行:声明开始虚拟地址号为虚拟地址范围的第一个。
      • 第 290 行:声明需要拷贝的data总长度。
      • 第 292 行:获取源本次拷贝的src切片。
      • 第 293 行:获取拷贝的目标物理内存的切片。从传入的当前逻辑段所属的地址空间的多级页表中,手动查找迭代到的虚拟页号被映射到的物理页帧,并通过 get_bytes_array 方法获取该物理页帧的字节数组型可变引用,最后再获取它的切片用于数据拷贝。
      • 第 298 行:直接使用 copy_from_slice 完成复制。
      • 第 299 行:下一次开始复制的data地址加一页的大小。
      • 第 300 行:判断拷贝是否完成。
      • 第 303 行:切换到下一页。
  • 第 310 - 313 行:声明枚举映射的类型,Identical 表示恒等映射方式;而 Framed 则表示对于每个虚拟页面都有一个新分配的物理页帧与之对应,虚地址与物理地址的映射关系是相对随机的。

  • 第 315 - 323 行:声明MapPermission结构体使得可以对每一位进行操作。

  • 第 326 - 346 行:通过手动查找内核多级页表的方式验证代码段和只读数据段不允许被写入,同时不允许从数据段上取指执行。

【地址空间:一系列有关联的逻辑段】

地址空间 是一系列有关联的不一定连续的逻辑段,这种关联一般是指这些逻辑段组成的虚拟内存空间与一个运行的程序(目前把一个运行的程序称为任务,后续会称为进程)绑定,即这个运行的程序对代码和数据的直接访问范围限制在它关联的虚拟地址空间之内。这样我们就有任务的地址空间,内核的地址空间等说法了。地址空间使用 MemorySet 类型来表示。

通过地址空间可以区分不同的应用,因为他们最终所映射到的物理页是不同的。地址空间是最上面的集合,每个地址空间下还有许多的逻辑段,逻辑段是地址空间的最小单位。地址空间只需要管两部分,它所含有的逻辑段有哪些和它所具有的物理地址有哪些。逻辑段则是真正操作的部分。

【内核地址空间】

启用分页模式下,内核代码的访存地址也会被视为一个虚拟地址并需要经过 MMU 的地址转换,因此我们也需要为内核对应构造一个地址空间,它除了仍然需要允许内核的各数据段能够被正常访问之后,还需要包含所有应用的内核栈以及一个 跳板 (Trampoline) 。

如图所示为内核地址256GiB的内存布局

../_images/kernel-as-high.png

跳板放在最高的一个虚拟页面中。接下来则是从高到低放置每个应用的内核栈,内核栈的大小由 config 子模块的 KERNEL_STACK_SIZE 给出。它们的映射方式为 MapPermission 中的 rw 两个标志位,意味着这个逻辑段仅允许 CPU 处于内核态访问,且只能读或写。

相邻两个内核栈之间会预留一个 保护页面 (Guard Page) ,它是内核地址空间中的空洞,多级页表中并不存在与它相关的映射。它的意义在于当内核栈空间不足(如调用层数过多或死递归)的时候,代码会尝试访问空洞区域内的虚拟地址,然而它无法在多级页表中找到映射,便会触发异常,此时控制权会交给内核 trap handler 函数进行异常处理。由于我们的内核非常简单且内核栈的大小设置比较宽裕,在当前的设计中我们仅将空洞区域的大小设置为单个页面。

如图所示为内核地址256GiB的内存布局

../_images/kernel-as-low.png

四个逻辑段 .text/.rodata/.data/.bss 被恒等映射到物理内存,这使得我们在无需调整内核内存布局 os/src/linker.ld 的情况下就仍能象启用页表机制之前那样访问内核的各个段。

  • 四个逻辑段的 U 标志位均未被设置,使得 CPU 只能在处于 S 特权级(或以上)时访问它们;
  • 代码段 .text 不允许被修改;
  • 只读数据段 .rodata 不允许被修改,也不允许从它上面取指执行;
  • .data/.bss 均允许被读写,但是不允许从它上面取指执行。

内核地址空间中将各个段分别映射为一个逻辑段并添加到对应的内核地址空间中。

【应用地址空间 -> user/src/linker.ld -> os/src/build.rs -> os/src/loader.rs】

在第三章中,每个应用链接脚本中的起始地址被要求是不同的,这样它们的代码和数据存放的位置才不会产生冲突。现在,借助地址空间的抽象,我们终于可以让所有应用程序都使用同样的起始地址,这也意味着所有应用可以使用同一个链接脚本:

/* user/src/linker.ld */

OUTPUT_ARCH(riscv)
ENTRY(_start)

BASE_ADDRESS = 0x10000;

SECTIONS
{
    . = BASE_ADDRESS;
    .text : {
        *(.text.entry)
        *(.text .text.*)
    }
    . = ALIGN(4K);
    .rodata : {
        *(.rodata .rodata.*)
        *(.srodata .srodata.*)
    }
    . = ALIGN(4K);
    .data : {
        *(.data .data.*)
        *(.sdata .sdata.*)
    }
    .bss : {
        start_bss = .;
        *(.bss .bss.*)
        *(.sbss .sbss.*)
        end_bss = .;
    }
    /DISCARD/ : {
        *(.eh_frame)
        *(.debug*)
    }
}

我们将起始地址 BASE_ADDRESS 设置为 0x10000 (我们这里并不设置为 0x0 ,因为它一般代表空指针),显然它只能是一个地址空间中的虚拟地址而非物理地址。我们将入口汇编代码段放在最低的地方,这也是整个应用的入口点,而无需像之前一样将其硬编码到代码中。在 .text.rodata 中间以及 .rodata.data 中间我们进行了页面对齐,因为前后两个逻辑段的访问方式限制是不同的,由于我们只能以页为单位对这个限制进行设置,因此就只能将下一个逻辑段对齐到下一个页面开始放置(不然的化在同一页访问方式都是一样的)。而 .data.bss 两个逻辑段由于访问限制相同(可读写),它们中间则无需进行页面对齐。如图所示为应用地址空间的内存布局

在这里插入图片描述

从 0x10000 开始向高地址放置应用内存布局中的各个逻辑段,最后放置带有一个保护页面的用户栈。这些逻辑段都是以 Framed 方式映射到物理内存的,从访问方式上来说都加上了 U 标志位代表 CPU 可以在 U 特权级也就是执行应用代码的时候访问它们。右侧则给出了最高的 256GiB ,可以看出它只是和内核地址空间一样将跳板放置在最高页,还将 Trap 上下文放置在次高页中。这两个虚拟页面虽然位于应用地址空间,但是它们并不包含 U 标志位,事实上它们在地址空间切换的时候才会发挥作用。

os/src/build.rs 中,我们不再将丢弃了所有符号的应用二进制镜像链接进内核,因为在应用二进制镜像中,内存布局中各个逻辑段的置和访问限制等信息都被裁剪掉了。我们直接使用保存了逻辑段信息的 ELF 格式的应用可执行文件

//! Building applications linker
// os/src/build.rs
use std::fs::{read_dir, File};
use std::io::{Result, Write};

fn main() {
    println!("cargo:rerun-if-changed=../user/src/");
    println!("cargo:rerun-if-changed={}", TARGET_PATH);
    insert_app_data().unwrap();
}

static TARGET_PATH: &str = "../user/build/elf/";

/// get app data and build linker
fn insert_app_data() -> Result<()> {
    let mut f = File::create("src/link_app.S").unwrap();
    let mut apps: Vec<_> = read_dir("../user/build/elf/")
        .unwrap()
        .into_iter()
        .map(|dir_entry| {
            let mut name_with_ext = dir_entry.unwrap().file_name().into_string().unwrap();
            name_with_ext.drain(name_with_ext.find('.').unwrap()..name_with_ext.len());
            name_with_ext
        })
        .collect();
    apps.sort();

    writeln!(
        f,
        r#"
    .align 3
    .section .data
    .global _num_app
_num_app:
    .quad {}"#,
        apps.len()
    )?;

    for i in 0..apps.len() {
        writeln!(f, r#"    .quad app_{}_start"#, i)?;
    }
    writeln!(f, r#"    .quad app_{}_end"#, apps.len() - 1)?;

    for (idx, app) in apps.iter().enumerate() {
        println!("app_{}: {}", idx, app);
        writeln!(
            f,
            r#"
    .section .data
    .global app_{0}_start
    .global app_{0}_end
    .align 3
app_{0}_start:
    .incbin "{2}{1}.elf"
app_{0}_end:"#,
            idx, app, TARGET_PATH
        )?;
    }
    Ok(())
}
  • 第 12 行:指明了ELF格式文件的目标地址为../user/build/elf/

  • 第 16 行:创建链接文件link_app.S

  • 第 17 行:获取elf文件的迭代器。

  • 第 21 行:获取elf文件的名称

  • 第 22 行:获取整个elf文件的内容,因为elf文件section都是以.为开头的,所以找到第一个.就找到了开头,再给个整体elf长度便有了对应应用的整个ELF文件长度。(ELF文件获取需要两个参数,第一个.的起始位置和整个ELF文件的大小

    这里写图片描述

  • 第 26 行:对apps进行排序

  • 第 28 - 37 行:在link_app.S中添加开头信息。在链接每个 ELF 执行文件之前我们都加入一行 .align 3 来确保它们对齐到 8 字节,这是由于如果不这样做, xmas-elf crate 可能会在解析 ELF 的时候进行不对齐的内存读写,例如使用 ld 指令从内存的一个没有对齐到 8 字节的地址加载一个 64 位的值到一个通用寄存器。

    在这里插入图片描述

  • 第 40 行:添加各个应用的起始名称,.quad表面为8个字节

    r#" 字符串 "#为添加字符串,中间的字符串不需要进行字符转换。判定的依据为#"#",例如:r##"foo #"# bar"##表示字符串foo #"# bar。文字不会在中间的引号处停止,因为它后面只有一个#,而文字是以两个#开始的。

  • 第 42 行:添加结束位。

  • 第 44 行:在迭代过程中添加计数和元素的迭代器。

  • 第 39 - 58 行:以固定的格式生成每个应用的段信息。

    在这里插入图片描述

os/src/build.rs文件只生成了对应应用APP的链接脚本,链接的源为user/build/elf文件,链接各个应用的ELF可执行文件。最终生成link_app.S,系统就可以通过这个汇编文件进行加载链接好的应用了。

可以理解为通过这个文件生成的link_app.S就指明了各个APP的文件地址。

os/src/loader.rs它仅需要提供两个函数: get_num_app 获取链接到内核内的应用的数目,而 get_app_data 则根据传入的应用编号取出对应应用的 ELF 格式可执行文件数据。

//! Loading user applications into memory
// os/src/loader.rs
/// Get the total number of applications.
pub fn get_num_app() -> usize {
    extern "C" {
        fn _num_app();
    }
    unsafe { (_num_app as usize as *const usize).read_volatile() }
}

/// get applications data
pub fn get_app_data(app_id: usize) -> &'static [u8] {
    extern "C" {
        fn _num_app();
    }
    let num_app_ptr = _num_app as usize as *const usize;
    let num_app = get_num_app();
    let app_start = unsafe { core::slice::from_raw_parts(num_app_ptr.add(1), num_app + 1) };
    assert!(app_id < num_app);
    unsafe {
        core::slice::from_raw_parts(
            app_start[app_id] as *const u8,
            app_start[app_id + 1] - app_start[app_id],
        )
    }
}
  • 第 4 -9 行:获取link_app.S文件中从_num_app这个标识符的第一行.quad 12,也就是应用数量。
  • 第 4 -9 行:获取对应应用的数据
    • 第 16 行:获取_num_app也就是应用起始地址。
    • 第 17 行:获取总体应用数量。
    • 第 18 行:获取所有应用的切片
    • 第 20 - 24 行:获取(需要的应用切片位置开始,下一个应用开始)位置之间的地址。也就是代码段。

os/src/loader.rs获取对应APPELF文件。

在创建应用地址空间的时候,我们需要get_app_data 得到的 ELF 格式数据进行解析,找到各个逻辑段所在位置访问限制并插入进来,最终得到一个完整的应用地址空间

【基于地址空间的分时多任务】

【建立并开启基于分页模式的虚拟地址空间】

当 SBI 实现初始化完成后, CPU 将跳转到内核入口点并在 S 特权级上执行,此时还并没有开启分页模式,内核的每次访存是直接的物理内存访问。而在开启分页模式之后,内核代码在访存时只能看到内核地址空间,此时每次访存需要通过 MMU 的地址转换。这两种模式之间的过渡在内核初始化期间完成

【创建内核地址空间】

rust_main 函数中,我们首先调用 mm::init 进行内存管理子系统的初始化

//! Memory management implementation
//! 
//! SV39 page-based virtual-memory architecture for RV64 systems, and
//! everything about memory management, like frame allocator, page table,
//! map area and memory set, is implemented here.
//! 
//! Every task or process has a memory_set to control its virtual memory.


mod address;
mod frame_allocator;
mod heap_allocator;
mod memory_set;
mod page_table;

pub use address::{PhysAddr, PhysPageNum, VirtAddr, VirtPageNum};
use address::{StepByOne, VPNRange};
pub use frame_allocator::{frame_alloc, FrameTracker};
pub use memory_set::remap_test;
pub use memory_set::{MapPermission, MemorySet, KERNEL_SPACE};
pub use page_table::{translated_byte_buffer, PageTableEntry};
use page_table::{PTEFlags, PageTable};

/// initiate heap allocator, frame allocator and kernel space
pub fn init() {
    heap_allocator::init_heap();
    frame_allocator::init_frame_allocator();
    KERNEL_SPACE.lock().activate();
}
  • 第 26 行:最先进行了全局动态内存分配器的初始化,因为接下来马上就要用到 Rust 的堆数据结构。
  • 第 27 行:接下来我们初始化物理页帧管理器(内含堆数据结构 Vec<T>使能可用物理页帧的分配和回收能力
  • 第 28 行:最后我们创建内核地址空间让 CPU 开启分页模式, MMU 在地址转换的时候使用内核的多级页表
    • 首先,我们引用 KERNEL_SPACE ,这是它第一次被使用,就在此时它会被初始化,调用 MemorySet::new_kernel 创建一个内核地址空间并使用 Arc<Mutex<T>> 包裹起来;
    • 接着使用 .exclusive_access() 获取一个可变引用 &mut MemorySet 。需要注意的是这里发生了两次隐式类型转换:
      1. 我们知道 exclusive_accessUPSafeCell<T> 的方法而不是 Arc<T> 的方法,由于 Arc<T> 实现了 Deref Trait ,当 exclusive_access 需要一个 &UPSafeCell<T> 类型的参数的时候,编译器会自动将传入的 Arc<UPSafeCell<T>> 转换为 &UPSafeCell<T> 这样就实现了类型匹配;
      2. 事实上 UPSafeCell<T>::exclusive_access 返回的是一个 RefMut<'_, T> ,这同样是 RAII 的思想,当这个类型生命周期结束后互斥锁就会被释放。而该类型实现了 DerefMut Trait,因此当一个函数接受类型为 &mut T 的参数却被传入一个类型为 &mut RefMut<'_, T> 的参数的时候,编译器会自动进行类型转换使参数匹配。
    • 最后,我们调用 MemorySet::activate 来设置CSRsatp 寄存器来开启MMU。在这之后 S 和 U 特权级的访存地址会被视为一个虚拟地址,它需要经过 MMU 的地址转换变为一个物理地址,再通过它来访问物理内存
      • 我们必须注意切换 satp CSR 是否是一个 平滑 的过渡:由于切换前我们使用的是物理地址直接取指的方式,而切换后我们使用的是MMU查找多级页表的方式(由于切换 satp 的指令并不是一条跳转指令, pc 只是简单的自增当前指令的字长)。这就要求前后两个地址空间在切换 satp 的指令 附近 的映射满足某种意义上的连续性。
      • 幸运的是,我们做到了这一点。这条写入 satp 的指令及其下一条指令都在内核内存布局的代码段中,在切换之后是一个恒等映射,而在切换之前是视为物理地址直接取指,也可以将其看成一个恒等映射。这完全符合我们的期待:即使切换了地址空间,指令仍应该能够被连续的执行。
【检查内核地址空间的多级页表设置】

调用 mm::init 之后我们就使能了内核动态内存分配、物理页帧管理,还启用了分页模式进入了内核地址空间。之后我们可以通过 mm::remap_test检查内核地址空间的多级页表是否被正确设置。也就是验证各个段相应的权限能力(查看标志位),类似于可读可写可执行。

【跳板机制的实现】

第二章介绍过的 Trap 上下文保存与恢复:当一个应用 Trap 到内核时,sscratch 已指向该应用的内核栈栈顶,我们用一条指令即可从用户栈切换到内核栈,然后直接将 Trap 上下文压入内核栈栈顶。当 Trap 处理完毕返回用户态的时候,将 Trap 上下文中的内容恢复到寄存器上,最后将保存着应用用户栈顶的 sscratchsp 进行交换,也就从内核栈切换回了用户栈。

一旦使能了分页机制,一切就并没有这么简单了:我们必须在这个过程中同时完成地址空间的切换。具体来说,当 __alltraps 保存 Trap 上下文的时候,我们必须通过修改 satp 从应用地址空间切换到内核地址空间,因为 trap handler 只有在内核地址空间中才能访问;同理,在 __restore 恢复 Trap 上下文的时候,我们也必须从内核地址空间切换回应用地址空间,因为应用的代码和数据只能在它自己的地址空间中才能访问,应用是看不到内核地址空间的。

我们为何将应用的 Trap 上下文放到应用地址空间的次高页面而不是内核地址空间中的内核栈中呢?

原因在于,在保存 Trap 上下文到内核栈中之前,我们必须完成两项工作:

1)必须先切换到内核地址空间,这就需要将内核地址空间的 token 写入 satp 寄存器;

2)之后还需要保存应用的内核栈栈顶的位置,这样才能以它为基址保存 Trap 上下文。

这两步需要用寄存器作为临时周转然而我们无法在不破坏任何一个通用寄存器的情况下做到这一点。因为事实上我们需要用到内核的两条信息:内核地址空间的 token ,以及应用的内核栈栈顶的位置,RISC-V却只提供一个 sscratch 寄存器可用来进行周转。所以,我们不得不将 Trap 上下文保存在应用地址空间的一个虚拟页面中,而不是切换到内核地址空间去保存。

也就是说之所以将Trap上下文保存到应用地址空间是因为没有多余的寄存器供我们指向内核地址空间的应用栈的地址让我们保存Trap上下文到这里。

【扩展Trap 上下文 -> os/src/trap/context.rs】
//! Implementation of [`TrapContext`]
// os/src/trap/context.rs
use riscv::register::sstatus::{self, Sstatus, SPP};

#[repr(C)]
/// trap context structure containing sstatus, sepc and registers
pub struct TrapContext {
    pub x: [usize; 32],
    pub sstatus: Sstatus,
    pub sepc: usize,
    pub kernel_satp: usize,
    pub kernel_sp: usize,
    pub trap_handler: usize,
}

impl TrapContext {
    pub fn set_sp(&mut self, sp: usize) {
        self.x[2] = sp;
    }
    pub fn app_init_context(
        entry: usize,
        sp: usize,
        kernel_satp: usize,
        kernel_sp: usize,
        trap_handler: usize,
    ) -> Self {
        let mut sstatus = sstatus::read();
        sstatus.set_spp(SPP::User);
        let mut cx = Self {
            x: [0; 32],
            sstatus,
            sepc: entry,
            kernel_satp,
            kernel_sp,
            trap_handler,
        };
        cx.set_sp(sp);
        cx
    }
}
  • 第 11 行:kernel_satp 表示内核地址空间的 token ,即内核页表的起始物理地址;

  • 第 12 行:kernel_sp 表示当前应用在内核地址空间中的内核栈栈顶的虚拟地址

  • 第 13 行:trap_handler 表示内核中 trap handler 入口点的虚拟地址。

    它们在应用初始化的时候由内核写入应用地址空间中的 TrapContext 的相应位置,此后就不再被修改。

  • 第 20 - 39 行:初始化TrapContext结构体。

【切换地址空间 -> os/src/trap/trap.S】

__alltraps__restore 在保存和恢复 Trap 上下文的同时也切换地址空间:

# os/src/trap/trap.S
.altmacro
.macro SAVE_GP n
    sd x\n, \n*8(sp)
.endm
.macro LOAD_GP n
    ld x\n, \n*8(sp)
.endm
    .section .text.trampoline
    .globl __alltraps
    .globl __restore
    .align 2
__alltraps:
    csrrw sp, sscratch, sp
    # now sp->*TrapContext in user space, sscratch->user stack
    # save other general purpose registers
    sd x1, 1*8(sp)
    # skip sp(x2), we will save it later
    sd x3, 3*8(sp)
    # skip tp(x4), application does not use it
    # save x5~x31
    .set n, 5
    .rept 27
        SAVE_GP %n
        .set n, n+1
    .endr
    # we can use t0/t1/t2 freely, because they have been saved in TrapContext
    csrr t0, sstatus
    csrr t1, sepc
    sd t0, 32*8(sp)
    sd t1, 33*8(sp)
    # read user stack from sscratch and save it in TrapContext
    csrr t2, sscratch
    sd t2, 2*8(sp)
    # load kernel_satp into t0
    ld t0, 34*8(sp)
    # load trap_handler into t1
    ld t1, 36*8(sp)
    # move to kernel_sp
    ld sp, 35*8(sp)
    # switch to kernel space
    csrw satp, t0
    sfence.vma
    # jump to trap_handler
    jr t1

__restore:
    # a0: *TrapContext in user space(Constant); a1: user space token
    # switch to user space
    csrw satp, a1
    sfence.vma
    csrw sscratch, a0
    mv sp, a0
    # now sp points to TrapContext in user space, start restoring based on it
    # restore sstatus/sepc
    ld t0, 32*8(sp)
    ld t1, 33*8(sp)
    csrw sstatus, t0
    csrw sepc, t1
    # restore general purpose registers except x0/sp/tp
    ld x1, 1*8(sp)
    ld x3, 3*8(sp)
    .set n, 5
    .rept 27
        LOAD_GP %n
        .set n, n+1
    .endr
    # back to user stack
    ld sp, 2*8(sp)
    sret
  • 第 9 行:将trap.S放置在.text.trampolinesection中。
  • 第 10 行:将__alltraps设为该段的开头。
  • 第 11 行:紧接着跟着__restore
  • 第 12 行:设置对其方式。
  • 第 13 行:开始进入保存Trap部分。
  • 第 14 行:刚开时 sp 寄存器仍指向用户栈,但 sscratch 则被设置为指向应用地址空间中存放 Trap 上下文的位置(实际在次高页面)。随后,就像之前一样,我们 csrrw 交换 spsscratch
  • 第 15 - 34 行:基于指向 Trap 上下文位置的 sp 开始保存通用寄存器和一些 CSR。也就是在用户地址空间保存了Trap上下文。
  • 第 36 行:将内核地址空间的 token 载入到 t0 寄存器中。
  • 第 38 行:将 trap handler 入口点的虚拟地址载入到 t1 寄存器中。
  • 第 40 行:直接将 sp 修改为应用内核栈顶的地址。
  • 第 42 - 43 行:将 satp 修改为内核地址空间的 token 并使用 sfence.vma 刷新快表,这就切换到了内核地址空间。
  • 第 45 行: 最后通过 jr 指令跳转到 t1 寄存器所保存的trap handler 入口点的地址。
  • 第 47 行:内核将 Trap 处理完毕准备返回用户态的时候会 调用 __restore (符合RISC-V函数调用规范),它有两个参数第一个Trap 上下文在应用地址空间中的位置,这个对于所有的应用来说都是相同的,在 a0 寄存器中传递;第二个则是即将回到的应用的地址空间的 token ,在 a1 寄存器中传递。
  • 第 50 - 51 行:切换到应用的地址空间并刷新快表。因为Trap上下文是保存在应用地址空间中的。
  • 第 52 行:将传入的 Trap 上下文位置保存在 sscratch 寄存器中,这样 __alltraps 中才能基于它将 Trap 上下文保存到正确的位置。
  • 第 53 行: sp 修改为 Trap 上下文的位置。
  • 第 54 - 69 行:恢复各通用寄存器和 CSR
  • 第 70 行:通过 sret 指令返回用户态。

用户态陷入到内核态的时候首先保存通用寄存器和一些 CSR到当前应用的地址空间中。之后通过TrapContext 最后三位成员切换到内核态地址空间,切换到内核栈,最后执行处理函数。

内核态返回到用户态的时候首先切换到用户空间,之后依据用户空间的TrapContext 恢复通用寄存器和CSR寄存器。然后执行sret返回用户态。

【建立跳板页面 - > os/src/linker.ld】
# os/src/linker.ld

OUTPUT_ARCH(riscv)
ENTRY(_start)
BASE_ADDRESS = 0x80200000;

SECTIONS
{
    . = BASE_ADDRESS;
    skernel = .;

    stext = .;
    .text : {
        *(.text.entry)
        . = ALIGN(4K);
        strampoline = .;
        *(.text.trampoline);
        . = ALIGN(4K);
        *(.text .text.*)
    }

    . = ALIGN(4K);
    etext = .;
    srodata = .;
    .rodata : {
        *(.rodata .rodata.*)
        *(.srodata .srodata.*)
    }

    . = ALIGN(4K);
    erodata = .;
    sdata = .;
    .data : {
        *(.data .data.*)
        *(.sdata .sdata.*)
    }

    . = ALIGN(4K);
    edata = .;
    sbss_with_stack = .;
    .bss : {
        *(.bss.stack)
        sbss = .;
        *(.bss .bss.*)
        *(.sbss .sbss.*)
    }

    . = ALIGN(4K);
    ebss = .;
    ekernel = .;

    /DISCARD/ : {
        *(.eh_frame)
    }
}
  • 第 3 行:我们设置了目标平台为 riscv ;
  • 第 4 行:我们设置了整个程序的入口点为之前定义的全局符号 _start
  • 第 5 行:定义了一个常量 BASE_ADDRESS0x80200000 ,为OS的起始地址。
  • 第 7 行:SECTIONS命令告诉链接器如何将输入部分映射到输出部分,以及如何将输出部分放置在内存中。
  • 第 9 行:.为定位器符号,从BASE_ADDRESS开始设置内存。
  • 第 10 行:设置内核的起始地址为BASE_ADDRESS
  • 第 12 行:设置开始代码段
  • 第 13 行:开始表面.text段到底有哪些
  • 第 14 行:将 _start 所在的 .text.entry 放在整个程序的开头; 系统在初始化后跳转到 0x80200000,就进入了用户库的 _start 函数;
  • 第 15 行:表示以4K个字节对其。也就是说明下边界现在到了BASE_ADDRESS+4K的大小
  • 第 16 行:将下一个物理页的地址被外部符号 strampoline 标记,也就是__alltraps的位置。
  • 第 17 行:将.text.trampoline在调整内存布局的时候将它对齐到代码段的一个页面中。由于__alltraps位于.text.trampoline的开头部分,所以__alltraps 恰好位于这个物理页帧的开头。
  • 第 18 行:表示以4K个字节对其。
  • 第 19 行:将剩余所有.text代码段进行匹配到这块内存。
  • 第 22 行:表示以4K个字节对其。
  • 第 23 行:给出.text段的结束部分。
  • 第 24 - 49 行:将剩余段落分别进行匹配。
  • 第 50 行:给出内核结束地址的标志。

跳板页面:在开启分页模式之后,内核和应用代码都只能看到各自的虚拟地址空间,而在它们的视角中,这段汇编代码都被放在它们各自地址空间的最高虚拟页面上,由于这段汇编代码在执行的时候涉及到地址空间切换,故而被称为跳板页面。

个人理解:在应用空间中可以通过跳板机制进行地址空间的切换,切换到内核空间。应用地址空间存放的跳板页面转换为物理地址就是__alltraps开始的物理页。通过跳板可以使得内核用户空间和应用用户空间都能访问到这一段汇编代码。

【加载和执行应用程序】

【扩展任务控制块 -> os/src/task/task.rs】

os/src/task/task.rs主要包含对任务进行控制的相应的操作。还有TCB的结构等。

//! Types related to task management
// os/src/task/task.rs
use super::TaskContext;
use crate::config::{kernel_stack_position, TRAP_CONTEXT};
use crate::mm::{MapPermission, MemorySet, PhysPageNum, VirtAddr, KERNEL_SPACE};
use crate::trap::{trap_handler, TrapContext};

/// task control block structure
pub struct TaskControlBlock {
    pub task_status: TaskStatus,
    pub task_cx: TaskContext,
    pub memory_set: MemorySet,
    pub trap_cx_ppn: PhysPageNum,
    pub base_size: usize,
}

impl TaskControlBlock {
    pub fn get_trap_cx(&self) -> &'static mut TrapContext {
        self.trap_cx_ppn.get_mut()
    }
    pub fn get_user_token(&self) -> usize {
        self.memory_set.token()
    }
    pub fn new(elf_data: &[u8], app_id: usize) -> Self {
        // memory_set with elf program headers/trampoline/trap context/user stack
        let (memory_set, user_sp, entry_point) = MemorySet::from_elf(elf_data);
        let trap_cx_ppn = memory_set
            .translate(VirtAddr::from(TRAP_CONTEXT).into())
            .unwrap()
            .ppn();
        let task_status = TaskStatus::Ready;
        // map a kernel-stack in kernel space
        let (kernel_stack_bottom, kernel_stack_top) = kernel_stack_position(app_id);
        KERNEL_SPACE.lock().insert_framed_area(
            kernel_stack_bottom.into(),
            kernel_stack_top.into(),
            MapPermission::R | MapPermission::W,
        );
        let task_control_block = Self {
            task_status,
            task_cx: TaskContext::goto_trap_return(kernel_stack_top),
            memory_set,
            trap_cx_ppn,
            base_size: user_sp,
        };
        // prepare TrapContext in user space
        let trap_cx = task_control_block.get_trap_cx();
        *trap_cx = TrapContext::app_init_context(
            entry_point,
            user_sp,
            KERNEL_SPACE.lock().token(),
            kernel_stack_top,
            trap_handler as usize,
        );
        task_control_block
    }
}

#[derive(Copy, Clone, PartialEq)]
/// task status: UnInit, Ready, Running, Exited
pub enum TaskStatus {
    UnInit,
    Ready,
    Running,
    Exited,
}
  • 第 12 行: memory_set为应用的地址空间。

  • 第 13 行:trap_cx_ppn为应用地址空间次高页的 Trap 上下文被实际存放在物理页帧的物理页号

  • 第 14 行:base_size 统计了应用数据的大小,也就是在应用地址空间中从 0x0 开始到用户栈结束一共包含多少字节,也就是低页面。

  • 第 18 - 20 行:从次高页对应的物理页号上获取TrapContext 。返回 'static 的可变引用和之前一样可以看成一个绕过 unsafe 的裸指针;而 PhysPageNum::get_mut 是一个泛型函数,由于我们已经声明了总体返回 TrapContext 的可变引用,则Rust编译器会给 get_mut 泛型函数针对具体类型 TrapContext 的情况生成一个特定版本的 get_mut 函数实现。在 get_trap_cx 函数中则会静态调用get_mut 泛型函数的特定版本实现。

  • 第 21 - 23 行:获取对应应用的token,可以依据此进行地址空间的切换。

  • 第 24 - 56 行:创建一个新的TCB

    • 第 26 行:解析传入的 ELF 格式数据构造应用的地址空间 memory_set 并获得其他信息。
    • 第 27 - 30 行:从地址空间 memory_set 中查多级页表找到应用地址空间中的 Trap 上下文实际被放在哪个物理页帧。
    • 第 31 行:创建任务的状态为Ready
    • 第 33 行:根据传入的应用 ID app_id 调用在 config 子模块中定义的 kernel_stack_position 找到应用的内核栈预计放在内核地址空间 KERNEL_SPACE 中的哪个位置
    • 第 34 - 38 行:通过 insert_framed_area 实际将这个应用内核栈逻辑段加入到内核地址空间中
    • 第 39 - 41 行:在应用的内核栈顶压入一个跳转到 trap_return 而不是 __restore 的任务上下文,这主要是为了能够支持对该应用的启动并顺利切换到用户地址空间执行。
    • 第 42 - 46 行:用上面的信息来创建并返回任务控制块实例 task_control_block
    • 第 47 行:查找该应用的 Trap 上下文的内核虚地址。
    • 第 48 - 54 行:调用 TrapContext::app_init_context 函数,通过应用的 Trap 上下文的可变引用来对其进行初始化。

    TCB初始化的过程:首先依据应用的ELF创建应用的地址空间,在内核地址空间中添加应用的内核逻辑栈,创建TCB,对trap上下文进行初始化。

【更新对任务控制块的管理 -> os/src/task/mod.rs】

在内核初始化的时候,需要将所有的应用加载到全局应用管理器中,os/src/task/mod.rs负责与应用管理,应用调度有关的事情:

// os/src/task/mod.rs
//! Task management implementation
//!
//! Everything about task management, like starting and switching tasks is
//! implemented here.
//!
//! A single global instance of [`TaskManager`] called `TASK_MANAGER` controls
//! all the tasks in the operating system.
//!
//! Be careful when you see [`__switch`]. Control flow around this function
//! might not be what you expect.

mod context;
mod switch;
#[allow(clippy::module_inception)]
mod task;

use crate::loader::{get_app_data, get_num_app};
use crate::sync::UPSafeCell;
use crate::trap::TrapContext;
use alloc::vec::Vec;
use lazy_static::*;
pub use switch::__switch;
pub use task::{TaskControlBlock, TaskStatus};

pub use context::TaskContext;

/// The task manager, where all the tasks are managed.
///
/// Functions implemented on `TaskManager` deals with all task state transitions
/// and task context switching. For convenience, you can find wrappers around it
/// in the module level.
///
/// Most of `TaskManager` are hidden behind the field `inner`, to defer
/// borrowing checks to runtime. You can see examples on how to use `inner` in
/// existing functions on `TaskManager`.
pub struct TaskManager {
    /// total number of tasks
    num_app: usize,
    /// use inner value to get mutable access
    inner: UPSafeCell<TaskManagerInner>,
}

/// The task manager inner in 'UPSafeCell'
struct TaskManagerInner {
    /// task list
    tasks: Vec<TaskControlBlock>,
    /// id of current `Running` task
    current_task: usize,
}

lazy_static! {
    /// a `TaskManager` instance through lazy_static!
    pub static ref TASK_MANAGER: TaskManager = {
        info!("init TASK_MANAGER");
        let num_app = get_num_app();
        info!("num_app = {}", num_app);
        let mut tasks: Vec<TaskControlBlock> = Vec::new();
        for i in 0..num_app {
            tasks.push(TaskControlBlock::new(get_app_data(i), i));
        }
        TaskManager {
            num_app,
            inner: unsafe {
                UPSafeCell::new(TaskManagerInner {
                    tasks,
                    current_task: 0,
                })
            },
        }
    };
}

impl TaskManager {
    /// Run the first task in task list.
    ///
    /// Generally, the first task in task list is an idle task (we call it zero process later).
    /// But in ch4, we load apps statically, so the first task is a real app.
    fn run_first_task(&self) -> ! {
        let mut inner = self.inner.exclusive_access();
        let next_task = &mut inner.tasks[0];
        next_task.task_status = TaskStatus::Running;
        let next_task_cx_ptr = &next_task.task_cx as *const TaskContext;
        drop(inner);
        let mut _unused = TaskContext::zero_init();
        // before this, we should drop local variables that must be dropped manually
        unsafe {
            __switch(&mut _unused as *mut _, next_task_cx_ptr);
        }
        panic!("unreachable in run_first_task!");
    }

    /// Change the status of current `Running` task into `Ready`.
    fn mark_current_suspended(&self) {
        let mut inner = self.inner.exclusive_access();
        let current = inner.current_task;
        inner.tasks[current].task_status = TaskStatus::Ready;
    }

    /// Change the status of current `Running` task into `Exited`.
    fn mark_current_exited(&self) {
        let mut inner = self.inner.exclusive_access();
        let current = inner.current_task;
        inner.tasks[current].task_status = TaskStatus::Exited;
    }

    /// Find next task to run and return task id.
    ///
    /// In this case, we only return the first `Ready` task in task list.
    fn find_next_task(&self) -> Option<usize> {
        let inner = self.inner.exclusive_access();
        let current = inner.current_task;
        (current + 1..current + self.num_app + 1)
            .map(|id| id % self.num_app)
            .find(|id| inner.tasks[*id].task_status == TaskStatus::Ready)
    }

    /// Get the current 'Running' task's token.
    fn get_current_token(&self) -> usize {
        let inner = self.inner.exclusive_access();
        inner.tasks[inner.current_task].get_user_token()
    }

    #[allow(clippy::mut_from_ref)]
    /// Get the current 'Running' task's trap contexts.
    fn get_current_trap_cx(&self) -> &mut TrapContext {
        let inner = self.inner.exclusive_access();
        inner.tasks[inner.current_task].get_trap_cx()
    }

    /// Switch current `Running` task to the task we have found,
    /// or there is no `Ready` task and we can exit with all applications completed
    fn run_next_task(&self) {
        if let Some(next) = self.find_next_task() {
            let mut inner = self.inner.exclusive_access();
            let current = inner.current_task;
            inner.tasks[next].task_status = TaskStatus::Running;
            inner.current_task = next;
            let current_task_cx_ptr = &mut inner.tasks[current].task_cx as *mut TaskContext;
            let next_task_cx_ptr = &inner.tasks[next].task_cx as *const TaskContext;
            drop(inner);
            // before this, we should drop local variables that must be dropped manually
            unsafe {
                __switch(current_task_cx_ptr, next_task_cx_ptr);
            }
            // go back to user mode
        } else {
            panic!("All applications completed!");
        }
    }
}

/// Run the first task in task list.
pub fn run_first_task() {
    TASK_MANAGER.run_first_task();
}

/// Switch current `Running` task to the task we have found,
/// or there is no `Ready` task and we can exit with all applications completed
fn run_next_task() {
    TASK_MANAGER.run_next_task();
}

/// Change the status of current `Running` task into `Ready`.
fn mark_current_suspended() {
    TASK_MANAGER.mark_current_suspended();
}

/// Change the status of current `Running` task into `Exited`.
fn mark_current_exited() {
    TASK_MANAGER.mark_current_exited();
}

/// Suspend the current 'Running' task and run the next task in task list.
pub fn suspend_current_and_run_next() {
    mark_current_suspended();
    run_next_task();
}

/// Exit the current 'Running' task and run the next task in task list.
pub fn exit_current_and_run_next() {
    mark_current_exited();
    run_next_task();
}

/// Get the current 'Running' task's token.
pub fn current_user_token() -> usize {
    TASK_MANAGER.get_current_token()
}

/// Get the current 'Running' task's trap contexts.
pub fn current_trap_cx() -> &'static mut TrapContext {
    TASK_MANAGER.get_current_trap_cx()
}
  • 第 37 - 42 行:创建了任务管理结构体,TaskManager结构体为控制TCB的最外围的结构体
    • 第 39 行:num_app代表了总共的应用数量。
    • 第 41 行:innerTaskManagerInner的内部可变引用。
  • 第 45 - 50 行:TaskManagerInner 结构体为控制TCB的内部结构体
    • 第 47 行:tasks为任务控制块数组。
    • 第 49 行:代表当前正在执行的应用编号。
  • 第 52 - 72 行:以lazy_static的方式初始化全局任务管理器TASK_MANAGER
    • 第 56 行:依据 loader 子模块提供的 get_num_app 函数获取链接到内核的应用数量
    • 第 58 行:创建tasks向量空间
    • 第 59 - 61 行:依次将各个应用创建相应的任务控制块并将其加入到tasks向量空间中。
    • 第 62 - 70 行:返回刚创建的TASK_MANAGER结构体。并将 current_task 设置为 0 ,表示内核将从第 0 个应用开始执行。
  • 第 119 - 122 行:为TaskManager实现了获取当前正在执行的应用的地址空间的 token
  • 第 126 - 129 行:为TaskManager实现了获取应用地址空间中的 Trap 上下文。也就是可以在内核地址空间中修改位于该应用地址空间中的 Trap 上下文的可变引用。
  • 第 187 - 189 行:对TASK_MANAGER.get_current_token()这个方法进行了封装。
  • 第 192 - 194 行:对TASK_MANAGER.get_current_trap_cx()这个方法进行了封装。

【改进 Trap 处理的实现】

改进了Trap的返回方式。

//! os/src/trap/mod.rs
//! Trap handling functionality
//!
//! For rCore, we have a single trap entry point, namely `__alltraps`. At
//! initialization in [`init()`], we set the `stvec` CSR to point to it.
//!
//! All traps go through `__alltraps`, which is defined in `trap.S`. The
//! assembly language code does just enough work restore the kernel space
//! context, ensuring that Rust code safely runs, and transfers control to
//! [`trap_handler()`].
//!
//! It then calls different functionality based on what exactly the exception
//! was. For example, timer interrupts trigger task preemption, and syscalls go
//! to [`syscall()`].
mod context;

use crate::config::{TRAMPOLINE, TRAP_CONTEXT};
use crate::syscall::syscall;
use crate::task::{
    current_trap_cx, current_user_token, exit_current_and_run_next, suspend_current_and_run_next,
};
use crate::timer::set_next_trigger;
use riscv::register::{
    mtvec::TrapMode,
    scause::{self, Exception, Interrupt, Trap},
    sie, stval, stvec,
};

core::arch::global_asm!(include_str!("trap.S"));

pub fn init() {
    set_kernel_trap_entry();
}

fn set_kernel_trap_entry() {
    unsafe {
        stvec::write(trap_from_kernel as usize, TrapMode::Direct);
    }
}

fn set_user_trap_entry() {
    unsafe {
        stvec::write(TRAMPOLINE as usize, TrapMode::Direct);
    }
}

pub fn enable_timer_interrupt() {
    unsafe {
        sie::set_stimer();
    }
}

#[no_mangle]
pub fn trap_handler() -> ! {
    set_kernel_trap_entry();
    let cx = current_trap_cx();
    let scause = scause::read();
    let stval = stval::read();
    match scause.cause() {
        Trap::Exception(Exception::UserEnvCall) => {
            cx.sepc += 4;
            cx.x[10] = syscall(cx.x[17], [cx.x[10], cx.x[11], cx.x[12]]) as usize;
        }
        Trap::Exception(Exception::StoreFault)
        | Trap::Exception(Exception::StorePageFault)
        | Trap::Exception(Exception::LoadPageFault) => {
            error!("[kernel] PageFault in application, bad addr = {:#x}, bad instruction = {:#x}, core dumped.", stval, cx.sepc);
            exit_current_and_run_next();
        }
        Trap::Exception(Exception::IllegalInstruction) => {
            error!("[kernel] IllegalInstruction in application, core dumped.");
            exit_current_and_run_next();
        }
        Trap::Interrupt(Interrupt::SupervisorTimer) => {
            set_next_trigger();
            suspend_current_and_run_next();
        }
        _ => {
            panic!(
                "Unsupported trap {:?}, stval = {:#x}!",
                scause.cause(),
                stval
            );
        }
    }
    trap_return();
}

#[no_mangle]
pub fn trap_return() -> ! {
    set_user_trap_entry();
    let trap_cx_ptr = TRAP_CONTEXT;
    let user_satp = current_user_token();
    extern "C" {
        fn __alltraps();
        fn __restore();
    }
    let restore_va = __restore as usize - __alltraps as usize + TRAMPOLINE;
    unsafe {
        core::arch::asm!(
            "fence.i",
            "jr {restore_va}",
            restore_va = in(reg) restore_va,
            in("a0") trap_cx_ptr,
            in("a1") user_satp,
            options(noreturn)
        );
    }
}

#[no_mangle]
pub fn trap_from_kernel() -> ! {
    panic!("a trap from kernel!");
}

pub use context::TrapContext;
  • 第 31 - 33 行:在内核一上电的时候就将 stvec (trap处理代码的地址)设置为 Direct 模式指向__alltraps的地址。

  • 第 35 - 39 行:将 stvec 修改为同模块下另一个函数 trap_from_kernel 的地址。这就是说,一旦进入内核后再次触发到 STrap,则硬件在设置一些 CSR 寄存器之后,会跳过对通用寄存器的保存过程,直接跳转到 trap_from_kernel 函数,在这里直接 panic 退出。这是因为内核和应用的地址空间分离之后,U态 –> S态 与 S态 –> S态 的 Trap 上下文保存与恢复实现方式/Trap 处理逻辑有很大差别。这里为了简单起见,弱化了 S态 –> S态的 Trap 处理过程:直接 panic

  • 第 41 - 45 行:将 stvec 修改为 TRAMPOLINE 的地址。stvec寄存器控制 Trap 处理代码的入口地址,则其直接转到应用的跳板处陷入到内核用户空间。

  • 第 47 - 51 行:设置 sie.stie(s-mode下时钟中断的enable比特位), 使得 S 特权级时钟中断不会被屏蔽。

  • 第 90 - 109 行:在 trap_handler 完成 Trap 处理之后,我们需要调用 trap_return 返回用户态。

    • 第 91 行:在 trap_return 的开始处就调用 set_user_trap_entry ,来让应用 Trap 到 S 的时候可以跳转到 __alltraps

      注:我们把 stvec 设置为内核和应用地址空间共享的跳板页面的起始地址 TRAMPOLINE 而不是编译器在链接时看到的 __alltraps 的地址。这是因为启用分页模式之后,内核只能通过跳板页面上的虚拟地址来实际取得 __alltraps__restore 的汇编代码。

    • 第 92 - 93 行:准备好 __restore 需要两个参数:分别是 Trap 上下文在应用地址空间中的虚拟地址和要继续执行的应用地址空间的 token

    • 第 98 行:最后我们需要跳转到 __restore ,以执行:切换到应用地址空间、从 Trap 上下文中恢复通用寄存器、 sret 继续执行应用。它的关键在于如何找到 __restore 在内核/应用地址空间中共同的虚拟地址。计算 __restore 虚地址的过程:由于 __alltraps 是对齐到地址空间跳板页面的起始地址 TRAMPOLINE 上的, 则 __restore 的虚拟地址只需在 TRAMPOLINE 基础上加上 __restore 相对于 __alltraps 的偏移量即可。这里 __alltraps__restore 都是指编译器在链接时看到的内核内存布局中的地址。

    • 第 100 - 107 行:首先需要使用 fence.i 指令清空指令缓存 i-cache 。这是因为,在内核中进行的一些操作可能导致一些原先存放某个应用代码的物理页帧如今用来存放数据或者是其他应用的代码,i-cache 中可能还保存着该物理页帧的错误快照。因此我们直接将整个 i-cache 清空避免错误。接着使用 jr 指令完成了跳转到 __restore 的任务

    在处理完trap之后需要返回到用户态的时候首先设置下次trap到内核态的stvec寄存器为__alltraps地址,也就是切换内核空间的地址。之后设置__restore所需要的两个寄存器携带的参数值。然后计算__restore的地址,跳转过去执行__restore汇编。

  • 第 112 - 114 行:trap_from_kernel函数直接进行panic提示有S态 –> S态 的 Trap

os/src/task/context.rs里面保存了任务切换所需要的结构体声明和一些方法

//! os/src/task/context.rs
//! Implementation of [`TaskContext`]
use crate::trap::trap_return;

#[derive(Copy, Clone)]
#[repr(C)]
/// task context structure containing some registers
pub struct TaskContext {
    ra: usize,
    sp: usize,
    s: [usize; 12],
}

impl TaskContext {
    pub fn zero_init() -> Self {
        Self {
            ra: 0,
            sp: 0,
            s: [0; 12],
        }
    }
    pub fn goto_trap_return(kstack_ptr: usize) -> Self {
        Self {
            ra: trap_return as usize,
            sp: kstack_ptr,
            s: [0; 12],
        }
    }
}
  • 第 24 行:ra寄存器保存的是return address返回后继续执行所需要的地址。
  • 第 25 行:sp寄存器保存的是栈指针。
  • 第 26 行:s0 - s11这12个寄存器为保存寄存器,也就是函数执行的上下文。

__switch 切换到该应用的任务上下文的时候,内核将会跳转到 trap_return 并返回用户态开始该应用的启动执行。

【改进 sys_write 的实现 -> os/src/syscall/fs.rs】

由于内核和应用地址空间的隔离, sys_write 不再能够直接访问位于应用空间中的数据,而需要手动查页表才能知道那些数据被放置在哪些物理页帧上并进行访问。

// os/src/syscall/fs.rs
//! File and filesystem-related syscalls

use crate::mm::translated_byte_buffer;
use crate::task::current_user_token;

const FD_STDOUT: usize = 1;

pub fn sys_write(fd: usize, buf: *const u8, len: usize) -> isize {
    match fd {
        FD_STDOUT => {
            let buffers = translated_byte_buffer(current_user_token(), buf, len);
            for buffer in buffers {
                print!("{}", core::str::from_utf8(buffer).unwrap());
            }
            len as isize
        }
        _ => {
            panic!("Unsupported fd in sys_write!");
        }
    }
}

在这里改进了sys_write这个函数,通过translated_byte_buffer尝试将按应用的虚地址指向的缓冲区转换为一组按内核虚地址指向的字节数组切片构成的向量,然后把每个字节数组切片转化为字符串&str 然后输出即可。

就是从应用地址空间获取一段地址将其数据显示出来。但这涉及到应用地址空间和内核地址空间,所以需要使用页表模块 page_table提供的translated_byte_buffer这个函数。

基于地址空间的分时多任务小结:

系统上电后

  1. 依据当时编译的linker.ld文件。在.text段的开头放置的是entry这个汇编,所以系统依据代码流程会首先执行entry.asm
  2. 分配64KiB的启动栈空间,将栈指针 sp 设置为先前分配的启动栈栈顶地址。Rust 代码在进行函数调用和返回的时候就可以正常在启动栈上分配和回收栈帧了。
  3. 进入主函数入口rust_main

主函数rust_main的启动流程

  1. bss段进行零初始化。
  2. 初始化日志。
  3. 初始化内存方面相关的。
    1. 首先初始化全局动态内存分配器,使可以用到Rust的堆数据结构
    2. 其次初始化物理页帧管理器,可以进行物理页帧的回收分配
    3. 最后创建内核地址空间并开启分页模式,在创建内核地址空间的时候就已经把所以的应用地址空间创建出来并加载到内核地址空间中了。
  4. 进行trap的初始化,也就是将 trap处理的寄存器地址设置为__alltraps的地址。当发生trap的时候直接跳到__alltraps进行执行。
  5. 然后开启时钟中断。
  6. 开始运行第一个任务,由于任务上下文的ra寄存器保存了下一行代码运行的地址。

Trap的处理(时钟中断到达)

  1. 由于有跳板机制,依据初始化时设置的trap地址,跳转到__alltraps上。此时还处于应用地址空间中。
  2. 在应用地址空间中保存应用上下文。
  3. 切换为内核地址空间并刷新快表。此时变为内核地址空间后就可以访问到trap_handler了。
  4. __alltraps最后跳到trap_handler执行处理。
  5. trap_handler刚开始需要将trap处理的寄存器修改为panic 退出的地址。一旦进入内核后再次触发到 STrap,在这里直接 panic 退出,也就是屏蔽S态到S态的trap
  6. 之后trap_handler依据传入的寄存器参数进行trap处理
  7. trap_handler处理完成后通过trap_return 返回用户态。
  8. trap_return修改 trap处理的寄存器地址,设置为__alltraps的地址。让下一次应用TrapS的时候可以跳到__alltraps
  9. 设置__restore返回的两个参数。
  10. 找到__restore的地址。
  11. 清空指令缓存。
  12. 跳转到__restore
  13. 切换到用户空间。
  14. 之后依据用户空间的TrapContext 恢复通用寄存器和CSR寄存器。
  15. 然后执行sret返回用户态。

由于有了地址空间的隔离机制,所以在从用户态与内核态相互切换的时候同时需要切换地址空间,从而能够访问到对应的数据。

在进行上下文保存的时候,原来是直接访问内核栈空间将相应的寄存器值保存进去,但现在处于用户地址空间是看不到内核地址空间栈的而且我们的所有通用寄存器的值是我们要保存的值是不能变化的,我们无法在不改变通用寄存器的情况下先切换到内核地址空间再保存上下文,所以将保存的上下文放到了用户地址空间中。

跳板的目的是为了能够访问到__alltraps__restore,使得可以通过这两个汇编代码跳转到内核地址空间并跳回来。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值