rCore-Tutorial-Book-v3
rCore-Tutorial-Book-v3 copied to clipboard
rCore-Tutorial-Book-v3/chapter4/4sv39-implementation-2
实现 SV39 多级页表机制(下) — rCore-Tutorial-Book-v3 0.1 文档
https://rcore-os.github.io/rCore-Tutorial-Book-v3/chapter4/4sv39-implementation-2.html
您好,我想问问在map和unmap之后为啥没有刷新TLB
@honeyhhhh 由于应用和内核在不同的地址空间下,我们无需在每一次map/unmap之后都立即刷新TLB,只需在所有的操作结束后,即将切换回应用地址空间之前刷新一次TLB即可,这可以参考__restore
的实现。这样做是由于刷新TLB是一个十分耗时的操作,需要尽可能避免不必要的刷新。
您好,我觉得应该发现了一个框架代码中的逻辑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不通过的问题。所以我觉得它可能是有一点问题的。
页表基本数据结构与访问接口一节的第一行到第二行的位置:
因此 PageTable``要保存它根节点的物理页号 ``root_ppn 作为页表唯一的区分标志
这里估计多写了一个"`"符号导致了高亮错乱
但Bootloader把操作系统内核加载到物理内存中后,物理内存上已经有一部分用于放置内核的代码和数据。
但->当
函数 find_pte 和 find_pte_create 中的 pte 合法性检查放在 i==2 检查前会不会更严谨
我感觉 PageTable 这个结构体的 frames 段可以直接设计成 BTreeMap 映射。因为这样就不需要在 MemoryArea 中再设计 BTreeMap 映射了,内聚性会好一点。
函数 find_pte 和 find_pte_create 中的 pte 合法性检查放在 i==2 检查前会不会更严谨
我们的PageTable仅负责从VPN查到页表项的位置,但是并不要求这个页表项必须合法,这个检查工作应该由find_pte
的调用者完成。
我感觉 PageTable 这个结构体的 frames 段可以直接设计成 BTreeMap 映射。因为这样就不需要在 MemoryArea 中再设计 BTreeMap 映射了,内聚性会好一点。
看起来是个更好的设计。
上一节更多的是站在硬件的角度来分析SV39多级页表的硬件机制,本节我们主要讲解基于 SV39 多级页表机制的操作系统内存管理。这还需进一步管理计算机系统中当前已经使用是或空闲的物理页帧,
这还需进一步管理计算机系统中当前已经使用"是或"空闲的物理页帧 ---这里应该是“或是”
回收掉 FRAME_ALLOCATOR 中: 改成 回收到
这里我们只需为
FrameTracker
实现Drop
Trait 即可。当一个FrameTracker
实例被回收的时候,它的drop
方法会自动被编译器调用,通过之前实现的frame_dealloc
我们就将它控制的物理页帧回收以供后续使用了。
对于“它的 drop
方法会自动被编译器调用”这句表述是不是不太恰当,调用 drop
方法是在运行时进行的,此时显然和编译器没有什么关系。
比如在某个函数中使用了一个 FrameTracker
的实例 frame_tracker
,编译器只是在编译的时候在此函数的末尾自动的插入core::mem::drop(frame_tracker);
这一函数调用语句,应用运行期间才会进行 drop
方法的真正调用。
从 find_pte 的实现还可以看出,即使找到的页表项不合法,还是会将其返回回去而不是返回 None 。这说明在目前的实现中,页表和页表项是相对解耦合的。
这一段为什么说find_pte不返回None?笔误?和代码、前面的内容也对不上
PageTable::find_pte 与 find_pte_create 的不同在于当找不到合法叶子节点的时候不会新建叶子节点而是直接返回 None 即查找失败。
@longguzzz 这里主要在说find_pte
的这段逻辑:
if i == 2 {
result = Some(pte);
break;
}
这里在if
里面并没有再判断pte
是否合法,而是将pte
直接包裹起来返回。所以find_pte
可能返回一个不合法(即标志位V
为0)的页表项。注意叶子节点和页表项并不是一个概念:叶子节点指的是页表树结构的叶子,它包含512个页表项。
当然,这里的描述可能还有些混乱,稍后有空想想如何修改。
@longguzzz 这里主要在说
find_pte
的这段逻辑:if i == 2 { result = Some(pte); break; }
这里在
if
里面并没有再判断pte
是否合法,而是将pte
直接包裹起来返回。所以find_pte
可能返回一个不合法(即标志位V
为0)的页表项。注意叶子节点和页表项并不是一个概念:叶子节点指的是页表树结构的叶子,它包含512个页表项。当然,这里的描述可能还有些混乱,稍后有空想想如何修改。
看代码其实很清晰明确。而且树算法还可以从递归角度想初始条件、转移规则、递归出口,也很清楚。
看了后几章,回过头来想,可能必须要用递归的方式来理解内存寻址,才能理解清楚。 (但也只是个人观点,不知是否正确)
因为用递归的方式思考,更容易发现逻辑上潜在的“访虚址->需页表->在内存中->访虚址”递归链。所以,虽然可能实现算法并不需要用递归,但是为了跳出“虚址<->页表”的逻辑循环思考,可能有必要用递归的方式重新描述一遍算法。
比如这个问题:开启分页机制后,不考虑TLB,内核访问应用数据,通过页表需要几次访问物理内存?(比如sys_waitpid
里用到translated_refmut
,从而在内核里为用户进程传进来的exit_code_ptr
保存其子进程的exit_code
,translated_refmut
调用translate_va
,translate_va
调用find_pte
。从调用translated_refmut
,到把exit_code
存到内存里,这样的过程要访问多少次物理内存?是4次,还是(3+1)*(3+1)=16次?)
用sys_waitpid
里的translated_refmut
举例,(SV39)MMU物理访存是16次的理由如下:
<1>对于通常的访存问题,未开启分页时访问物理内存只需要1次
<2>对于通常的访存问题,开启分页时只需要4次(但从递归的角度理解)。
-
find_pte
里的ppn.get_pte_array()[idxs[i]];
的部分,是把ppn
位移成pa
,然后由core::slice::from_raw_parts_mut
解释成512个PTE的slice起始地址。之后获取PTE数据要访问内存某地址取数据。 - 获取PTE数据要内存寻址,访问内存地址就要考虑是虚拟内存访问,还是物理内存访问。由于访问虚址就要再找内存里的页表,页表也需要内存地址来确定的,又需要考虑是虚拟地址还是物理地址。所以这里有个递归结构。
- 递归出口是硬件MMU物理访址。
- SV39访问虚拟地址,之所以可以3+1次完成取数据,是因为MMU直接从
satp
寄存器出发,直接来3次访问物理页表,再来1次拿数据。
<3>sys_waitpid
里translated_refmut
的find_pte
,也是访问“虚拟内存”,但不是4次访问物理内存,而是16次。如果从递归的角度理解寻址,会更好理解。
- 内存寻址是一个递归概念。如果递归过程发生改变,则访问物理内存的次数也会发生改变。
-
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次物理访存。 - 最后在内核空间查到了真实物理内存上的
exit_code
存放位置,但是依旧需要再来4次物理内存访问(恒等映射)把exit_code
数据放进去。 - 合计3*4+4=16次
所以,从这个问题出发,细节地阐述一下个人为什么认为“虚拟内存这个概念不用递归的方式就很难说得清晰”。
- 比如,要访问地址,在字典树上找PTE过程的层数是一个常数吗?
如果把虚拟内存访问理解成在树上找PTE,最后再从物理地址取数据,那么在这里想直观理解为什么是16次访存将会比较困难。因为按照代码实现里直观的算法描述,访问虚拟内存,找PTE过程的层数就应该是一个常数。那么为什么“原先的叶结点”为什么又“长出了”新的子树?为什么树高会变化呢?如果按照递归的方式来理解就比较清楚,因为按照递归的方式理解只有一个出口:MMU物理访存。而其他的访存都是递归过程。所以,从递归的角度来观察,在
ppn.get_pte_array()[idxs[i]];
的部分其实隐藏了一个递归调用(“[idxs[i]]去访存,即递归过程”),而这是不用递归来描述所会掩盖的事。 - 比如,在
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
寄存器现在是什么状态。 - 比如,
PageTable::from_token
的形式参数名称satp
是什么语义? 这里也有某种两重语义,satp
可能来自satp
寄存器,也可能来自内存某个地址上保存的satp
值。比如sys_waitpid
里调用find_pte
,用的就是用户进程地址空间的token
。(但satp
这样的形参名字,单独在PageTable里看的时候,如果不清楚可能会有sys_waitpid
这样的调用场景,那就可能会感觉是在暗示它来自satp
寄存器。)所以或许将PageTable::from_token的形式参数名改为token会更合适?然后可以补充说明,该token可以传satp寄存器的值。 - 比如,
find_pte
是什么语义? find_pte本身有双重语义,在分页机制开启之前,它就代表分页机制开启后的MMU寻址过程。但是在分页机制开启之后,这个语义就变化了,它就变得只是普通的“找pte”算法:因为里面涉及到的访存还要再嵌套MMU寻址过程。 造成这种双重语义的另一层因素,可能是root_ppn
这个字段的名字问题。如果root_ppn
直接取自satp寄存器,两者语义重合,那么正是MMU直接3+1访问。但内核访问用户数据时,satp
与用户root_ppn
是两个值,所以就必须要区分出这两种情况没,这时候root_ppn名字暗示的含义就可能导致困惑。(所以可能root_ppn
叫做类似token
之类的变量名会更合适。)
为什么在StackFrameAllocator
的alloc
实现中还需要Some((self.current - 1).into())
?我的理解这里的current
已经就是PhysPageNum
了吧,还是说Rust不支持这样的类型转换?
我的理解这里的current已经就是PhysPageNum了吧,还是说Rust不支持这样的类型转换? 看一下数据结构,current是usize类型
勘误:
在分页机制开启前,这样做自然成立;而开启之后,虽然裸指针被视为一个虚拟地址,但是上面已经提到,基于恒等映射,虚拟地址会映射到一个相同的物理地址,因此在也是成立的
少了一个字,应为“因此现在”
请问一下如何建立“恒等”映射啊?比如如何使用虚拟地址0x80400000访问物理地址0x80400000啊,虚拟地址应该要经过三次页表项的访问才能拿到物理地址,但是页表项本身又怎么建立起来的呢,如何在虚拟地址寻址的情况下,维护页表本身啊
请问一下如何建立“恒等”映射啊?比如如何使用虚拟地址0x80400000访问物理地址0x80400000啊,虚拟地址应该要经过三次页表项的访问才能拿到物理地址,但是页表项本身又怎么建立起来的呢,如何在虚拟地址寻址的情况下,维护页表本身啊
经过分析大概明白了,感觉这块还是比较复杂的,它是在物理寻址的时候建立“恒等”映射,而不是开启虚拟地址寻址后再建立“恒等”映射,在物理寻址期间,会根据MapArea的映射需求建立一个三级页表,页表本身存放到ekernel~MEMORY_END之间,核心是:比如要建立地址ppn的恒等映射,它会先把ppn按照虚拟地址的格式解析,按照类似字典树的方式建立一个三级页表,并在最后一级页表(叶节点)的页表项的物理地址字段写入ppn,即写入本次寻址的地址本身,这样就建立好了一个“恒等”映射。
是否应该将解析设备树相关的内容,从 rustsbi-qemu 移动到这个学习文档中来。 进一步,应该直接基于 rustsbi 来构建 os ,而不是基于 rustsbi-qemu 。理论上 rustsbi、opensbi 是可以相互替换的。