【结构框架】
├── 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
。
【其他语言动态内存分配】
- 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.io
或GitHub
上的依赖包,也可以引用存放在本地文件系统中的依赖包。- 从
crates.io
引入依赖包:默认下,Cargo
就从crates.io
上下载依赖包,只需要一个包名和版本号即可:符合"x.y.z"
的形式,其中x
被称为主版本major
,y
被称为小版本minor
,而z
被称为补丁patch
,可以看出从左到右,版本的影响范围逐步降低,补丁的更新是无关痛痒的,并不会造成API
的兼容性被破坏。 - 从其他注册服务引入依赖包:引入
git
仓库中的库作为依赖包,你至少需要提供一个仓库的地址:由于没有指定版本,Cargo 会假定我们使用master
或main
分支的最新commit
。可以使用rev
、tag
或branch
来指定想要拉取的版本。 - 通过路径引入本地依赖包: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)实现,具体流程如下所示:
- 将伙伴分配器的项目依赖包导入进去。
- 对伙伴分配器实例化。
- 指定伙伴分配器与Rust预留的动态内存分配接口对应。
- 增加堆分配错误处理函数。
- 增加初始化函数,赋予其内存使其可以在这段内存上通过伙伴分配器对其进行分配。(堆建立完成)
【实现 SV39 多级页表机制】
【虚拟地址和物理地址】
【内存控制相关的CSR(控制与状态)寄存器】
默认情况下 MMU 未被使能,此时无论 CPU 处于哪个特权级,访存的地址都将直接被视作物理地址。 可以通过修改 S 特权级的 satp
CSR 来启用分页模式,此后 S 和 U 特权级的访存地址会被视为虚拟地址,经过 MMU 的地址转换获得对应物理地址,再通过它来访问物理内存。
satp
的字段分布:
MODE
:控制 CPU 使用哪种页表实现。当MODE
设置为 0 的时候,代表所有访存都被视为物理地址;而设置为 8 的时候,SV39
分页机制被启用,所有 S/U 特权级的访存被视为一个 39 位的虚拟地址,它们需要先经过MMU
的地址转换流程,如果顺利的话,则会变成一个 56 位的物理地址来访问物理内存;否则则会触发异常,这体现了分页机制的内存保护能力。ASID
:表示地址空间标识符,这里还没有涉及到进程的概念,我们不需要管这个地方;PPN
:存的是根页表所在的物理页号。这样,给定一个虚拟页号,CPU 就可以从三级页表的根页表开始一步步的将其映射到一个物理页号。
【地址格式与组成】
分页管理的单个页面的大小设置为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 行:
VirtPageNum
的indexes
可以取出虚拟页号的三级页索引,并按照从高到低的顺序返回。注意它里面包裹的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
的数据的可变引用。
- 第 155 - 158 行:实现了
-
第 178 - 186 行:声明泛型
SimpleRange
结构体,里面具有l
和r
两个成员。 -
第 187 - 201 行:为泛型
SimpleRange
结构体实现方法。- 第 191 - 194 行:实现了
new
方法,l
为开始地址,r
为结束地址。 - 第 195 - 197 行:实现了
get_start
方法,返回l
。 - 第 198 - 200 行:实现了
get_end
方法,返回r
。
- 第 191 - 194 行:实现了
-
第 245 行:声明虚拟页号区间结构体
SimpleRange
。
【页表项的数据结构抽象与类型定义 -> os/src/mm/page_table.rs】
在页表中以虚拟页号作为索引不仅能够查到物理页号,还能查到一组保护位,它控制了应用对地址空间每个虚拟页面的访问权限。物理页号和全部的标志位以某种固定的格式保存在一个结构体中,它被称为页表项 (PTE, Page Table Entry) ,是利用虚拟页号在页表中查到的结果。下图为 SV39 分页模式下的页表项:
- [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 行:实现了页表项中的标志位
PTEFlags
,bitflags
是一个Rust
中常用来比特标志位的crate
。它提供了一个bitflags!
宏,如上面的代码段所展示的那样,可以将一个u8
封装成一个标志位的集合类型,支持一些常见的集合运算。需要在cargo
中引入bitflags
包。 -
第 23 行:我们让编译器自动为
PageTableEntry
实现Copy/Clone
Trait
,来让这个类型以值语义赋值/传参的时候不会发生所有权转移,而是拷贝一份新的副本。从这一点来说PageTableEntry
就和usize
一样,因为它也只是后者的一层简单包装,并解释了usize
各个比特段的含义。 -
第 26 - 28 行:定义了
PTE
的结构体。 -
第 31 - 35 行:为
PTE
实现了new
方法,将ppn
与PTEFlags
按照上图结合起来。 -
第 36 - 38 行:为
PTE
实现了清空方法。 -
第 39 - 41 行:为
PTE
实现了获取ppn
的方法,获取44位的值。 -
第 42 - 44 行:为
PTE
实现了获取PTEFlags
的方法。 -
第 45 - 47 行:依据
PTEFlags
的V
位判断是否可用。 -
第 48 - 50 行:依据
PTEFlags
的R
位判断是否可读。 -
第 51 - 53 行:依据
PTEFlags
的W
位判断是否可写。 -
第 54 - 56 行:依据
PTEFlags
的E
位判断是否可执行。 -
第 60 - 63 行:创建页表的结构
- 第 61 行:每个应用的地址空间都对应一个不同的多级页表,这也就意味这不同页表的起始地址(即页表根节点的地址)是不一样的。因此
PageTable
要保存它根节点的物理页号root_ppn
作为页表唯一的区分标志。 - 第 62 行:向量
frames
以FrameTracker
的形式保存了页表所有的节点(包括根节点)所在的物理页帧。即将这些FrameTracker
的生命周期进一步绑定到PageTable
下面。当PageTable
生命周期结束后,向量frames
里面的那些FrameTracker
也会被回收,也就意味着存放多级页表节点的那些物理页帧被回收了。
- 第 61 行:每个应用的地址空间都对应一个不同的多级页表,这也就意味这不同页表的起始地址(即页表根节点的地址)是不一样的。因此
-
第 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 ,ptr
和len
则分别表示该地址空间中的一段缓冲区的起始地址和长度(注:这个缓冲区的应用虚拟地址范围是连续的)。translated_byte_buffer
会以向量的形式返回一组可以在内核空间中直接访问的字节数组切片
【多级页表】
按需分配策略:有多少合法的虚拟页号,我们就维护一个多大的映射,并为此使用多大的内存用来保存映射。应用的地址空间最开始均不合法,这样的页表自然不需要占用任何内存。而在后面,内核在该应用的页表上也就是一对对映射顺次被插入进来,自然页表所占据的内存大小也就逐渐增加。
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) ,因此这是一种三级页表。
刚才我们提到若页表项满足 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.ld
中ekernel
地址 - 物理内存结束地址:在
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 行:将分配来的物理页帧的物理页号作为参数传给
FrameTracker
的new
方法来创建一个FrameTracker
实例。由于这个物理页帧之前可能被分配过并用做其他用途,我们在这里直接将这个物理页帧上的所有字节清零。 -
第 34 - 38 行:当一个
FrameTracker
生命周期结束被编译器回收的时候,我们需要将它控制的物理页帧回收到FRAME_ALLOCATOR
中,当一个FrameTracker
实例被回收的时候,它的drop
方法会自动被编译器调用,通过之前实现的frame_dealloc
我们就将它控制的物理页帧回收以供后续使用了。 -
第 40 - 44 行:声明一个
FrameAllocator
Trait
来描述一个物理页帧管理器需要提供哪些功能。创建一个物理页帧管理器的实例,以及以物理页号为单位进行物理页帧的分配和回收。 -
第 47 - 51 行:实现了一种栈式物理页帧管理策略,,
- 第 48 - 49 行:从未被分配出去过的物理页号区间 [
current
,end
) 也就是可用的物理页号。 - 第 50 行:
recycled
以后入先出的方式保存了被回收的物理页号。
- 第 48 - 49 行:从未被分配出去过的物理页号区间 [
-
第 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 行:分配刚减一的物理页号出去。
- 第 68 行:首先判断
- 第 77 - 85 行:回收指定的物理页帧,在回收
dealloc
的时候,我们需要检查回收页面的合法性,然后将其压入recycled
栈中- 第 80 行:判断传入的
ppn
是否正确,由于该页面之前一定被分配出去过,因此它的物理页号一定 <current ;并且该页面没有正处在回收状态,即它的物理页号不能在栈recycled
中找到。 - 第 84 行:将其压入
recycled
栈中。
- 第 80 行:判断传入的
- 第 60 - 66 行:创建一个
-
第 88 行:创建
StackFrameAllocator
的全局实例FrameAllocatorImpl
,并 -
第 90 - 94 行:在正式分配物理页帧之前将
FRAME_ALLOCATOR
初始化。- 第 92 行:这里我们使用
UPSafeCell<T>
来包裹栈式物理页帧分配器使得其具有内部可变性。每次对该分配器进行操作之前,我们都需要先通过FRAME_ALLOCATOR.exclusive_access()
拿到分配器的可变借用。
- 第 92 行:这里我们使用
-
第 97 - 105 行:物理页号起始于
ekernel
并终止于MEMORY_END
,通过这两个地址启动帧分配器。通过init
方法初始化可用区间两端。我们调用物理地址PhysAddr
的floor/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
下则挂着对应逻辑段中的数据所在的物理页帧。
- page_table:地址空间的多级页表。
-
第 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
这个值写入当前CPU
的satp 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 四个标志位,因为其他的标志位仅与硬件的地址转换机制细节相关,这样的设计能避免引入错误的标志位。
- 第 225 行:
-
第 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 行:调用
PageTable
的unmap
接口删除以传入的虚拟页号为键的键值对即可。然而,当以Framed
映射的时候,不要忘记同时将虚拟页面被映射到的物理页帧FrameTracker
从data_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 行:切换到下一页。
- 第 288 行:声明开始复制的
- 第 232 - 237 行:
-
第 310 - 313 行:声明枚举映射的类型,
Identical
表示恒等映射方式;而Framed
则表示对于每个虚拟页面都有一个新分配的物理页帧与之对应,虚地址与物理地址的映射关系是相对随机的。 -
第 315 - 323 行:声明
MapPermission
结构体使得可以对每一位进行操作。 -
第 326 - 346 行:通过手动查找内核多级页表的方式验证代码段和只读数据段不允许被写入,同时不允许从数据段上取指执行。
【地址空间:一系列有关联的逻辑段】
地址空间 是一系列有关联的不一定连续的逻辑段,这种关联一般是指这些逻辑段组成的虚拟内存空间与一个运行的程序(目前把一个运行的程序称为任务,后续会称为进程)绑定,即这个运行的程序对代码和数据的直接访问范围限制在它关联的虚拟地址空间之内。这样我们就有任务的地址空间,内核的地址空间等说法了。地址空间使用 MemorySet
类型来表示。
通过地址空间可以区分不同的应用,因为他们最终所映射到的物理页是不同的。地址空间是最上面的集合,每个地址空间下还有许多的逻辑段,逻辑段是地址空间的最小单位。地址空间只需要管两部分,它所含有的逻辑段有哪些和它所具有的物理地址有哪些。逻辑段则是真正操作的部分。
【内核地址空间】
启用分页模式下,内核代码的访存地址也会被视为一个虚拟地址并需要经过 MMU 的地址转换,因此我们也需要为内核对应构造一个地址空间,它除了仍然需要允许内核的各数据段能够被正常访问之后,还需要包含所有应用的内核栈以及一个 跳板 (Trampoline) 。
如图所示为内核地址高256GiB
的内存布局:
跳板放在最高的一个虚拟页面中。接下来则是从高到低放置每个应用的内核栈,内核栈的大小由 config
子模块的 KERNEL_STACK_SIZE
给出。它们的映射方式为 MapPermission
中的 rw 两个标志位,意味着这个逻辑段仅允许 CPU 处于内核态访问,且只能读或写。
相邻两个内核栈之间会预留一个 保护页面 (Guard Page) ,它是内核地址空间中的空洞,多级页表中并不存在与它相关的映射。它的意义在于当内核栈空间不足(如调用层数过多或死递归)的时候,代码会尝试访问空洞区域内的虚拟地址,然而它无法在多级页表中找到映射,便会触发异常,此时控制权会交给内核 trap handler
函数进行异常处理。由于我们的内核非常简单且内核栈的大小设置比较宽裕,在当前的设计中我们仅将空洞区域的大小设置为单个页面。
如图所示为内核地址低256GiB
的内存布局:
四个逻辑段 .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 行:获取(需要的应用切片位置开始,下一个应用开始)位置之间的地址。也就是代码段。
- 第 16 行:获取
os/src/loader.rs
获取对应APP
的ELF
文件。
在创建应用地址空间的时候,我们需要对 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
。需要注意的是这里发生了两次隐式类型转换:- 我们知道
exclusive_access
是UPSafeCell<T>
的方法而不是Arc<T>
的方法,由于Arc<T>
实现了Deref
Trait
,当exclusive_access
需要一个&UPSafeCell<T>
类型的参数的时候,编译器会自动将传入的Arc<UPSafeCell<T>>
转换为&UPSafeCell<T>
这样就实现了类型匹配; - 事实上
UPSafeCell<T>::exclusive_access
返回的是一个RefMut<'_, T>
,这同样是 RAII 的思想,当这个类型生命周期结束后互斥锁就会被释放。而该类型实现了DerefMut
Trait
,因此当一个函数接受类型为&mut T
的参数却被传入一个类型为&mut RefMut<'_, T>
的参数的时候,编译器会自动进行类型转换使参数匹配。
- 我们知道
- 最后,我们调用
MemorySet::activate
来设置CSR
的satp
寄存器来开启MMU
。在这之后 S 和 U 特权级的访存地址会被视为一个虚拟地址,它需要经过 MMU 的地址转换变为一个物理地址,再通过它来访问物理内存- 我们必须注意切换
satp CSR
是否是一个 平滑 的过渡:由于切换前我们使用的是物理地址直接取指的方式,而切换后我们使用的是MMU
查找多级页表的方式(由于切换 satp 的指令并不是一条跳转指令, pc 只是简单的自增当前指令的字长)。这就要求前后两个地址空间在切换 satp 的指令 附近 的映射满足某种意义上的连续性。 - 幸运的是,我们做到了这一点。这条写入
satp
的指令及其下一条指令都在内核内存布局的代码段中,在切换之后是一个恒等映射,而在切换之前是视为物理地址直接取指,也可以将其看成一个恒等映射。这完全符合我们的期待:即使切换了地址空间,指令仍应该能够被连续的执行。
- 我们必须注意切换
- 首先,我们引用
【检查内核地址空间的多级页表设置】
调用 mm::init
之后我们就使能了内核动态内存分配、物理页帧管理,还启用了分页模式进入了内核地址空间。之后我们可以通过 mm::remap_test
来检查内核地址空间的多级页表是否被正确设置。也就是验证各个段相应的权限能力(查看标志位),类似于可读可写可执行。
【跳板机制的实现】
第二章介绍过的 Trap
上下文保存与恢复:当一个应用 Trap
到内核时,sscratch
已指向该应用的内核栈栈顶,我们用一条指令即可从用户栈切换到内核栈,然后直接将 Trap
上下文压入内核栈栈顶。当 Trap
处理完毕返回用户态的时候,将 Trap
上下文中的内容恢复到寄存器上,最后将保存着应用用户栈顶的 sscratch
与 sp
进行交换,也就从内核栈切换回了用户栈。
一旦使能了分页机制,一切就并没有这么简单了:我们必须在这个过程中同时完成地址空间的切换。具体来说,当 __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.trampoline
的section
中。 - 第 10 行:将
__alltraps
设为该段的开头。 - 第 11 行:紧接着跟着
__restore
。 - 第 12 行:设置对其方式。
- 第 13 行:开始进入保存
Trap
部分。 - 第 14 行:刚开时
sp
寄存器仍指向用户栈,但sscratch
则被设置为指向应用地址空间中存放Trap
上下文的位置(实际在次高页面)。随后,就像之前一样,我们csrrw
交换sp
和sscratch
。 - 第 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_ADDRESS
为0x80200000
,为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上下文进行初始化。
- 第 26 行:解析传入的 ELF 格式数据构造应用的地址空间
【更新对任务控制块的管理 -> 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 行:
inner
为TaskManagerInner
的内部可变引用。
- 第 39 行:
- 第 45 - 50 行:
TaskManagerInner
结构体为控制TCB的内部结构体。- 第 47 行:
tasks
为任务控制块数组。 - 第 49 行:代表当前正在执行的应用编号。
- 第 47 行:
- 第 52 - 72 行:以
lazy_static
的方式初始化全局任务管理器TASK_MANAGER
。- 第 56 行:依据
loader
子模块提供的get_num_app
函数获取链接到内核的应用数量 - 第 58 行:创建
tasks
向量空间 - 第 59 - 61 行:依次将各个应用创建相应的任务控制块并将其加入到
tasks
向量空间中。 - 第 62 - 70 行:返回刚创建的
TASK_MANAGER
结构体。并将current_task
设置为 0 ,表示内核将从第 0 个应用开始执行。
- 第 56 行:依据
- 第 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
的地址。这就是说,一旦进入内核后再次触发到S
态Trap
,则硬件在设置一些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
这个函数。
基于地址空间的分时多任务小结:
系统上电后:
- 依据当时编译的
linker.ld
文件。在.text
段的开头放置的是entry
这个汇编,所以系统依据代码流程会首先执行entry.asm
。- 分配
64KiB
的启动栈空间,将栈指针sp
设置为先前分配的启动栈栈顶地址。Rust 代码在进行函数调用和返回的时候就可以正常在启动栈上分配和回收栈帧了。- 进入主函数入口
rust_main
:主函数
rust_main
的启动流程:
- 将
bss
段进行零初始化。- 初始化日志。
- 初始化内存方面相关的。
- 首先初始化全局动态内存分配器,使可以用到
Rust
的堆数据结构- 其次初始化物理页帧管理器,可以进行物理页帧的回收分配
- 最后创建内核地址空间并开启分页模式,在创建内核地址空间的时候就已经把所以的应用地址空间创建出来并加载到内核地址空间中了。
- 进行
trap
的初始化,也就是将trap
处理的寄存器地址设置为__alltraps
的地址。当发生trap的时候直接跳到__alltraps
进行执行。- 然后开启时钟中断。
- 开始运行第一个任务,由于任务上下文的
ra
寄存器保存了下一行代码运行的地址。
Trap
的处理(时钟中断到达):
- 由于有跳板机制,依据初始化时设置的
trap
地址,跳转到__alltraps
上。此时还处于应用地址空间中。- 在应用地址空间中保存应用上下文。
- 切换为内核地址空间并刷新快表。此时变为内核地址空间后就可以访问到
trap_handler
了。- 在
__alltraps
最后跳到trap_handler
执行处理。trap_handler
刚开始需要将trap
处理的寄存器修改为panic
退出的地址。一旦进入内核后再次触发到S
态Trap
,在这里直接panic
退出,也就是屏蔽S
态到S
态的trap
。- 之后
trap_handler
依据传入的寄存器参数进行trap
处理trap_handler
处理完成后通过trap_return
返回用户态。trap_return
修改trap
处理的寄存器地址,设置为__alltraps
的地址。让下一次应用Trap
到S
的时候可以跳到__alltraps
。- 设置
__restore
返回的两个参数。- 找到
__restore
的地址。- 清空指令缓存。
- 跳转到
__restore
。- 切换到用户空间。
- 之后依据用户空间的
TrapContext
恢复通用寄存器和CSR
寄存器。- 然后执行
sret
返回用户态。由于有了地址空间的隔离机制,所以在从用户态与内核态相互切换的时候同时需要切换地址空间,从而能够访问到对应的数据。
在进行上下文保存的时候,原来是直接访问内核栈空间将相应的寄存器值保存进去,但现在处于用户地址空间是看不到内核地址空间栈的而且我们的所有通用寄存器的值是我们要保存的值是不能变化的,我们无法在不改变通用寄存器的情况下先切换到内核地址空间再保存上下文,所以将保存的上下文放到了用户地址空间中。
跳板的目的是为了能够访问到
__alltraps
和__restore
,使得可以通过这两个汇编代码跳转到内核地址空间并跳回来。