|
sv39分页
|
|
内存控制相关CSR寄存器
|
默认MMU不使能
|
访问地址直接是物理地址
|
修改satp CSR启用分页模式
|
RV64 架构下 satp 的字段分布。
|
|
MODE
0
|
所有访存都被视为物理地址
|
|
8
|
SV39 分页
|
所有 S/U 特权级的访存被视为一个 39 位的虚拟地址
|
MMU 会将其转换成 56 位的物理地址;
|
如果转换失败,则会触发异常。
|
|
|
|
|
address格式组成
|
|
每个virtual page和phy page都按 4 KB 对齐
|
|
|
page offset
|
因此virtual address和phy address分成两部分
|
低12 bit都是page offset
|
用来定位address位于一个4kb page里的哪个位置
|
|
|
PFN
|
phy frame number,将整个物理地址分成page大小后对应的每个page的index
|
VPN最后会映射到一个PFN
|
|
VPN
|
virtual address的高27 bit[38:12]是virtual page number
|
PPN
|
phy address的高44 bit[55:12]是phy page number
|
|
page number用来定位virtual/phy address属于哪一个virtual/phy page frame
|
|
|
转换
page为单位
|
前后page offset不变
|
MMU从virtual address取出27bit的VPN
|
page table中查找到对应的PPN
|
找到,就将PPN与page offset合并,获得44+12=56 bit的物理地址
|
|
RV64架构的sv39,只有低39bit有真正意义
|
要求64bit的virtual address的[63:39]这25bit
|
与第38bit相同
|
否则MMU认为是不合法地址
|
只有2^39的地址,最低256g(38bit为0)和最高的256g(38bit为1)可以通过MMU检查
|
|
|
|
address和page number
|
|
PAGE_SIZE = 4096
|
|
PAGE_SIZE_BITS = 12
|
page_offset
|
page_offset(&self) -> usize { self.0 & (PAGE_SIZE - 1) }
|
相当于取低12bit内容
|
|
从PPN到phy addr
|
fn from(v: PhysPageNum) -> Self { Self(v.0 << PAGE_SIZE_BITS) }
|
左移12bit
|
|
phy addr需要保证与page大小4k对齐
|
才能通过右移转换为PPN
|
|
fn from(v: PhysAddr) -> Self {
|
|
assert_eq!(v.page_offset(), 0);
|
|
v.floor()
|
|
}
|
|
|
|
pub fn floor(&self) -> PhysPageNum { PhysPageNum(self.0 / PAGE_SIZE) }
|
向下取整floor
|
pub fn ceil(&self) -> PhysPageNum { PhysPageNum((self.0 + PAGE_SIZE - 1) / PAGE_SIZE) }
|
向上取整ceil
|
|
|
page table entry,PTE
|
|
[53:10] 44bit是PPN
|
[7:0] 8bit是标志位
V(Valid) 位为 1 时,页表项才是合法的;
|
R/W/X 分别控制索引到这个页表项的对应虚拟页面是否允许读/写/取指;
|
U 控制索引到这个页表项的对应虚拟页面是否在 CPU 处于 U 特权级的情况下是否被允许访问;
|
G 我们不理会;
|
A(Accessed) 记录自从页表项上的这一位被清零之后,页表项的对应虚拟页面是否被访问过;
|
D(Dirty) 则记录自从页表项上的这一位被清零之后,页表项的对应虚拟页表是否被修改过。
|
代码里用bitflags,用u8封装成一个标志位集合类型
|
|
通过PPN和一个PTEFlags生成PTE
|
PageTableEntry {
|
bits: ppn.0 << 10 | flags.bits as usize,
|
}
|
|
PTE拿PPN
|
(self.bits >> 10 & ((1usize << 44) - 1)).into()
1usize << 44) - 1
|
只取44bits的PPN
|
|
PTE拿flags
|
PTEFlags::from_bits(self.bits as u8).unwrap()
|
|
|
PFN的分配与回收
|
通过多级VPN最后索引到的就是PFN
|
其中每一级VPN会索引到一个PPN,这个PPN指向了下一级的page table
|
直到最后一个page table的pte的ppn是最终需要的pfn(pfn和ppn道理上都是物理页号所以pfn直接用ppn结构体表示)
|
|
|
需要知道哪一部分物理内存可用
|
os/src/linker.ld
|
ekernel
|
指明内核数据终止phy address
|
之后的物理内存都可用
|
|
|
os/src/config
|
MEMORY_END: usize = 0x80800000;
|
硬编码整块物理内存终止物理地址0x80800000
|
物理起始地址0x80000000
|
可用内存大小8M
|
|
|
因而内存PFN左开右闭从ekernel上取整-MEMORY_END下取整
|
|
FramAllocator
|
描述PFN管理器需要提供的功能
|
|
|
最简单栈式PFN管理
|
PFN[current,end),表示从未分配的区间
|
recycled后入先出方式保存被回收的PFN
|
|
|
初始化设置current和end
|
|
alloc
从recycled pop
|
有直接返回
|
|
不然检查current == end没内存了回None
|
|
不然current +=1,返回current -1
|
|
PFN跟PPN的表示方式一样
|
所以这里使用的是PPN
|
|
dealloc
如果ppn >= current 或者recycled里找到了ppn
|
报错,分配过了
|
不然recycled push这个ppn
|
|
包装在全局FRAME_ALLOCATOR
|
装成PFN
|
|
包装的frame_alloc返回FrameTracker
|
FrameTracker这里将ppn先做了清零
|
再保存ppn
|
因为ppn可能被分配干过的别的,需要先清零
|
|
|
实现了drop trait,被drop的时候自动dealloc ppn
|
|
|
|
|
多级page table实现
|
sv39多级页表以节点为单位管理
|
每个节点在一个PFN中
|
位置可以用PPN来表示
|
|
结构
|
init
|
先alloc一个frame里面的ppn给root_ppn
|
|
alloc的frame塞进frames
|
|
|
恒等映射identical Mapping
|
每个pfn用一个在多级页表中与其ppn相等的vpn映射
|
|
impl ppn
|
由于恒等映射
|
裸指针虽然对应virtual addr
|
所以映射到相同的phy addr
|
所以可以直接使用
|
|
|
pub fn get_pte_array(&self) -> &'static mut [PageTableEntry] {
let pa: PhysAddr = self.clone().into();
|
前边定义了从ppn到phy address,相当于ppn当前page的开始地址
|
core::slice::from_raw_parts_mut(pa.0 as *mut PageTableEntry, 512)
|
返回512个pte
|
一个page 4k / 一个pte 8bytes
|
得到一个page table有512个pte
|
|
|
|
get_bytes_array(&self) -> &'static mut [u8] {
core::slice::from_raw_parts_mut(pa.0 as *mut u8, 4096)
|
pa同上,返回的是一个page的bytes
|
|
|
pub fn get_mut(&self) -> &'static mut T {
(pa.0 as *mut T).as_mut().unwrap()
|
pa同上,返回当前地址的任意类型
|
|
|
impl vpn
|
pub fn indexes(&self) -> [usize; 3] {
for i in (0..3).rev() {
|
|
idx[i] = vpn & 511;
|
// 取当前低 9 位作为索引(511 = 0b1_1111_1111)
|
vpn >>= 9;
|
|
}
|
|
idx
|
|
|
取出vpn的三级页索引
|
这个缺了图例,vpn实际上在sv39的时候会有三级索引
|
但这个图例是three piece里的,这里的低是12bit不是图例里的8bit
|
每个索引是9bit不是这里的7bit
|
|
|
|
按照从高到低顺序返回
|
|
impl PageTable
|
find_pte_create(&mut self, vpn: VirtPageNum) -> Option<&mut PageTableEntry> {
拿了root的ppn
|
|
返回当前ppn的所有的PageTableEntry
|
get_pte_array
|
根据idxs[i]来索引找到对应pte
|
|
如果i == 2是最后一级pte里面有vpn映射的真实pfn
|
这里获得最后的pte
|
不然判断如果pte不valid
frame_alloc新的
|
新建一个pte
|
frames加新
|
|
|
更新ppn为当前pte的ppn,继续循环
|
|
|
pub fn map(&mut self, vpn: VirtPageNum, ppn: PhysPageNum, flags: PTEFlags) {
pte到ppn的映射
|
|
|
先根据vpn
|
find_pte_create(vpn)
|
找个要不创个
|
!is_valid有问题
|
assert
|
|
用新的ppn和flag更新pte
|
|
|
|
pub fn unmap(&mut self, vpn: VirtPageNum) {
|
不经过MMU手动查页表的方法们
|
说是这么说也得解析多级vpn,只是不创建新的pte
|
相当于只读pagetable
|
|
pub fn from_token(satp: usize) -> Self {
临时创建一个手动查页表的PageTable
|
只有一个satp token得到的多级页表root节点的ppn
|
frames为空
|
|
fn find_pte(&self, vpn: VirtPageNum) -> Option<&PageTableEntry> {
与上面的find_pte_create类似
|
只是不会分配ppn,遇到is_valid()直接返回None
|
|
pub fn translate(&self, vpn: VirtPageNum) -> Option {
调用find_pte,找到了返回一个clone()
|
|
|
|
|
地址空间address space
|
address space
|
地址空间抽象的重要意义在于 隔离 (Isolation)
|
|
执行每个应用的代码的时候,
|
内核需要控制 MMU 使用这个app address space的多级page table进行地址转换
|
|
|
应用的address space只能访问自己page table映射到的pfn
|
|
|
os也需要通过MMU,有自己的address space(MeomrySet)
|
|
|
保存App的KernelStack
|
大小KERNEL_STACK_SIZE
|
rw,可读可写
|
|
中间用Guard Page隔开
|
这样在空间不足的时候
|
访问到空洞
|
空洞page table没有映射
|
会触发exception
|
可以交给trap handler处理错误
|
|
但
|
由于编译器会对访存顺序和局部变量在栈帧中的位置进行优化
|
难以确定一个已经溢出的栈帧中的哪些位置会先被访问
|
总的来说,空洞区域被设置的越大,我们就能越早捕获到这一错误并避免它覆盖其他重要数据
|
|
|
|
|
|
低256g
|
4个逻辑section,恒等映射
|
.text/.rodata/.data/.bss
|
无需调整内核内存布局 os/src/linker.ld
|
|
仍能和启用页表机制之前那样访问内核的各数据段
|
|
加入了访问限制
|
U都没有设置
|
只能S态访问
|
.text
|
不能w
|
.rodata
|
不能wx
|
.data/.bss
|
可以rw不能x
|
|
|
os address space需要存在一个恒等映射到os 数据段之外的可用pfn的MapArea,
|
这个嗯
|
TODO
|
点了之前也没搞清楚为啥需要
|
对应那个ekernel - MEMORY_END的内存
|
|
这样才能在启用page table机制之后,内核仍能以纯软件的方式读写这些物理页帧。
|
|
它们的标志位仅包含 rw ,意味着该 逻辑段只能在 S 特权级以上访问,并且只能读写。
|
|
|
|
|
|
逻辑段
|
MMU 通过查多级page table 可以正确完成地址转换的连续的virtual address区间
|
区间内包含的所有vpn都以一种相同的方式映射到pfn,
|
具有可读/可写/可执行等属性
|
|
逻辑段MapArea
|
描述一段连续地址的virtual address
|
pub struct MapArea {
vpn_range: VPNRange,
|
vpn的连续区间
|
data_frames: BTreeMap,
|
type是Framed
|
每个vpn和映射到的pfn的FrameTracker的一个键值对容器
|
BTreeMap
|
这些pfn用来存放实际内存data,而不是多级页表中间节点
|
声明周期绑定MapAread,一种RAII
|
|
map_type: MapType,
|
vpn映射到pfn的方式
Identical
|
恒等映射
|
Framed
|
每个vpn需要映射到一个新分配的pfn
|
|
map_perm: MapPermission,
|
访问方式
|
PTEFlags子集
|
|
仅有U/R/W/X
|
|
|
|
}
|
pub fn map_one(&mut self, page_table: &mut PageTable, vpn: VirtPageNum) {
page_table已知
|
如果恒等Identical,ppn就是vpn
|
如果Framed
frame_alloc
|
新的pfn
|
ppn更新成frame.ppn
|
|
data_frames插入
|
vpn映射frame
|
|
设置pte_flags
|
更新page_table里vpn , ppn映射
|
|
pub fn unmap_one(&mut self, page_table: &mut PageTable, vpn: VirtPageNum) {
如果Framed类型,从data_frames里删除vpn key
|
page_table unmap vpn
|
|
pub fn map(&mut self, page_table: &mut PageTable) {
针对vpn_range里的每个vpn
|
跑一次map_one
|
|
pub fn unmap(&mut self, page_table: &mut PageTable) {
|
pub fn copy_data(&mut self, page_table: &mut PageTable, data: &[u8]) {
需要假设
|
/// data: start-aligned but maybe with shorter length
|
/// assume that all frames were cleared before
|
|
|
vpn_range.get_start()拿到开头的vpn
|
loop
data截取到start+PAGE_SIZE大小
|
通过vpn获得对应的ppn里的所有bytes的,src.len()长度bytes内容
|
copy_from_slice过去
|
start+PAGE_SIZE
|
如果start >= len 数据拷贝完成,退出
|
进入下个vpn
|
|
|
|
MemorySet
|
地址空间,一系列有关联的逻辑段MapArea
|
|
pub struct MemorySet {
page_table: PageTable,
|
多级页表
|
挂着所有多级页表的每个节点pte所在的pfn
|
|
areas: Vec,
|
MapArea vec
|
挂着对应逻辑段中的data所在pfn
|
|
|
}
|
pub fn new_bare() -> Self {
|
fn push(&mut self, mut map_area: MapArea, data: Option<&[u8]>) {
map_area
|
map
|
当前的page_table
|
|
|
如果有data
|
说明里说如果framed映射才插入
|
但没有这个判断
|
|
map_area,copy_data
|
|
最后areas push(map_area)
|
|
|
pub fn insert_framed_area(&mut self, start_va: VirtAddr, end_va: VirtAddr, permission: MapPermission) {
|
pub fn new_kernel() -> Self;
|
pub fn from_elf(elf_data: &[u8]) -> (Self, usize, usize);
生成elf的address space
|
插入跳板
|
xmas_elf解析elf数据
|
根绝elf魔数判断是否合法elf
|
直接得到program header 数目
遍历
|
将合适区域加入address space
|
|
确认program header类型LOAD
|
|
表明有被os加载的必要
|
|
通过ph.virtual_addr() ph.mem_size()计算这一区域在app address space中位置
|
|
通过ph.flags()确认访问限制,转换为MapPermission,默认包含U
|
|
创建map_area
|
|
push到address space
|
|
完成data拷贝,data存放位置通过ph.offset() 和ph.file_size()找到
|
|
注意
|
存在一部分零初始化
|
ph.file_size()小于ph.mem_size()
|
|
|
这些零出于缩减可执行文件大小的原因不应该实际出现在 ELF 数据中
|
|
|
处理app stack,之前维护的max_end_vpn记录目前涉及最大vpn
|
在上面接着放个保护页
|
app stack
|
然后映射次高页面,存放TrapContext
|
返回memory_set,user_stack_top,entry_point
|
|
|
|
os地址空间
|
pub fn new_kernel() -> Self {
map跳板
|
|
map各个section, .text, .rodata, .data, .bss
|
|
还有一块固定的ekernel-MEMORY_END这个是干啥的?来着?
|
TODO
|
|
|
app地址空间
|
效仿内核地址空间的设计
|
借助页表page table机制
|
使得应用地址空间的各个MapArea也可以有不同的访问方式限制
|
提早检测出app的错误
|
|
之前每个应用app链接脚本中的起始地址base address被要求是不同的
|
代码和数据存放的位置才不会产生冲突
|
|
现在,借助地址空间的抽象
|
所有应用程序都使用同样的起始地址base address,只需要一个ld script
|
|
|
|
在 .text 和 .rodata 中间以及 .rodata 和 .data 中间我们进行了page对齐ALIGN(4K)
|
前后两个logical section的访问方式限制是不同的
|
鉴于只能以page为单位对这个限制进行设置
|
|
.data 和 .bss 两个逻辑段由于限制相同,无需进行页面对齐。
|
|
左侧是低256g
|
直观
|
右侧高256g
|
最高方跳板,直接映射
|
所以与os address space的内容一致
|
TrapContext也在那,但
|
|
这俩都不U
|
只在address space切换发挥作用
|
|
|
|
os/src/build.rs
|
|
|
pub fn get_app_data(app_id: usize) -> &'static [u8] {
get_app_data 则根据传入的应用编号取出对应应用的 ELF 格式可执行文件数据
|
基于 build.rs 生成的 link_app.S 给出的符号来确定其位置
|
实际放在内核的.data中
|
loader 模块中原有的os和app stack则分别作为逻辑段放在os和app的space address中,
|
我们无需再去专门为其定义一种类型。
|
|
|
|
|
|
更新分时多任务
|
SBI初始化完成
|
CPU跳转到os entry并在S-Mode上执行 instruction
|
还没有开启分页
|
开启分页后,内核每次访存看的都是虚拟地址
|
|
|
os/src/mm/memory_set.rs
|
创建内核地址空间
pub static ref KERNEL_SPACE: Arc> = Arc::new(unsafe {
UPSafeCell::new(MemorySet::new_kernel()
|
|
)});
|
编译进全局.data
|
|
|
|
os/src/mm/mod.rs
init
初始化heap
|
init_frame_allocator用到了vec需要heap
|
KERNEL_SPACE.activate
|
这里开启了mmu
|
|
|
|
|
implMemorySet
pub fn activate(&self) {
通过老样子token获得root的ppn
|
写入satp,正式开始mmu
|
需要注意satp CSR是否是一个平滑的过渡
|
两条相邻指令的virtual address相邻
|
由于切换satp的instauction不是一条跳转指令
|
pc只简单自增
|
|
而它们所在的phy address一般情况下相邻
|
但经过地址转换后的virtual address却不同
|
|
切换satp导致mmu查多级页表
|
要求前后两个address space在切换satp的指令附近映射满足连续
|
这条写入satp的instruction和下一条instruction都在os内存布局的.text段中
|
|
切换后是一个恒等映射
|
|
切换前是物理地址直接取指-类似一个恒等映射
|
|
|
|
插入sfence.vma
因多级页表需要多次访问得到最后的pfn
|
加入TLB快表来缓存vpn到pfn的映射,加速访存
|
修改了satp切换了address space
|
TLB失效,需要刷新
|
|
|
|
|
跳板
跳板干什么用
|
?
|
为什么高地址次级页面存放TrapContext
|
?
|
为什么不把TrapContext放入应用的内核栈
|
?
|
|
曾经
当app trap进os
|
sscratch指出app stack top
|
用一条指令可从app stack 切换到os stack
|
然后直接将TrapContext压入os stack top
|
|
Trap处理完毕,返回app
|
TrapContext恢复到寄存器
|
将保存app stack top的sscratch与sp交换
|
从os stack切换回app stack
|
|
|
一旦分页
必须在这个过程同时完成address space的切换
|
当__alltraps保存Trap Context的时候
|
需要修改satp从app address space切换到os address space
|
因为trap handler只有在os address space才能访问
|
|
|
address space切换不能影响instruction连续执行
|
要求app和os的address space切换address space的instruction附近平滑
|
|
|
内核与应用地址空间的隔离
采用xv6的os和app address space隔离的方法
|
不需要内核逻辑段在每个app address space映射一次
|
在嵌入式平台里无法忽视这种内存开销
|
相对每次trap进内核,TLB都会失效
|
但任务切换不会,因为都在os内完成
|
|
可以一定程度上应对Meltdown漏洞
https://cacm.acm.org/magazines/2020/6/245161-meltdown/fulltext
|
|
|
app的TrapContext放入address space次高page
假如放入os stack
保存TrapContext之前
|
必须先切换到os address space
|
需要将os address space token写入satp
|
|
之后还需要一个通用reg保存os stack top
|
才能以它为base addres保存TrapContext
|
|
我们无法在不破坏任何一个通用reg的情况下做到
|
事实上需要用到os两条信息
address space token
|
app 内核stack top
|
|
硬件只提供一个sscratch周转
|
不得不将TrapContext保存在app address space的一个virtual page中
|
避免切换到os address space才能保存TrapContext
|
|
在TraopContext包含更多内容
app初始化的时候由内核写入应用地址空间app address space中的 TrapContext 的相应位置,此后就不再被修改
|
不需要每次被保存/恢复
|
pub struct TrapContext {
|
|
pub x: [usize; 32],
|
|
pub sstatus: Sstatus,
|
|
pub sepc: usize,
|
|
pub kernel_satp: usize,
|
表示内核地址空间的 token ;
|
pub kernel_sp: usize,
|
当前应用app在内核地址空间os address space中的内核栈栈顶os stack top的虚拟地址virtual address;
|
pub trap_handler: usize,
|
内核os中 trap handler 入口点的虚拟地址virtual address。
|
}
|
|
|
|
|
os/src/trap/trap.S
__alltraps和__restore
|
保存和恢复 TrapContext的同时也切换Address Space的:
|
__alltraps
Trap进入,
|
|
sp指向app stack
|
sscratch指向app address存放TrapContext的位置,在次高Page
|
|
见__restore
|
里面会将TrapContext保存到sscratch
|
|
交换sp-sscratch
|
|
sp已经变成了TrapContext
|
开始保存reg和csr
|
|
|
到28结束,全程在app address space完成保存TrapContext
|
|
|
然后加载os stack address space token到t0
|
trap handler entry virtual address到t1
|
|
|
将sp修改为app的os stack top的地址
|
|
satp修改为os stack address space token使用sfence.vma刷新tlb
|
完成切换
|
jr跳转到t1保存的trap handler入口点地址
|
|
不能像之前直接call trap_handler
|
|
|
__restore
两个参数
TrapContext在app address space的位置
|
对于所有应用相同
|
|
a0传递
|
即将回到app的address space token
|
a1传递
|
|
由于TrapContext保存在app address space
|
|
先切换回app address space
|
|
将传入的TrapContext保存回sscratch寄存器
|
__alltraps才能基于它将TrapContext保存到正确位置
|
将sp修改为TrapContext
|
|
基于它恢复reg和csr
|
|
通过sret返回U-Mode
|
|
|
|
|
os/src/linker.ld
考虑切换address space后instruction能否连续执行
|
3 stext = .;
|
4 .text : {
|
5 *(.text.entry)
|
6+ . = ALIGN(4K);
|
7+ strampoline = .;
|
8+ *(.text.trampoline);
|
9+ . = ALIGN(4K);
|
10 *(.text .text.*)
|
|
将trap.S整段汇编代码放在.text.trampoline section
|
调整布局对齐到.text section的一个page中
|
|
这样,这段汇编代码放在一个pfn中
|
且 __alltraps 恰好位于这个pfn的开头
|
物理地址被外部符号 strampoline 标记
|
|
开启分页模式之后
|
内核和应用代码都只能看到各自的虚拟地址空间
|
在它们的视角中,这段汇编代码被放在它们地址空间的最高虚拟页面上
|
|
trap前后的一小段时间内会有一个比较 极端 的情况
|
刚产生trap时,CPU已经进入了内核态(即Supervisor Mode)
|
此时执行代码和访问数据还是在app所处的app address space中
|
|
CPU指令 为什么能够被连续执行呢?
|
无论是os还是app的address space
|
跳板的虚拟页均位于同样位置
|
将会映射到同一个实际存放这段汇编代码的pfn
|
|
|
impl MemorySet
这个在new_kernel和每个elf文件映射都会调用
|
fn map_trampoline(&mut self) {
self.page_table.map(
|
VirtAddr::from(TRAMPOLINE).into(),
|
PhysAddr::from(strampoline as usize).into(),
|
PTEFlags::R | PTEFlags::X,
|
);
|
直接在多级页表中插入一个从地址空间的最高virtual address
|
映射到 跳板汇编代码所在的phy address的键值对
|
|
|
解释了__alltraps使用jr而不是直接call trap_handler
|
内存布局中,这条 .text.trampoline section中的call指令和 trap_handler 都在.text之内
|
汇编器(Assembler) 和链接器(Linker)会根据 linker.ld 的地址布局描述,
|
计算两个instruction的地址偏移量
|
并让call指令的实际效果为当前 pc 自增这个偏移量。
|
|
实际上我们知道由于我们设计的缘故,
|
|
这条指令在被执行的时候,
|
|
它的虚拟地址被操作os设置在address space中的最高page之内
|
见上面的map_trampoline
|
|
jr绝对跳转,直接跳转到reg中存储地址
|
不依赖pc偏移
|
|
|
|
|
|
加载执行app
扩展TCB
|
3pub struct TaskControlBlock {
|
|
4 pub task_status: TaskStatus,
|
|
5 pub task_cx: TaskContext,
|
|
6 pub memory_set: MemorySet,
|
app地址空间
|
7 pub trap_cx_ppn: PhysPageNum,
|
在高page的TrapContext实际存放的ppn
|
8 pub base_size: usize,
|
应用数据大小
app address space从0x0到app stack结束一共多少字节
|
|
后续应包含用于app 动态分配的heap空间大小
|
暂不支持
|
9}
|
|
|
扩展对tcb的管理
|
impl TaskControlBlock {
pub fn new(elf_data: &[u8], app_id: usize) -> Self {
解析elf,构造address space: memory_set
|
通过memory_set查找多级页表,找到TrapContext所在的ppn
|
根据传入的app_id,调用kernel_stack_position找到app的os stack的地址range,根据TRAMPOLINE偏移得到
pub fn kernel_stack_position(app_id: usize) -> (usize, usize) {
let top = TRAMPOLINE - app_id * (KERNEL_STACK_SIZE + PAGE_SIZE);
|
let bottom = top - KERNEL_STACK_SIZE;
|
(bottom, top)
|
|
}
|
|
通过insert_framed_area将这个stack_bottom, stack_top放入os address space
|
|
初始化tcb
其中task_cx设置为跳转到trap_return不是__restore
|
为了支持对app的启动并顺利切换到app address space执行
|
只是将ra寄存器设置为trap_return地址
|
|
初始化app的TrapContext
在app的address space而不是os的
|
只能手动查页表找到TrapContext实际被放在的ppn
|
再获得在app address space的TrapContext的可变引用,用于初始化
|
|
|
|
|
TaskManager
|
// os/src/task/mod.rs
|
|
|
current_task设置0,从0app开始执行
|
|
提供current_user_token和current_trap_cx
|
获得当前正在执行的app address space token
|
和可以在os address space修改位于该app address space的TrapContext的可变引用
|
|
|
os/build.rs
|
在.incbin不再插入清除全部符号的应用二进制镜像*.bin
|
将ELF格式文件直接链接进来
|
|
链接每个ELF之前.align 3确保8bytes对齐
|
如果不这样做,xmas-elf crate解析ELF进行不对齐内存读写
例如使用ld指令从内存的一个没有对齐到8bytes的address加载一个64bit的值到一个通用reg
|
|
|
|
改进Trap处理
|
TrapContext不再os address space
|
通过调用current_trap_cx获得TrapContext可变引用
|
|
在trap_handler开头调用set_kernel_trap_entry
|
将stvec修改为通同模块下另一个函数trap_from_kernel地址
|
就是说:一旦进入os再次触发到S-Mode的Trap
|
会在硬件设置一些CSR之后跳过寄存器的保存过程
|
直接跳到trap_from_kernel函数
|
直接panic退出
|
|
因为os和app的address space分离后
|
从U trap到S和从S trap到S的处理方式差别大
|
不得不实现两遍不太可能整合两个
|
为了简单直接panic
|
|
trap_handler完成Trap处理,调用trap_return返回app的U-Mode
先调用set_user_trap_entry
stvec::write(TRAMPOLINE as usize, TrapMode::Direct);
|
|
更新stvec为跳板页而不是链接的__alltraps地址
|
因为分页后只能通过跳板页面上的虚拟地址来实际获得__alltraps和__restore
|
|
__restore需要俩参数
TrapContext在app address space中虚拟地址
|
继续执行app address space的token
|
|
找到__restore在os/app address space共同的虚拟地址
let restore_va = __restore as usize - __alltraps as usize + TRAMPOLINE;
|
计算__restore相对于__alltraps的偏移量
|
|
使用jr跳转
|
|
fence.i清空指令缓存i-cache
|
|
os中进行一些操作导致一些原先存放某个app代码的pfn如今用来存放数据或者是其他应用的代码
|
|
i-cache可能保存着该pfn的错误快照
|
|
|
|
|
改进sys_write实现
|
sys_write不再能够直接访问位于app address space中的数据
|
手动查询页表
|
|
page_table提供将app address space中一个缓冲区
|
转化为在os space address中能够直接访问的形式的辅助函数
|
pub fn translated_byte_buffer(
token是某个app address space的token
|
ptr和len,该address space一段缓冲区,起始address和长度
|
以vec形式返回一组在os space address直接访问的bytes slice
|
|
将每个bytes slice转化为&str输出
|
|
|
|
|
|
|
|
|