- 地址空间
- 分页硬件
- xv6的VM代码
虚拟内存概述
问题:假设shell程序有一个bug:有时,它会向一个随机的内存地址写数据。那么我们该怎样阻止shell程序破坏内核和破坏其他进程呢?
我们想有彼此隔离的地址空间
每个进程都有自己的内存
进程读写自己的内存
进程不能读写其他任何内存我们面临的挑战是:在保持内存间隔离的同时,如何在一个物理内存之上多路复用若干个内存?
xv6使用RISC-V的分页硬件来实现地址空间。
分页提供了一层间接寻址。
CPU->MMU->RAM
VA PA
RISC-V指令使用的是虚拟地址,而不是物理地址。
内核告诉MMU每个虚拟地址是如何映射到一个物理地址的。
从本质上讲,MMU有一个表,表的索引是va,能生成一个pa。因此,MMU也叫做页表。
一个地址空间对应一个页表。
MMU能限制用户代码可以使用什么虚拟地址。
通过对MMU编程,内核可以完全控制虚拟地址va到物理地址pa的映射,允许许多有意思的操作系统特征或者技巧。RISC-V映射4KB的页
在xv6中,RISC-V使用64位的地址,其中12位用于在一个页上的寻址,52位用于虚拟地址。
52位的虚拟地址中高25位是没有被使用的,即虚拟地址的索引位有27位。MMU的转换过程
请参考书中的图3.1
使用虚拟地址的索引位来查找一个页表项PTE;
使用来自PTE的PPN和虚拟地址的偏移量来构建虚拟地址;一个PTE由哪些组成?
每个PTE有64位,但是只有54位被使用;
PTE的高44位是物理地址的高44位,即物理页号PPN;
PTE的低10位是标记位,比如present标记、writable标记等;
注意:虚拟地址的大小不等于物理地址的大小页表被保存在哪里?
页表被保存在RAM中,MMU加载和保存页表项PTE;
操作系统可以读写页表项,读写页表项对应的内存位置;页表是由页表项组成的数组,这种结构合理吗?
一个页表由多大?
共有个页表项,大约是1.34亿;
一个页表项有64位;
即,一个页表的大小是134*8MB。
由于
每个应用要一个地址空间;
每个地址空间要一个页表;
每个页表浪费大约1GB的空间;
对于小型应用来说,这就浪费了大量内存。
实际上,你仅需要映射几百个页就够了,剩下的几百万个页表项还在内存中,但是并不需要。RISC-V 64使用3级页表来节省空间。
请看书中的图3.2;
页目录(page directory)
页目录有512个页表项,每个页表项要么指向的是另一个页目录或者要么本身就是叶子。
因此,总共有512*512*512个页表项。
页目录的条目可以是无效的,即那些页表项指向的页不需要存在,所以小型地址空间的页表可以很小。MMU是如何知道页表在RAM中的位置的?
寄存器satp中记录了顶层页目录的物理地址
页可以在RAM中的任何位置,且页不需要是连续的
当切换到另一个地址空间或者应用时,需要重写寄存器stapRISC-V分页硬件是如何转换虚拟地址的?
需要查找到正确的页表项
寄存器satp指向顶层/L2层页目录;
高9位索引到L2层页目录中获取L1页目录的物理地址;
中间9位索引到L1页目录获取L0层页目录的物理地址;
低9位索引到L0页目录中获取页表项的物理地址;
来自页表项的PPN+虚拟地址的低12位;在页表项中有哪些标记?
V,R,W,X,U
xv6使用上述所有的标记如果V标记位没有设置,会发生什么?
如果执行store指令,但是W标记位没有设置,会发生什么?
答:会出现页故障;
强制切换到内核,请看xv6的源码trap.c;
内核只是简单地产生错误,杀死进程,比如在xv6中,就是"usertrap():unexpected causes... pid =... spec=... stval=...";
或者安装一个页表项,重启进程,比如在重磁盘加载内存页之后。间接寻址允许分页硬件解决许多问题,比如
物理内存不一定要连续,这样就避免了碎片化问题;
延迟分配;
copy-on-write派生fork进程;
以及更多的技术;为什么要在内核中使用虚拟内存?
对于用户进程来说,使用页表是有好处的。但是为什么内核也有一张页表?
内核运行时可以只使用物理地址吗?可以。
注意,大部分标准的内核确实使用的是虚拟地址。
为什么标准内核也这么做?
- 硬件使得关闭虚拟内存很难,但是进入系统调用,可以使虚拟内存失效。
- 内核自身也能从虚拟地址中受益
标记文本页的X标记位,但是不标记数据页的X标记位,这样可以有助于追踪bug;
取消在内核栈之下的某个页的映射,这样有助于指针追踪bug;
在用户空间和内核空间中映射一个页,这样有助于用户模式和内核模式之间的切换;
在xv6中的虚拟内存
内核页表
先看一下教材的图3.3
大部分是简单的映射,即虚拟地址到物理地址的一对一的映射;
注意:
trampoline的双映射;
权限问题;
为什么要映射设备?每个进程都有自己的地址空间,以及自己的页表
请看书中的图3.4
注意:trampoline和trapframe是不能被用户进程写的;
当进程切换时,内核会切换页表,即设置寄存器satp;
问题:为什么是这样的地址空间安排?
- 用户虚拟地址空间从0开始,当然对每个进程来说,虚拟地址0是映射到不同的物理地址的
- 用户堆可连续增长到
16,777,216GB,但是并不需要连续的物理地址,因此没有碎片化问题。 - 内核和用户都会映射trampoline页和trapframe页,这样使得用户模式和内核模式的切换变得容易
- 内核不会映射用户应用
- 内核读写用户内存是有困难的
需要将用户的虚拟地址转换成内核虚拟地址;
对隔离性有利; - 使得内核容易读写物理内存
将虚拟地址x映射到物理地址x
问题:内核不得不映射所有的物理内存到虚拟地址空间吗?
代码一览
设置内核地址空间
kvmma()
问:什么是地址0x10000000?
答:可寻址的最大地址空间为256M
问:1个L2条目能覆盖多大的地址空间?
问:1个L1条目能覆盖多大的地址空间?
问:1个L0条目能覆盖多大的地址空间?
输出内核页表
问:内核地址空间有多大?
问:在首次调用kvmmap()后,使用了多大内存来表示内核?
问:CLINT中有多少条目?
问:PLINT中有多少条目?
问:内核代码段占了多少个页?
问:内核总共用了多少页?
问:trampoline是被映射了两次吗?
kvminithart()
问:为什么在执行w_satp()后的下一条指令是sfence_vma?
在vm.c中的mappages()函数
参数是一级目录页PD、虚拟地址va、大小size、物理地址pa、perm等;
添加从虚拟地址va的范围到物理地址pa的范围的映射;
对于范围内的补齐页的地址,调用walkpgdir函数来查找页表项的地址,向页表项PTE中放入期望的物理地址pa,标记页表项PTE为有效地的w/PTE_P等;
在vm.c中的walk函数
模仿了分页硬件是如何查找一个地址的页表项;
PX拉取高9位,&pagetable[PX(level, va)]就是相关的页表项的地址;
如果PTE_V,则相关的页表页已存在,PTE2PA就从PTE中
抽取PPN。
否则,分配一个页表页,使用PPN来填页表项pte;
至此,我们想要的页表项已在页表页中了。
在proc.c中的procinit()函数
为每个内核栈分配一个页时都使用一个保护页
设置用户地址空间
allocproc():分配一个空的一级页表
fork():uvmcopy()
exec():用新进程的页表替换旧进程的页表
- uvmalloc
- loadseg
用sh输出用户的页表
问:条目2指的是什么?
进程调用sbrk(n)来请求多分配n个字节的堆内存
用户的umalloc调用sbrk来获取内存用于分配程序;
每个进程有一个大小,内核可在进程的末尾添加新的内存,并增加大小;
sbrk()分配了物理内存,将该物理内存映射到进程的页表中,返回该新内存的开始地址;在proc.c中的函数growproc()
proc->sz是进程的当前大小;
uvmalloc函数做了大部分工作;
当切换到用户空间时,satp将被加载成被更新的页表;在vm.c中的uvmalloc函数
为什么PGROUNDUP?
mappages()的参数有哪些?