XV6学习(3)Page tables

页表是操做系统中很是重要的一部分,用于将虚拟地址转化为物理地址。虚拟内存是操做系统实现进程隔离的关键技术。
在 XV6 中经过 RISC-V 的页表机构完成了虚拟地址向物理地址的转换。数组

分页硬件机构

XV6 运行于 Sv39 RISC-V 上,64 位地址中的低 39 位被使用。RISC-V 的页表逻辑上是 page table entries (PTEs) 的数组,长度为 2^27。PTE 包含 44 位物理地址号(PPN)。页的大小为 4KB,所以,分页硬件使用 39 位中的高 27 位查找 PTE,以后转化为 56 位的物理地址。
地址转换app

而实际上,RISC-V 使用的三级页表,1 级页表为 1 页(4KB),包含 512 个 PTE。27 位页号中的高 9 位为一级页表,中间 9 位为 2 级页表,末 9 位为三级页表。函数

在 PTE 中,低 8 位为标志位,其中 PTE_V 表明地址是否有效,当访问无效页面时会触发page fault;PTE_U表明地址可否在用户模式被访问,若是未设置则页面只能在 supervisor mode 中访问。
ui

为了使分页机构可以正常运行,操做系统必须设置satp寄存器为1级页表的物理地址。操作系统

内核地址空间

XV6 每一个进程拥有一个独立页表,同时内核也拥有一个页表用于描述内核地址空间。内核会将自身地址空间直接映射到物理地址上,来方便访问物理内存和硬件资源。内核地址空间定义在memlayout.h中,以下图所示:

在QEMU中,0~0x80000000用于映射设备接口,而0x80000000(KERNBASE) ~ 0x86400000(PHYSTOP)为RAM。指针

有一小部份内核地址空间不是直接映射的,Trampoline 页面在地址空间最高的位置,随后是每一个进程对应的内核栈,每一个栈之间都有一个 Guard page ,该页的 PTE_V 设置为 0,用于避免缓冲区溢出。code

若是内核栈使用直接映射的方法,那么 Guard page 相对应的物理内存中将会产生不少空洞,致使内存管理变得困难。对象

Code: 建立地址空间

与地址空间有关的代码主要在vm.c文件中。pagetable_t表明一级页表,实际数据类型是一个指针,指向页表的物理地址。blog

walk函数是最核心的函数,该函数经过页表pagetable将虚拟地址va转换为PTE,若是alloc1就会分配一个新页面。接口

kvminit分析

kvminit函数用于初始化内核页表,该函数在内核启动开启分页机制前被调用,所以是直接对物理地址进行操做的。函数首先经过kalloc申请了一个页面用于保存一级页表。kalloc函数就简单地从kmem.freelist中取出一个空闲页面。

kmem结构体的初始化在kinit中进行,该函数在kvminit以前被调用。该函数首先初始化锁,以后使用freerange函数将内核以后的所有空闲 RAM 以 4KB 为一页加入该链表。

void kinit()
{
  initlock(&kmem.lock, "kmem");
  freerange(end, (void*)PHYSTOP);
}

回到kvminit函数,在申请到页表后,经过调用kvmmap函数,将物理地址中的UART0 CLINT等映射到内核页表中,完成了内核页表的初始化。

kvminit函数完成后,main函数紧接着就会调用kvminithart函数。在该函数中,使用MAKE_SATP产生SATP的值,将该值写入satp寄存器中,以后使用sfence_vma刷新 TLB,完成了虚拟地址转换的开启,以后代码中的地址就所有会经过地址转换机构进行转换。

#define SATP_SV39 (8L << 60)
#define MAKE_SATP(pagetable) (SATP_SV39 | (((uint64)pagetable) >> 12))

而在MAKE_SATP中使用SATP_SV39设置 MODE 域为 8,即开启 SV39 地址转换,以下图所示。
satp

kvminithart函数执行完成后,就会调用procinit函数,初始化全部进程结构体,对每一个进程结构体申请两个页面做为内核栈,以后将该页面映射到内核地址空间的高位上。最后再次调用kvminithart函数,刷新 TLB,使硬件知道新 PTEs 的加入,防止使用旧的 TLB 项。

sfence.vma

sfence.vma rs1, rs2指令是一条特权指令,用于通知处理器页表的修改。rs1指示了页表哪一个虚址对应的转换被修改了;rs2给出了被修改页表的进程的地址空间标识符(ASID)。若是二者都是x0,便会刷新整个 TLB。

sfence.vma 仅影响执行当前指令的 hart 的地址转换硬件。当 hart 更改了另外一个 hart 正在使用的页表时,前一个 hart 必须用处理器间中断来通知后一个 hart,他应该执行 sfence.vma 指令。这个过程一般被称为 TLB 击落。

在 XV6 中,两个地方使用了sfence.vma指令,一个是上文提到的kvminithart函数,另外一个就是trampoline.S中,当陷入内核以及返回用户态时会调用。

进程地址空间

每一个进程拥有独立的地址空间,当进程切换时同时会对页表进行切换。XV6 进程地址空间从 0 开始到 MAXVA,即 256GB。

当进程申请内存时,内核就会先调用kalloc函数申请物理页面,以后构造PTE加入进程对应的页表项中。
进程地址空间
在进程地址空间的最高位置为 trampoline,全部进程的该页面映射到同一个物理页面上。一样地,在用户栈的下方也设置了一个 guard page 来防止缓冲区溢出。

Code: sbrk

系统调用char* sbrk(int)用于增长或减小物理内存,当参数为正数时增长,负数时减小。sbrk实际经过growproc进行,growproc调用uvmallocuvmdealloc完成工做。

进程地址空间是从 0 开始连续向上增加的,所以经过proc.sz获取已分配字节数,就能够计算获得当前已分配空间的顶部地址,以后就能够获得对应的页面地址。

uvmalloc函数先计算须要申请的页面数,以后在进程地址空间顶部再申请所需的连续的页面。函数经过kalloc申请物理页面,以后使用mappages函数映射到进程页表中。

uvmdealloc函数先计算须要减小的页面数,以后经过uvmunmap删除页面。在uvmunmap函数内部经过walk获取对应 PTE,将PTE_V设置为0,最后经过kfree函数将该物理页面添加到空闲链表中。

Code: exec

系统调用exec用于建立进程地址空间。函数首先使用namei获取可执行文件,读取 ELF 头,检查 ELF 中的 magic。以后使用proc_pagetable建立进程页表。

proc_pagetable函数中,先使用uvmcreate函数申请一个页面,以后将 trampoline 和 trapframe 映射到高位地址空间中。

exec以后使用uvmalloc申请内存空间,再使用loadseg函数将程序加载到对应页面中。在 Program Header 中描述了各段的 filesz,memsz等信息,当 filesz 小于 memsz 时,中间的空隙用 0 填充(如C语言中的全局变量)。

程序加载完成后,再申请两块页面,第一块为 guard page ,使用uvmclear函数将该页面PTE_U设置为0,即不容许 user mode 访问。第二页设置为进程的栈。而后将argcargv和返回地址压栈,完成栈的准备工做。

最后,exec函数更新进程结构体,将旧页表释放。

在 Program Header 的vaddr中,程序能够指定被加载到的虚拟地址,而这多是危险的,所以在exec中会检查if(ph.vaddr + ph.memsz < ph.vaddr),避免发生加法溢出。

实际操做系统

在 XV6 中,内核直接加载到 0x80000000 的位置上,而在实际操做系统中,通常会使用 kaslr 技术,即内核地址随机化,使攻击者不能直接经过反汇编获取内核变量和函数的地址。

在 RISC-V 中支持 super pages,即大小为4MB的页面,用于下降大内存机器上的页表开销。

XV6 也缺乏相似于 malloc 的机制来减小使用sbrk大量分配小对象的开销。

相关文章
相关标签/搜索