练习实验书: https://learningos.cn/rCore-Tutorial-Guide-2023A/chapter4/7exercise.html
我的代码: https://github.com/LearningOS/2023a-rcore-ToniXWD
1 编程作业
- 重写
sys_get_time
和sys_task_info
- 实现
mmap
和munmap
两个系统调用,通过所有测例
1.1 重写 sys_get_time
和 sys_task_info
这一部分的实现很简单, 引入虚拟内存和页表后无非就是内核无法直接使用系统调用传递的地址了, 需要将这个用户空间的虚拟地址做一层手动翻译, 这样的方法已经为我们提供了: translated_struct_ptr
, 因此实现很简单:
1 | pub fn sys_get_time(_ts: *mut TimeVal, _tz: usize) -> isize { |
1.2 实现 mmap
和 munmap
mmap
在Linux
中主要用于在内存中映射文件, 本次实验简化它的功能,仅用于申请内存
1.2.1 MemorySet
结构体
由于mmap
要在地址空间中添加新的映射, 因此无非就是在MemorySet
中添加一块映射的内存区域, 先看看每个内存地址空间本来的结构:
1 | /// address space |
其中areas
存放的是本地址空间的多个逻辑段, 每个逻辑段的地址是连续的, 但mmap
是动态申请地址空间的, 多次调用会创建多哥不连续的内存区域, 因此不适合存放在areas
, 因此创建一个新的成员存放:
1 | /// address space |
这里的map_tree
还是RAII
的思想, munmap
调用时, 会将页帧从map_tree
中释放, 触发drop trait
使内存页分配器回收内存页
1.2.2 MemorySet::mmap
&& MemorySet::unmmap
有了上述的数据结构, mmap
时只需要调用frame_alloc
分配一个新的内存页即可:
1 | // os/src/mm/memory_set.rs |
unmmap
则更简单, 从map_tree
中删除即可, 这会自动触发drop trait
:
1 | // os/src/mm/memory_set.rs |
2 简答作业
2.1 请列举 SV39
页表页表项的组成,描述其中的标志位有何作用?
如上图所示, 每个页表项是PPN
+ Flags
组成, PPN
指向下一级页表的物理页地址, Flags
为各种标志位:
- V: 页表项是否有效
- R/W/X: 控制对应页是否具有读、写、执行权限。这些权限位为操作系统实现内存保护提供了机制。
- U: 控制页的访问级别,标明用户空间是否可以访问。
- G: 全局位用于性能优化,它允许某些页表项在多个上下文之间共享,减少TLB刷新。
- A/D: 访问和脏位用于页面替换策略,帮助操作系统确定哪些页面是活跃的,哪些页面可以被换出内存。
- RSW: 保留位, 留给自定义的实现, 常用于实现COW写时复制,用于实现
fork
系统调用时的内存效率优化。
2.2 缺页
缺页指的是进程访问页面时页面不在页表中或在页表中无效的现象,此时 MMU 将会返回一个中断, 告知 os 进程内存访问出了问题。os 选择填补页表并重新执行异常指令或者杀死进程
2.2.1 请问哪些异常可能是缺页导致的?
- 访问了一个没有映射的虚拟地址。
- 访问了映射了虚拟地址但尚未加载到物理内存的页面。
- 访问违反了权限的页面,如对只读页面进行写操作。
2.2.2 发生缺页时,描述相关重要寄存器的值,上次实验描述过的可以简略。
- PC (Program Counter): 保存发生异常指令的地址。
- stval/utval (Store/Trap Value Register): 保存造成异常的虚拟地址。
- satp (Supervisor Address Translation and Protection Register): 包含页表的基址和一些控制位,用于地址转换。
2.2.3 Lazy
缺页有两个常见的原因,其一是 Lazy
策略,也就是直到内存页面被访问才实际进行页表操作。 比如,一个程序被执行时,进程的代码段理论上需要从磁盘加载到内存。但是 os
并不会马上这样做, 而是会保存 .text
段在磁盘的位置信息,在这些代码第一次被执行时才完成从磁盘的加载操作。
这样做有哪些好处?
- 减少了初始化时的加载时间,加快了程序启动速度。
- 仅加载实际使用的内存页面,节省了物理内存资源。
- 避免了不必要的I/O操作,提高了系统整体性能。
2.2.4 估算页表内存消耗
其实,我们的 mmap
也可以采取 Lazy
策略,比如:一个用户进程先后申请了 10G 的内存空间, 然后用了其中 1M 就直接退出了。按照现在的做法,我们显然亏大了,进行了很多没有意义的页表操作。
处理 10G 连续的内存页面,对应的 SV39 页表大致占用多少内存 (估算数量级即可)?
- 每个页表项占用8字节,对于10G内存:
- 一级页表(PTEs):
10G / 4K
(每页大小)=2560K
页表项 =2560K * 8
字节。 - 二级页表(PTEs):
2560K / 512
(每级页表条目数)=5K
页表项 =5K * 8
字节。 - 三级页表(PTEs):
5K / 512
=10
页表项 =10 * 8
字节。 - 总计约为
2560K * 8
字节 加上较小的二级和三级页表大小,数量级在几 M 字节。
2.2.5 实现Lazy
请简单思考如何才能实现 Lazy
策略,缺页时又如何处理?描述合理即可,不需要考虑实现。
- 在进程开始时,只为虚拟地址空间分配页表结构,不分配实际的物理页面。
- 页面第一次被访问时,通过缺页中断处理函数分配物理页面,并更新页表项。
- 缺页处理时,查找磁盘上相应的数据(如果之前已经映射),然后加载到内存中。
2.2.6 Swap
缺页的另一个常见原因是 swap
策略,也就是内存页面可能被换到磁盘上了,导致对应页面失效。
此时页面失效如何表现在页表项(PTE)上?
- 有效位(
V
): 如果页面被换出到磁盘,页表项的有效位会被清除。 - 使用
RSW
位来存储额外的状态信息,区分真正的无效的页表项和被换到磁盘上的页表项
2.3 双页表与单页表
为了防范侧信道攻击,我们的 o
s 使用了双页表。但是传统的设计一直是单页表的,也就是说, 用户线程和对应的内核线程共用同一张页表,只不过内核对应的地址只允许在内核态访问。 (备注:这里的单/双的说法仅为自创的通俗说法,并无这个名词概念,详情见 KPTI
)
在单页表情况下,如何更换页表?
单页表不需要切换, 因为页表已经包含了用户空间和内核空间的映射。但是,当切换进程时,需要更换页表以加载新进程的地址空间。通过更新satp
寄存器来完成,这个寄存器包含了当前活动页表的物理地址。单页表情况下,如何控制用户态无法访问内核页面?
内核页表的页表项不设置U
位即可单页表有何优势?
陷入内核时不需要切换页表, 同时仍然可以用硬件完成地址翻译, 效率更高双页表实现下,何时需要更换页表?假设你写一个单页表操作系统,你会选择何时更换页表(回答合理即可)?
- 双页表实现下,上下文切换和陷入内核都需要切换页表
- 单页表实现下, 切换不同的进程时才需要切换页表