xv6 Page Fault
Page Fault
触发page fault之后,进程被kill,会触发panic,page fault会使用trap
机制,
- 并将出错的地址虚拟地址放到
STVAL
寄存器中, - 并将
trap
的原因放入SCAUSE
寄存器中,那么usertrap
函数就能够对page fault做出处理,一般就是kill进程,,
- 还需要将触发
page fault
的指令的地址保存在SEPC
寄存器,同时在usertrap
函数中还会将其记录在trapframe->epc中,之所以关心触发page fault时的程序计数器值,是因为在page fault handler中我们或许想要修复page table,并重新执行对应的指令。理想情况下,修复完page table之后,指令就可以无错误的运行了。所以,能够恢复因为page fault中断的指令运行是很重要的。
Lazy page allocation
sbrk
是XV6提供的系统调用,在代表每一个进程的结构体proc
的p->sz
代表sbrk,sbrk
代表当前进程的可用heap的大小sys_sbrk
实际上调用的是growproc
,分配物理内存,并将内存映射记录到用户页表中,并将内存初始化为0,当然这里采用的
eager allocation
lazy allocation
的策略是只增长p -> sz
,当因为page fault
的时候才去真正的分配物理内存,并将内存映射记录到用户页表中,并将内存初始化为0
Zero Fill On Demand
- 对于
elf
文件中的.bss
段,由于其中存的全是0,这时候只需要在物理内存中,我只需要分配一个page,这个page的内容全是0。然后将所有虚拟地址空间的全0的page都map到这一个物理page上 - 对于这样的页,一开始PTE只需要设置为只读就好了,那么当发生store指令的时候,也就是写指令,便在物理内存中申请一个新的内存page,将其内容设置为0,因为我们预期这个内存的内容为0。之后我们需要更新这个page的mapping关系,首先PTE要设置成可读可写,然后将其指向新的物理page。这里相当于更新了PTE,之后我们可以重新执行指令。
Copy On Write Fork
fork
之后一般跟着exec
,那么子进程之前复制的父进程的PTE以及物理内存都没用了,- Copy On Write Fork的做法是,将子进程的PTE首先指向父进程的物理内存,并且修改PTE的写flag,也就是父进程与子进程的PTE的写flag都置空
- 当父进程或者是子进程需要去写的时候,触发page fault,拷贝page fault相关的物理页,并更新PTE,就是修改PPN与读写位
- 当物理页的引用计数被置为1的时候,那么可以引用该页的PTE置为可读可写了
- 细节是:如何判断当发生page fault时,我们其实是在向一个只读的地址执行写操作。内核如何能分辨现在是一个copy-on-write fork的场景,而不是应用程序在向一个正常的只读地址写数据。是不是说默认情况下,用户程序的PTE都是可读写的,除非在copy-on-write fork的场景下才可能出现只读的PTE?
Demand Paging
- 在未修改的xv6中,执行exec时,os会以
eager
的方式加载.data
,.text
段 - 但是根据我们在lazy allocation和zero-filled on demand的经验,为什么我们要以eager的方式将程序加载到内存中?为什么不再等等,直到应用程序实际需要这些指令的时候再加载内存?程序的二进制文件可能非常的巨大,将它全部从磁盘加载到内存中将会是一个代价很高的操作。又或者data区域的大小远大于常见的场景所需要的大小,我们并不一定需要将整个二进制都加载到内存中。
- 所以对于exec,在虚拟地址空间中,我们为text和data分配好地址段,但是相应的PTE并不对应任何物理内存page。对于这些PTE,我们只需要将valid bit位设置为0即可
Memory Mapped Files
- mmap是一个映射磁盘文件或者是匿名文件到进程的虚拟地址,xv6没有使用惰性加载之前,都是采取eager的方式,比如说在执行exec的时候,直接去将文件的
.text
与.data
段加载到进程虚拟地址空间对应的物理地址空间,而lazy的方式是记录进程的虚拟地址空间的PTE对应的文件描述符(会在一个VMA结构体中), - 匿名文件就是在物理地址开辟一块区域
- malloc与mmap的关系
参考这里