重读 xv6(III)
xv6 虚拟内存
四 #
我们希望对用户隐藏底层物理空间,并提供无限大的虚拟空间假象,这就是虚拟内存的想法
物理内存 #
源码文件:kalloc.c
xv6 采用链表组织内存块,初始化时,将可用内存按固定分块加入链表
运行时通过 kalloc、kfree 获取其物理地址,再绑定到 pagetable 映射,便可以被用户进程使用了
另起一个超大内存块的链表,进行分类讨论,就可以实现扩展的 superpage 特性
空间结构 #
源码文件:memlayout.h
解决物理地址的映射问题后,我们便只需要关注虚拟内存了,这个空间是如何组织的呢?
虚拟空间由以下部分组成:
- 用户程序与数据
- 用户栈,固定大小
- 堆,用于动态分配空间
TRAMPOLINE、Trapframe,用于Trap到内核态
用户栈是由
exec负责分配的,此处按下不表
地址转换 #
源码文件:vm.c
这种虚拟地址(virture address)的转换,必须有硬件做支持,事实上我们有一个专门用来记录页表地址的寄存器,这样用户引用 va 时,硬件可以自动完成地址翻译
因此我们只需要关心如何安排页表映射即可
xv6 以 mappage 与 walk 作为该逻辑的核心
walk按偏移量逐层查找多级页表mappage将内存段逐页加入页表映射
xv6 的页表设计:
- 三层
- 存储 物理地址 与 读写权限位
satp寄存器记录页表地址
权限位的设计,提供了极大的扩展性,我们可以添加自定义的标记以支持新功能
例如可以增加PTE_S权限位,只遍历前两层,用于指代superPage
应用层 #
以 kalloc 与 walk-mappage 逻辑为基础设施,我们便可以实现常用的辅助函数:
uvmalloc/uvmdealloc申请与释放堆空间uvmcopy复制fork进程的内存空间uvmmap/uvmunmap插入或移除 page 映射uvmcreate申请用户页表
现在我们就理解,II 中创建进程的 allocproc 在做什么了
- 申请页表
- 插入
trap与kernel stack映射
申请堆空间,kalloc 后更新映射与 size 计数即可
而 fork 就是复制页表后,逐页将其内容 copy 到新进程
fork时,我们当然可以现在一股脑把物理地址的内容也复制过去,但也有更好的做法由于
va机制是基于映射的,可以在映射上面做手脚,让两个不同的va指向同一块物理地址,真正写入时再进行区分,这就是 copy-on-write 的思想
内核特权 #
虚拟内存是针对用户而言的,那么 kernel 视角下的内存是什么样的呢?
按理说 kernel 拥有更高权限,应该能直接操作物理地址,但只有 machine mode 才能做到这一点
kernel mode 仍旧使用 页表,只不过我们将物理地址进行直接映射,把 x 映射到了 x
因此 kernel 可以直接访问设备、内核程序等内容,无缝运行内核代码
内核页表的映射,会将
PTE_U置为禁止用户访问,防止用户通过漏洞恶意访问内核地址举个例子,一般我们会将页表存入 TLB 缓存,方便读取,如果切换回用户态时,忘记清理 TLB 缓存,那么用户就能通过过期的kernel 映射,直接访问到内核的真实物理地址
此外,kernel 为 trapframe 与 kstack 提供了额外的映射,便于共享内存
这两个额外的 va 都放在
MAXVA顶部,其中 trap 映射是为了所有进程能够共享同一段只读程序,kstack映射则是为了在中间插入虚拟的guardPage,一旦访问到 guard 就会引发异常报错这里的一个技巧是假映射,通过让用户访问无效映射,引发 page fault
重新启动 #
检查完整个虚拟内存的设计后,我们回头看 main.c/kvminit 等初始化函数到底做了什么
kinit将所有空闲的物理内存块都塞入kalloc链表中,用于分配堆kvminit申请 kernel 页表,加入直接映射,分配内核栈映射,用于 kernel 运行kvmhartinit将页表寄存器%satp替换为kernel pagetable,启动地址转换功能
由于 kernel 采用的是直接映射,所以启动地址转换后,程序计数器的内容没有改变,程序流并不受影响,完成无缝切换
事实上,kernel mode 比 machine mode 的内容更加丰富,增加的映射是针对系统的优化
第一发现人 #
源码文件:exec.c
我们已经谈论过如何请求新的进程资源、复制一个现成的进程,可还是不明白,用户程序是怎样被加载到进程中的?
这就是 exec 的工作,它是一切的开端,我们调用 exec init 才有了第一个进程的出生
exec 的流程:
- 读取程序文件
- 载入
elf header,检查文件格式 - 申请新页表,将
program内容载入到内存中 - 申请用户栈,将
args[]压入栈,设置guard page的PTE_U为不可读 - 修改栈指针
%sp与返回地址%epc,重定向程序流 - 覆盖、释放原进程的页表空间
ret 后,程序将从 elf 文件定义的起点开始执行,完成程序镜像的替换
exec修改了程序流与内存空间,但是并不影响 pid 这些进程状态
至此我们理解了一个用户任务是如何载入 xv6 ,并参与 CPU 调度的
这一节我们回答了以下问题:
- 各个视角下的虚拟内存是怎么组织的?
- xv6 如何动态分配内存?
- xv6 如何管理页表资源?
- 地址转换的透明化是由硬件完成的
- 栈、堆、代码,这些资源分别是在何时映射到 xv6 进程中的?
vm.c中的接口如何被上层所使用?- 一块物理内存,从申请到释放,它的生命周期是怎样的?