第4课 虚拟内存

  • 地址空间
  • 分页硬件
  • 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中的任何位置,且页不需要是连续的
    当切换到另一个地址空间或者应用时,需要重写寄存器stap

  • RISC-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()的参数有哪些?

你可能感兴趣的:(第4课 虚拟内存)