rcore-os / rCore-Tutorial-Book-v3

A book about how to write OS kernels in Rust easily.
https://rcore-os.github.io/rCore-Tutorial-Book-v3/
GNU General Public License v3.0
1.16k stars 218 forks source link

rCore-Tutorial-Book-v3/chapter4/4sv39-implementation-2 #69

Open utterances-bot opened 3 years ago

utterances-bot commented 3 years ago

实现 SV39 多级页表机制(下) — rCore-Tutorial-Book-v3 0.1 文档

https://rcore-os.github.io/rCore-Tutorial-Book-v3/chapter4/4sv39-implementation-2.html

honeyhhhh commented 3 years ago

您好,我想问问在map和unmap之后为啥没有刷新TLB

wyfcyx commented 3 years ago

@honeyhhhh 由于应用和内核在不同的地址空间下,我们无需在每一次map/unmap之后都立即刷新TLB,只需在所有的操作结束后,即将切换回应用地址空间之前刷新一次TLB即可,这可以参考__restore的实现。这样做是由于刷新TLB是一个十分耗时的操作,需要尽可能避免不必要的刷新。

fadedzipper commented 3 years ago

您好,我觉得应该发现了一个框架代码中的逻辑bug。 find_pte_create()中: for i in 0..3 { 23 let pte = &mut ppn.get_pte_array()[idxs[i]]; 24 if i == 2 { 25 result = Some(pte); 26 break; 27 } 28 if !pte.is_valid() { 29 let frame = frame_alloc().unwrap(); 30 *pte = PageTableEntry::new(frame.ppn, PTEFlags::V); 31 self.frames.push(frame); 32 } 33 ppn = pte.ppn(); 34 } 判断页表项是否合法的逻辑在判断是否等于2之前,是比较合理的。因为三级页表实际上映射到物理页帧是在MapArea中。在这个函数结束时,已经分配了三级页表的物理页帧。 但在框架代码 find_pte()中: for i in 0..3 { 16 let pte = &ppn.get_pte_array()[idxs[i]]; 17 if i == 2 { 18 result = Some(pte); 19 break; 20 } 21 if !pte.is_valid() { 22 return None; 23 } 24 ppn = pte.ppn(); 25 } 我觉得应该把判断页表项是否合法的逻辑放在判断等于2之前。 这样的话第三级页表是否合法也在检测范围中。 而我确实因为这个改动解决了测试ch2t_write0不通过的问题。所以我觉得它可能是有一点问题的。

hhhxiao commented 2 years ago

页表基本数据结构与访问接口一节的第一行到第二行的位置:

因此 PageTable``要保存它根节点的物理页号 ``root_ppn 作为页表唯一的区分标志

这里估计多写了一个"`"符号导致了高亮错乱

liangyongrui commented 2 years ago
但Bootloader把操作系统内核加载到物理内存中后,物理内存上已经有一部分用于放置内核的代码和数据。

但->当

wei-huan commented 2 years ago

函数 find_pte 和 find_pte_create 中的 pte 合法性检查放在 i==2 检查前会不会更严谨

wei-huan commented 2 years ago

我感觉 PageTable 这个结构体的 frames 段可以直接设计成 BTreeMap 映射。因为这样就不需要在 MemoryArea 中再设计 BTreeMap 映射了,内聚性会好一点。

wyfcyx commented 2 years ago

函数 find_pte 和 find_pte_create 中的 pte 合法性检查放在 i==2 检查前会不会更严谨

我们的PageTable仅负责从VPN查到页表项的位置,但是并不要求这个页表项必须合法,这个检查工作应该由find_pte的调用者完成。

wyfcyx commented 2 years ago

我感觉 PageTable 这个结构体的 frames 段可以直接设计成 BTreeMap 映射。因为这样就不需要在 MemoryArea 中再设计 BTreeMap 映射了,内聚性会好一点。

看起来是个更好的设计。

workerwork commented 2 years ago

上一节更多的是站在硬件的角度来分析SV39多级页表的硬件机制,本节我们主要讲解基于 SV39 多级页表机制的操作系统内存管理。这还需进一步管理计算机系统中当前已经使用是或空闲的物理页帧,

这还需进一步管理计算机系统中当前已经使用"是或"空闲的物理页帧 ---这里应该是“或是”

dzwduan commented 2 years ago

回收掉 FRAME_ALLOCATOR 中: 改成 回收到

gfgafn commented 1 year ago

分配/回收物理页帧的接口

这里我们只需为 FrameTracker 实现 Drop Trait 即可。当一个 FrameTracker 实例被回收的时候,它的 drop 方法会自动被编译器调用,通过之前实现的 frame_dealloc 我们就将它控制的物理页帧回收以供后续使用了。

对于“它的 drop 方法会自动被编译器调用”这句表述是不是不太恰当,调用 drop 方法是在运行时进行的,此时显然和编译器没有什么关系。 比如在某个函数中使用了一个 FrameTracker 的实例 frame_tracker,编译器只是在编译的时候在此函数的末尾自动的插入core::mem::drop(frame_tracker);这一函数调用语句,应用运行期间才会进行 drop 方法的真正调用。

longguzzz commented 1 year ago

从 find_pte 的实现还可以看出,即使找到的页表项不合法,还是会将其返回回去而不是返回 None 。这说明在目前的实现中,页表和页表项是相对解耦合的。

这一段为什么说find_pte不返回None?笔误?和代码、前面的内容也对不上

PageTable::find_pte 与 find_pte_create 的不同在于当找不到合法叶子节点的时候不会新建叶子节点而是直接返回 None 即查找失败。

wyfcyx commented 1 year ago

@longguzzz 这里主要在说find_pte的这段逻辑:

if i == 2 {
    result = Some(pte);
    break;
}

这里在if里面并没有再判断pte是否合法,而是将pte直接包裹起来返回。所以find_pte可能返回一个不合法(即标志位V为0)的页表项。注意叶子节点和页表项并不是一个概念:叶子节点指的是页表树结构的叶子,它包含512个页表项。

当然,这里的描述可能还有些混乱,稍后有空想想如何修改。

longguzzz commented 1 year ago

@longguzzz 这里主要在说find_pte的这段逻辑:

if i == 2 {
    result = Some(pte);
    break;
}

这里在if里面并没有再判断pte是否合法,而是将pte直接包裹起来返回。所以find_pte可能返回一个不合法(即标志位V为0)的页表项。注意叶子节点和页表项并不是一个概念:叶子节点指的是页表树结构的叶子,它包含512个页表项。

当然,这里的描述可能还有些混乱,稍后有空想想如何修改。

看代码其实很清晰明确。而且树算法还可以从递归角度想初始条件、转移规则、递归出口,也很清楚。

longguzzz commented 1 year ago

看了后几章,回过头来想,可能必须要用递归的方式来理解内存寻址,才能理解清楚。 (但也只是个人观点,不知是否正确)

因为用递归的方式思考,更容易发现逻辑上潜在的“访虚址->需页表->在内存中->访虚址”递归链。所以,虽然可能实现算法并不需要用递归,但是为了跳出“虚址<->页表”的逻辑循环思考,可能有必要用递归的方式重新描述一遍算法。

比如这个问题:开启分页机制后,不考虑TLB,内核访问应用数据,通过页表需要几次访问物理内存?(比如sys_waitpid里用到translated_refmut,从而在内核里为用户进程传进来的exit_code_ptr保存其子进程的exit_codetranslated_refmut调用translate_vatranslate_va调用find_pte。从调用translated_refmut,到把exit_code存到内存里,这样的过程要访问多少次物理内存?是4次,还是(3+1)*(3+1)=16次?)

sys_waitpid里的translated_refmut举例,(SV39)MMU物理访存是16次的理由如下:

<1>对于通常的访存问题,未开启分页时访问物理内存只需要1次 <2>对于通常的访存问题,开启分页时只需要4次(但从递归的角度理解)。 1. `find_pte`里的`ppn.get_pte_array()[idxs[i]];`的部分,是把`ppn`位移成`pa`,然后由`core::slice::from_raw_parts_mut`解释成512个PTE的slice起始地址。之后获取PTE数据要访问内存某地址取数据。 2. 获取PTE数据要内存寻址,访问内存地址就要考虑是虚拟内存访问,还是物理内存访问。由于访问虚址就要再找内存里的页表,页表也需要内存地址来确定的,又需要考虑是虚拟地址还是物理地址。所以这里有个递归结构。 3. 递归出口是硬件MMU物理访址。 4. SV39访问虚拟地址,之所以可以3+1次完成取数据,是因为MMU直接从`satp`寄存器出发,直接来3次访问物理页表,再来1次拿数据。 <3>`sys_waitpid`里`translated_refmut`的`find_pte`,也是访问“虚拟内存”,但不是4次访问物理内存,而是16次。如果从递归的角度理解寻址,会更好理解。 1. 内存寻址是一个递归概念。如果递归过程发生改变,则访问物理内存的次数也会发生改变。 2. `sys_waitpid`借助`translated_refmut`保存`exit_code`,最后在`ppn.get_pte_array()[idxs[i]]`的部分,不是访问物理地址,而是访问虚拟地址。因为`satp`要代表内核空间,不能直接切换`satp`让MMU进行4次物理访问。所以访问这个用户虚拟地址就得通过`find_pte`的算法来寻址。而这个过程中,每一次`ppn.get_pte_array()[idxs[i]]`需要4次物理访存。 3. 最后在内核空间查到了真实物理内存上的`exit_code`存放位置,但是依旧需要再来4次物理内存访问(恒等映射)把`exit_code`数据放进去。 4. 合计3*4+4=16次 所以,从这个问题出发,细节地阐述一下个人为什么认为“虚拟内存这个概念不用递归的方式就很难说得清晰”。 1. 比如,**要访问地址,在字典树上找PTE过程的层数是一个常数吗?** 如果把虚拟内存访问理解成在树上找PTE,最后再从物理地址取数据,那么在这里想直观理解为什么是16次访存将会比较困难。因为按照代码实现里直观的算法描述,访问虚拟内存,找PTE过程的层数就应该是一个常数。那么为什么“原先的叶结点”为什么又“长出了”新的子树?为什么树高会变化呢?如果按照递归的方式来理解就比较清楚,因为按照递归的方式理解只有一个出口:MMU物理访存。而其他的访存都是递归过程。所以,从递归的角度来观察,在`ppn.get_pte_array()[idxs[i]];`的部分其实隐藏了一个递归调用(“[idxs[i]]去访存,即递归过程”),而这是不用递归来描述所会掩盖的事。 2. 比如,**在`sys_waitpid`中,最后用到`ppn.get_pte_array() [idxs[i]];`的访存过程中,ppn是什么语义?** PTE地址解释起来是ppn结合idx索引,在字面上有ppn。但实际上如果开启了页表的话,还要由MMU走几趟页表,这和字面上physcial page number的直观含义不同,容易导致困惑。 所以,ppn其实有两重语义,一重是在分页开启前为内核建立地址空间的场景中,ppn即真实的physcial page number,另一重则是像开启分页之后的`sys_waitpid`调用里内核去访问用户数据,此时的ppn只代表地址空间中的某个抽象地址,而内核自己也要MMU间接访问。 如果从递归的角度想,问“递归出口是什么?”,最终落脚点在硬件MMU,那么这里的“ppn”可能用"page_number"这样的名字会更好。因为page_number可以是ppn,也可以不是,总归落脚点在于MMU怎么访问,也即`satp`寄存器现在是什么状态。 3. 比如,**`PageTable::from_token`的形式参数名称`satp`是什么语义?** 这里也有某种两重语义,`satp`可能来自`satp`寄存器,也可能来自内存某个地址上保存的`satp`值。比如`sys_waitpid`里调用`find_pte`,用的就是用户进程地址空间的`token`。(但`satp`这样的形参名字,单独在PageTable里看的时候,如果不清楚可能会有`sys_waitpid`这样的调用场景,那就可能会感觉是在暗示它来自`satp`寄存器。)所以或许将PageTable::from_token的形式参数名改为token会更合适?然后可以补充说明,该token可以传satp寄存器的值。 4. 比如,**`find_pte`是什么语义?** find_pte本身有双重语义,在分页机制开启之前,它就代表分页机制开启后的MMU寻址过程。但是在分页机制开启之后,这个语义就变化了,它就变得只是普通的“找pte”算法:因为里面涉及到的访存还要再嵌套MMU寻址过程。 造成这种双重语义的另一层因素,可能是`root_ppn`这个字段的名字问题。如果`root_ppn`直接取自satp寄存器,两者语义重合,那么正是MMU直接3+1访问。但内核访问用户数据时,`satp`与用户`root_ppn`是两个值,所以就必须要区分出这两种情况没,这时候root_ppn名字暗示的含义就可能导致困惑。(所以可能`root_ppn`叫做类似`token`之类的变量名会更合适。)
xuanz20 commented 1 year ago

为什么在StackFrameAllocatoralloc实现中还需要Some((self.current - 1).into())?我的理解这里的current已经就是PhysPageNum了吧,还是说Rust不支持这样的类型转换?

Unik-lif commented 1 year ago

我的理解这里的current已经就是PhysPageNum了吧,还是说Rust不支持这样的类型转换? 看一下数据结构,current是usize类型

chestNutLsj commented 12 months ago

勘误:

在分页机制开启前,这样做自然成立;而开启之后,虽然裸指针被视为一个虚拟地址,但是上面已经提到,基于恒等映射,虚拟地址会映射到一个相同的物理地址,因此在也是成立的

少了一个字,应为“因此现在”

liaojiawei666 commented 9 months ago

请问一下如何建立“恒等”映射啊?比如如何使用虚拟地址0x80400000访问物理地址0x80400000啊,虚拟地址应该要经过三次页表项的访问才能拿到物理地址,但是页表项本身又怎么建立起来的呢,如何在虚拟地址寻址的情况下,维护页表本身啊

liaojiawei666 commented 9 months ago

请问一下如何建立“恒等”映射啊?比如如何使用虚拟地址0x80400000访问物理地址0x80400000啊,虚拟地址应该要经过三次页表项的访问才能拿到物理地址,但是页表项本身又怎么建立起来的呢,如何在虚拟地址寻址的情况下,维护页表本身啊

经过分析大概明白了,感觉这块还是比较复杂的,它是在物理寻址的时候建立“恒等”映射,而不是开启虚拟地址寻址后再建立“恒等”映射,在物理寻址期间,会根据MapArea的映射需求建立一个三级页表,页表本身存放到ekernel~MEMORY_END之间,核心是:比如要建立地址ppn的恒等映射,它会先把ppn按照虚拟地址的格式解析,按照类似字典树的方式建立一个三级页表,并在最后一级页表(叶节点)的页表项的物理地址字段写入ppn,即写入本次寻址的地址本身,这样就建立好了一个“恒等”映射。

sxci commented 9 months ago

是否应该将解析设备树相关的内容,从 rustsbi-qemu 移动到这个学习文档中来。 进一步,应该直接基于 rustsbi 来构建 os ,而不是基于 rustsbi-qemu 。理论上 rustsbi、opensbi 是可以相互替换的。

WGoodLive commented 1 month ago

学完这节,我一直没办法理解,是如何实现的恒等映射,我认为是随机映射(任意一个虚拟地址,返回一个目前首先找到的具有随机性的物理页面),我的理解如下:

fn find_pte_create(&mut self,vpn:VirtPageNum) -> Option<&mut PageTableEntry>{
...        
let frame = frame_alloc().unwrap();
*pte = PageTableEntry::new(frame.ppn, PTEFlags::V);
self.frames.push(frame);
...
}

在这里,没有实现映射的虚拟地址会调用frame_alloc()函数进行分配页面,frame_alloc()函数会调用alloc()函数,如下:

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())
        }
    }
}

alloc会找到一个没有被使用的的物理页面,返回回去。 通过上面的步骤,我没发现一个操作,能使虚拟地址得到的就是其相对应的物理页号的页面,他得到的首先是栈(recycled)里回收的页面,如果栈空,就重新申请新页面。

WGoodLive commented 1 month ago

额,这章没有实现恒等映射,他是下一章通过PhsyPageNum(vpn.0)直接访问虚拟页号,然后建立的恒等映射

wyfcyx commented 1 month ago

@WGoodLive find_pte_createframe_alloc分配出来的物理页帧并不是恒等映射的目标物理页帧,而只是用作多级页表的中间节点。find_pte_create的功能是根据虚拟地址找到一个最终的页表项,然后在页表项里填入与虚拟地址要恒等映射到的物理地址的物理页号以及权限位。因此建立恒等映射的关键一步是最后填页表项,即PageTable::map函数中的*pte = PageTableEntry::new(ppn, flags | PTEFlags::V);这一句。