一些别人那里学来的杂记
当指令需要访问或者操作某一个虚拟地址时,内存管理单元(MMU)会通过页表找到这个虚拟地址所对应的真实的地址。
页表在逻辑上是由一条条page table entries(PTEs)组成,PTE中一部分的位组成PPN(physical page num),即下一级页表所在的物理地址或者最终要得到的物理地址所属于的页的地址; 一部分的位组成flags,用于指示是否可以对该虚拟地址进行某些操作
在这个过程中,如果任意一个需要寻找的PTE不存在,或者不能被访问,硬件就会抛出一个异常,然后交由内核决定进行什么后续操作
在现实生活中,除了上述的页表,还有一种被称为反向页表的东西。 此类页表中的每个项代表系统中的一个物理页,用于告诉我们哪个进程正在使用该页,以及该进程的哪个虚拟页映射到此物理页。我们要找到正确的项就需要搜索整个页表。但是线性搜素是昂贵的,所以通常人们会在该基础结构上建立散列表,以加速查找。

在物理地址中,0~0x80000000是和硬件设备的接口对应,从地址空间0x80000000开始才是RAM。从0到PHYSTOP采用的是直接映射,这样可以简化一些读写操作(比如fork中涉及的拷贝操作)
但是在很靠后的空间,如trampoline不是直接映射的。trampoline所对应的物理地址在内核虚拟空间里有着俩个地方与之映射:一个是直接映射,一个是高位映射。有着同样映射方式的还包括每个进程的内核栈:每个内核栈的物理地址均存在着直接映射与高位映射。这样的好处是可以为每个内核栈添加一个guard page,该页在内核页表中对应PTE的PTE_V位是0。当内核栈的数据溢出到guard page时,硬件就会产生异常,这样就可以避免不同进程的内核栈相互干扰
图片说明

用户空间高位处的trampoline在每个用户进程和内核空间里显示的地址都是一样的。但是它在用户页表中对应的PTE的PTE_V位是0,所以用户不能获取它的物理地址
只有用户进程自己的页表中记录了哪些物理页被分配给了这个进程。在内核页表和其他用户进程的页表无相关记录
图片说明

在操作系统启动时运行的main函数里,会调用kvminit(kernel/vm.c)来创建内核页表。kvminit调用kvmmake函数来创建内核页表
kvmmake(kernel/vm.c)首先会为内核的根页表分配一个物理页,并初始化为0。然后函数调用kvmmap向内核页表里添加一些必须的映射(比如一些硬件接口的映射)。在这个函数里使用kvmmap进行映射的都是直接映射:使物理地址和虚拟地址相等。kvmmap内部是直接调用mappages(在后面介绍)实现的。最后调用proc\_mapstacks为每个进程的内核栈分配一个物理页。
proc_mapstacks(kernel/proc.c)会为每一个进程的内核栈分配一个物理页,然后将其映射至内核地址空间的高位处。在内核栈高位映射的虚拟页下方,是一块guard页,以防止内核栈溢出
紧接着main函数会调用kvminithart(kernel/vm.c)kvminithart函数将内核的根页表地址写入satp寄存器。在这条指令之前,我们使用的都是物理内存地址(上一节介绍的start函数将0写入了w_satp),这条指令之后page table开始生效了。从此刻开始,所有的内存地址都指的是虚拟内存地址。kvminithart还会调用sfence_vma()刷新TLB

walk函数(kernel/vm.c)可以返回虚拟地址va在第三级页表中对应的PTE所在地址。其查找过程与前面硬件处理相似:从根页表开始寻找,根据页表所属于的级数在va中截取合适的索引以获取在该级页表中相应的PTE(根页表使用va的3139位,中间一级使用2230位,最后一级使用13-21位)。根据PTE的PTE_V位判断该PTE所代表的映射关系是否是合法的。如果合法,则获取下一级页表地址继续查找。如果查找过程中发现某个PTE_V为0,则会判断alloc是否不为0。如果不为0的话,就会分配一个物理页,并在这一级的页表中将该PTE的PPN部分指向这个新物理页,同时修改PTE_V为1。值得注意的是,这里是以页为单位的。
mappages函数(kernel/vm.c)则为以va为起点的虚拟地址和以pa为起点的物理地址创建映射。创建映射的数目由size决定。perm代表创建映射时,最后一级页表中对应的PTE的flag状态。这里的va不一定得是某一页的首地址,size也不一定得是页大小的整数倍。(在xv6中,前俩级页表我们只关注PTE_V位的情况,只有最后一级页表中我们才关注PTE_WPTE_U等位)

xv6中物理内存一般是以页为单位进行整体分配和回收的。所有的空闲未分配的页会连在一起,构成一个链表。可以用指针freelist获取该链表头。freelist是在操作系统开始时创建的,main函数里调用kinit(kernel/kalloc.c)kinit调用freerange(kernel/kalloc.c)。在freerange里,会将所有可用的内存以一个页的大小进行切分,(注释1中每次p都是增加一个PGSIZE),这样每个切分的块就可以当成一个页了。然后调用kfree将页大小的块整体加入freelist
kfree(kernel/kalloc.c)原本是用于回收物理页的,其过程就是将整个页先填满垃圾值,然后将其加入freelist里。freerange在初始化所有页时,也需要进行类似操作,所以就直接调用了kfree

xv6阅读笔记
图片说明
kernel/memlayout.h声明了xv6内核内存布局的常量