如何实现 SV39 多级页表机制? |
就像给房子建楼层一样,我们需要在内核中搭建 RV64 架构的 SV39 分页系统。 |
分步骤来,先打地基(准备环境),再一层一层往上建。 |
我们就有了一个稳固的多层建筑,即实现了 SV39 分页机制。 |
虚拟地址和物理地址的区别是什么? |
访问内存时,就像找人,得知道是在虚拟世界还是现实世界找。 |
修改 satp 这个特殊的开关,告诉系统我们要在哪个世界里玩。 |
系统就能正确地把虚拟世界的地址翻译成现实世界的地址了。 |
如何启用分页模式? |
默认情况下,MMU(内存管理单元)这个大管家没上班,我们需要叫它起来工作。 |
把 satp 的 MODE 字段设置为 8,就像是给大管家发了个开工通知。 |
这样,大管家就正式上岗,开始处理分页事务了。 |
如何定义地址和页号的类型? |
在编程的世界里,给东西起名字很重要,尤其是地址和页号这种关键角色。 |
用 Rust 语言定义 PhysAddr 、VirtAddr 、PhysPageNum 和 VirtPageNum 四个小伙伴。 |
这样每个小伙伴都有了自己的身份证明,方便管理。 |
如何实现地址和页号之间的转换? |
地址和页号之间需要相互认识,好比是不同语言的人交流。 |
为它们实现 From 和 Into 特性,还有 floor 和 ceil 方法,就像教它们一门通用语言。 |
这样,地址和页号就可以无障碍沟通了。 |
如何定义页表项的数据结构? |
页表项是页表里的小兵,要有自己的样子。 |
使用 bitflags 工具箱来定义 PTEFlags 和 PageTableEntry ,给小兵们穿上制服。 |
这样,每个小兵都有了独特的身份标识。 |
如何生成页表项? |
小兵们需要有出生证,证明它们是合法的。 |
为 PageTableEntry 添加 new 和 empty 方法,就像是给小兵们颁发出生证。 |
这样,小兵们就有了合法的身份。 |
如何检查页表项的标志位? |
小兵们的任务和能力需要定期检查。 |
为 PageTableEntry 添加 is_valid 方法,就像是对小兵们的能力进行审查。 |
这样,我们就能确保每个小兵都在正确地执行任务。 |
如何管理物理页帧? |
物理页帧就像是仓库里的货物,需要有人管理。 |
实现 StackFrameAllocator ,就像请了一个仓库管理员。 |
这样,货物的进出就有条不紊了。 |
如何确定可用物理内存范围? |
仓库有多大,哪些地方可以存放货物,这些都是需要事先规划好的。 |
在链接脚本 os/src/linker.ld 里用 ekernel 符号标出内核数据的终点。 |
这样,我们就明确了哪些地方是可以用来存放货物的。 |
如何定义物理页帧管理器的 Trait? |
为了规范仓库管理员的行为,需要有一套标准。 |
定义 FrameAllocator Trait,就像是制定了仓库管理的标准流程。 |
这样,不管谁来当仓库管理员,都能按规矩办事。 |
如何实现 StackFrameAllocator ? |
有了标准,还需要有一个具体的实施者。 |
实现 StackFrameAllocator 结构体及相关方法,就像是选定了一个具体的仓库管理员。 |
这样,货物的管理就有了实际的操作者。 |
如何初始化 StackFrameAllocator ? |
仓库管理员上任前,需要了解仓库的情况。 |
为 StackFrameAllocator 实现 new 和 init 方法,就像是管理员做入职培训。 |
这样,管理员就能快速进入状态,开始工作。 |
如何实现物理页帧的分配? |
仓库管理员要懂得如何合理分配货物。 |
为 StackFrameAllocator 实现 alloc 方法,就像是训练管理员如何发放货物。 |
这样,货物的分配就更加合理有效了。 |
如何实现物理页帧的回收? |
当货物不再需要时,要能够及时回收,以便再次利用。 |
为 StackFrameAllocator 实现 dealloc 方法,就像是教管理员如何回收货物。 |
这样,仓库的空间利用率就提高了。 |
如何创建物理页帧的全局实例? |
仓库需要有一个总管,随时调用资源。 |
创建 FRAME_ALLOCATOR 全局实例,并完成初始化。 |
这样,任何地方都可以调用总管来获取或归还资源了。 |
如何实现物理页帧分配/回收的接口? |
总管需要提供简单直接的服务窗口。 |
实现 frame_alloc 和 frame_dealloc 函数,就像是开设了服务窗口。 |
这样,用户就能很方便地请求或归还资源了。 |
如何实现 FrameTracker ? |
为了确保货物的安全,需要有个追踪系统。 |
实现 FrameTracker 结构体及相关方法,就像是建立了一套货物追踪系统。 |
这样,货物的状态就能实时监控了。 |
如何实现 FrameTracker 的自动回收? |
当货物不再使用时,应该能够自动归还仓库。 |
为 FrameTracker 实现 Drop 特性,就像是给货物装上了自动归还装置。 |
这样,货物就能自动回到仓库,减少了人为操作。 |
如何实现多级页表的基本数据结构? |
多级页表就像是一栋多层楼的大厦,需要设计好每一层的布局。 |
定义 PageTable 结构体及相关方法,就像是设计好了大厦的蓝图。 |
这样,大厦的建设就有了依据。 |
如何初始化多级页表? |
大厦建设前,需要打好地基。 |
为 PageTable 实现 new 方法,就像是为大厦打好地基。 |
这样,大厦的建设就有了坚实的基础。 |
如何实现多级页表的映射和解除映射? |
大厦里的房间需要能够灵活地分配和收回。 |
为 PageTable 实现 map 和 unmap 方法,就像是制定了房间分配和收回的规则。 |
这样,房间的管理就更加灵活高效了。 |
如何在多级页表中查找页表项? |
在多层楼的大厦里找到特定的房间,需要有一张详细的楼层图。 |
为 PageTable 实现 find_pte_create 方法,就像是制作了一份详细的楼层指南。 |
这样,就能快速准确地找到目标房间了。 |
如何访问物理页帧? |
要想进入仓库取货,需要有专门的通道。 |
为 PhysPageNum 实现 get_pte_array 、get_bytes_array 和 get_mut 方法,就像是设置了专用的取货通道。 |
这样,取货就更加便捷了。 |
如何实现恒等映射? |
有时候,最简单的办法就是最好的。 |
使用恒等映射,就像是让虚拟页号和物理页号一一对应。 |
这样,系统就能以最简单的方式工作了。 |
如何实现手动查页表的方法? |
有时候,我们需要绕过系统,直接从底层获取信息。 |
为 PageTable 实现 from_token 、find_pte 和 translate 方法,就像是掌握了直达底层的密道。 |
这样,即使没有系统的帮助,也能快速获取所需的信息。 |
如何实现地址空间的抽象? |
地址空间就像是一个大容器,里面装着很多小容器。 |
定义 MapArea 和 MemorySet 结构体及方法,就像是设计了一个可以装很多小容器的大箱子。 |
这样,地址空间的管理就更加有序了。 |
如何描述逻辑段? |
每个小容器都有自己的用途,需要清楚地标记出来。 |
定义 MapArea 结构体,就像是给每个小容器贴上了标签。 |
这样,每个小容器的用途就一目了然了。 |
如何实现逻辑段的映射方式? |
不同的小容器可能有不同的打开方式。 |
定义 MapType 枚举,就像是给每种小容器设定了打开方式。 |
这样,无论哪种小容器,都能轻松打开。 |
如何实现逻辑段的权限控制? |
每个小容器都有自己的门锁,需要控制谁能开锁。 |
定义 MapPermission 结构体,就像是给每个小容器安装了智能门锁。 |
这样,只有被授权的人才能打开小容器。 |
如何实现地址空间的管理? |
管理一个大容器,需要有一个好的计划。 |
定义 MemorySet 结构体及其方法,就像是制定了一份管理大容器的计划书。 |
这样,大容器内的所有小容器就能得到有效的管理了。 |
如何初始化地址空间? |
需要初始化地址空间。 |
实现 MemorySet::new_bare 方法。 |
确保了地址空间的正确初始化。 |
如何在地址空间中插入逻辑段? |
需要在地址空间中插入新的逻辑段。 |
实现 MemorySet::push 方法。 |
确保了逻辑段的正确插入。 |
如何插入 Framed 方式的逻辑段? |
需要在地址空间中插入 Framed 方式的逻辑段。 |
实现 MemorySet::insert_framed_area 方法。 |
确保了 Framed 方式逻辑段的正确插入。 |
如何生成内核的地址空间? |
需要生成内核的地址空间。 |
实现 MemorySet::new_kernel 方法。 |
确保了内核地址空间的生成。 |
如何从 ELF 文件生成应用的地址空间? |
需要从 ELF 文件生成应用的地址空间。 |
实现 MemorySet::from_elf 方法。 |
确保了应用地址空间的生成。 |
如何实现逻辑段的映射? |
需要实现逻辑段的映射。 |
实现 MapArea::map 方法。 |
确保了逻辑段的正确映射。 |
如何实现逻辑段的解映射? |
需要实现逻辑段的解映射。 |
实现 MapArea::unmap 方法。 |
确保了逻辑段的正确解映射。 |
如何实现逻辑段的数据拷贝? |
需要实现逻辑段的数据拷贝。 |
实现 MapArea::copy_data 方法。 |
确保了逻辑段的数据拷贝。 |
如何实现单个虚拟页面的映射? |
需要实现单个虚拟页面的映射。 |
实现 MapArea::map_one 方法。 |
确保了单个虚拟页面的正确映射。 |
如何实现单个虚拟页面的解映射? |
需要实现单个虚拟页面的解映射。 |
实现 MapArea::unmap_one 方法。 |
确保了单个虚拟页面的正确解映射。 |
如何实现内核地址空间? |
需要实现内核地址空间。 |
定义 MemorySet::new_kernel 方法。 |
确保了内核地址空间的创建。 |
如何映射内核的跳板? |
需要映射内核的跳板。 |
实现 MemorySet::map_trampoline 方法。 |
确保了跳板的正确映射。 |
如何映射内核的各个逻辑段? |
需要映射内核的各个逻辑段。 |
在 MemorySet::new_kernel 方法中调用 push 方法。 |
确保了各个逻辑段的正确映射。 |
如何处理内核栈? |
需要处理内核栈。 |
在 MemorySet::new_kernel 方法中设置内核栈。 |
确保了内核栈的正确设置。 |
如何设置保护页面? |
需要设置保护页面。 |
在内核栈之间预留一个保护页面。 |
确保了内核栈的安全性。 |
如何实现恒等映射? |
需要实现恒等映射。 |
使用 MapType::Identical 映射方式。 |
确保了内核数据段的正确访问。 |
如何设置逻辑段的权限? |
需要设置逻辑段的权限。 |
使用 MapPermission 设置权限。 |
确保了逻辑段的权限控制。 |
如何创建应用地址空间? |
需要创建应用地址空间。 |
实现 MemorySet::from_elf 方法。 |
确保了应用地址空间的创建。 |
如何解析 ELF 格式数据? |
需要解析 ELF 格式数据。 |
使用 xmas_elf crate 解析 ELF 数据。 |
确保了 ELF 数据的正确解析。 |
如何映射跳板? |
需要映射跳板。 |
在 MemorySet::from_elf 方法中调用 map_trampoline 方法。 |
确保了跳板的正确映射。 |
如何映射应用的各个逻辑段? |
需要映射应用的各个逻辑段。 |
在 MemorySet::from_elf 方法中调用 push 方法。 |
确保了各个逻辑段的正确映射。 |
如何处理用户栈? |
需要处理用户栈。 |
在 MemorySet::from_elf 方法中设置用户栈。 |
确保了用户栈的正确设置。 |
如何设置保护页面? |
需要设置保护页面。 |
在用户栈下方预留一个保护页面。 |
确保了用户栈的安全性。 |
如何映射 Trap 上下文? |
需要映射 Trap 上下文。 |
在 MemorySet::from_elf 方法中映射 Trap 上下文。 |
确保了 Trap 上下文的正确映射。 |
如何返回用户栈顶地址和入口点地址? |
需要返回用户栈顶地址和入口点地址。 |
在 MemorySet::from_elf 方法中返回这些地址。 |
确保了这些地址的正确返回。 |
如何平滑地从物理地址直接访问过渡到分页模式下的虚拟地址访问 |
在开启分页模式后,CPU 访问内存的方式发生了变化,需要确保切换前后地址空间的连续性 |
在切换satp的指令附近保持地址空间映射的连续性,如保持恒等映射 |
成功实现了从物理地址直接访问到虚拟地址访问的平滑过渡,确保了CPU指令的连续执行不受影响 |
如何处理多级页表带来的性能下降问题 |
多级页表增加了MMU的访存次数,加大了多级缓存的压力,导致性能开销增大 |
引入快表(TLB)存储虚拟页号到页表项的映射,减少MMU的访存次数;在切换地址空间时清空快表以保证映射关系的正确性 |
有效缓解了多级页表带来的性能问题,提高了系统的整体效率 |
在启用分页机制后,如何高效地在Trap处理时完成地址空间的切换 |
启用分页机制后,Trap处理不仅需要切换栈,还需要切换地址空间,这增加了操作的复杂性 |
使用跳板机制,将应用的Trap上下文存放在应用地址空间的次高页面,避免了在保存Trap上下文前切换到内核地址空间 |
实现了Trap处理时地址空间的高效切换,简化了操作步骤,同时保证了地址空间的安全隔离 |
如何解决每个应用地址空间都映射内核段带来的内存占用和安全问题 |
每个应用地址空间都映射内核段会导致额外的内存占用,并且存在安全风险,如熔断漏洞 |
采用内核与应用地址空间隔离的设计,每个应用有自己的地址空间,内核有独立的地址空间 |
显著减少了内存占用,提升了系统的安全性,同时也能更好地支持任务的并发执行 |
如何高效地在 Trap 处理时保存和恢复上下文 |
Trap 处理不仅需要保存和恢复寄存器状态,还需要切换地址空间,传统的 Trap 处理方法在启用分页机制后变得复杂 |
在 Trap 上下文中增加额外的信息:kernel_satp (内核地址空间的 token)、kernel_sp (应用内核栈顶的虚拟地址)、trap_handler (trap handler 入口点的虚拟地址)。这些信息在应用初始化时由内核写入,之后不再修改 |
提高了 Trap 处理的效率,简化了地址空间切换的过程,确保了系统的稳定性和安全性 |
如何确保 Trap 处理时能够平滑地切换地址空间 |
在 Trap 处理过程中,需要从用户地址空间切换到内核地址空间,确保指令能够连续执行 |
使用跳板页面(Trampoline Page),将汇编代码放置在 .text.trampoline 段,并对齐到代码段的一个页面中。跳板页面在内核和应用地址空间中均映射到同一物理页帧 |
实现了平滑的地址空间切换,确保了 Trap 处理时指令的连续执行,提高了系统的整体性能 |
为什么在 __alltraps 中需要使用 jr 而不是 call |
在内存布局中,跳转指令和 trap_handler 都在代码段内,但实际执行时的虚拟地址与编译时设置的地址不同 |
使用 jr 指令通过寄存器跳转到 trap_handler 入口点,避免了地址偏移量计算错误的问题 |
确保了跳转指令的正确性,避免了因地址偏移量计算错误导致的程序崩溃 |
如何管理任务以确保应用的安全隔离 |
为了使应用在运行时有一个安全隔离且符合编译器给应用设定的地址空间布局,操作系统需要对任务进行更多的管理 |
扩展任务控制块(Task Control Block),包含应用的地址空间 memory_set 、Trap 上下文的物理页号 trap_cx_ppn 和应用数据的大小 base_size |
提高了任务管理的灵活性和安全性,确保了应用在运行时的安全隔离,支持了更多复杂的应用场景 |
应用程序的地址空间与内核地址空间分离,导致 Trap 处理变得复杂 |
内核和应用程序运行在不同的地址空间中,这使得 Trap 上下文的处理需要特别的机制来确保正确性 |
通过在 task_control_block 中为每个应用分配独立的内核栈,并在 trap_handler 中动态获取当前应用的 Trap 上下文 |
实现了更灵活的任务管理和更安全的 Trap 处理机制,每个应用都能在自己的地址空间中独立运行 |
应用程序的 Trap 上下文不在内核地址空间中,无法直接访问 |
应用程序的 Trap 上下文存储在用户空间,而 Trap 处理发生在内核空间 |
使用 current_trap_cx 函数获取当前应用的 Trap 上下文的可变引用 |
保证了 Trap 处理的高效性和安全性,同时简化了代码逻辑 |
从 S 模式到 S 模式的 Trap 没有合适的处理方式 |
当前系统设计主要关注 U 模式到 S 模式的 Trap,对于 S 模式到 S 模式的 Trap 缺乏考虑 |
在 trap_handler 开头调用 set_kernel_trap_entry 函数,将 stvec 设置为 trap_from_kernel 的地址 |
对于 S 模式到 S 模式的 Trap,采取直接 panic 的策略,避免了复杂的上下文保存和恢复过程 |
应用程序在从 Trap 返回用户态时需要正确设置 stvec |
为了确保应用程序能够正确地 Trap 回到内核,需要在返回用户态前设置正确的 stvec 值 |
在 trap_return 函数中调用 set_user_trap_entry 函数,将 stvec 设置为 TRAMPOLINE 地址 |
保证了应用程序能够通过 __alltraps 正确地 Trap 回到内核,提高了系统的健壮性 |
指令缓存可能包含已过时的代码或数据,影响应用程序的正确执行 |
内核中的一些操作可能导致物理页帧内容的变化,而指令缓存在这些变化后可能仍然保留旧的快照 |
在 trap_return 函数中使用 fence.i 指令清空指令缓存 |
确保了每次从 Trap 返回用户态时,应用程序能够基于最新的代码和数据执行,增强了系统的稳定性和可靠性 |
内核无法直接访问应用空间的数据 |
内核与应用地址空间隔离导致直接访问失败 |
使用 translated_byte_buffer 函数将应用空间的缓冲区转换为内核可直接访问的形式 |
实现了安全有效的数据访问,避免了内存访问违规 |
sys_write 需要支持不同文件描述符 |
当前实现仅支持标准输出,限制了功能扩展性 |
在 sys_write 中增加对其他文件描述符的支持,例如通过查找文件描述符对应的文件对象来确定写入目标 |
扩展了系统调用的功能,使其能适应更多使用场景,如文件写入等 |
字节数组切片转字符串可能失败 |
非UTF-8编码的字节序列可能导致转换错误 |
使用 .unwrap() 强制转换(虽然不推荐,但在本例中假设输入总是合法的),或更安全地处理转换错误 |
确保了输出的正确性和程序的健壮性,即使遇到非UTF-8编码的数据也能优雅地处理 |