MIT6.S081-lab5前置
lab5的前置知识主要是lecture8 page fault的内容。
我们之前提过xv6是没有实现我们的cow写时复制的,当然,我们的lab5需要做的就是为xv6实现一个写时复制。
页错误
在原本的情况,我们的xv6一旦触发页错误,就会立刻杀死进程,但是实际上,我们会利用页错误去干一些事情,通过它,我们而可以去动态的更新page table,但是为了实现这一点,我们需要一些信息才能够去正确处理这个page fault,而不是崩溃或者杀死进程。
在之前的page table实验中,我们可以知道,当我们触发panic的时候,内核会打印因为我们访问/操作而触发错误的地址,并且这个出错的地址会被保存到STVAL寄存器中,我们理所当然需要触发错误的地址,当然,也需要除法错误的原因(执行的指令)除此之外,我们还需要触发错误的指令的地址,以此来恢复我们的程序。
lazy allocation
我们的xv6默认是使用的eager allocation,这意味着,一旦调用sbrk,内核就会立即分配应用需要的物理内存,即便我们之后永远不会使用到这块空间,这就会造成我们内存的浪费,实际上,我们可以采取lazy allocation(懒加载)的策略来解决这个问题,我们在使用sbrk分配内存的时候,还不会真正的去分配内存,而是仅仅将p->sz改变,而之后如果真正使用到了这部分内存(大于stack,小于p->sz),就会触发页错误,然后通过页错误,并且根据我们分配的sz限制来加载需要的内存页(仅加载需要的页,而不是加载sz)。
当然,如果我们需要访问的内存已经大于了p->sz,则说明这是一个错误,去访问了自己并没有申请的地址,这样就不会出现无限申请的情况。
同时,我们还需要引入一个Zero Fill On Demand的概念,和lazy allocation一样,举个例子就是我们分配一个巨大的数组,我们通常期望它的数据是干净的,全部为0,也就是说,但是我们可能并不会去使用所有的内存,那么我们就可以在分配的时候,先将他映射到一个全0页,此时是只读的状态,直到真正要写的时候,我们才会去分配一个新的内存页,并且将他的所有数据都置为0,并且更新映射关系,设置PTE的权限位,然后指向新的物理地址。熟悉写时复制的朋友,可以将其类比一下。
Copy On Write
写时复制,举个例子,当我们fork一个子进程的时候,我们原本的xv6是会直接开辟一个新的空间,然后去复制父进程的所有数据,即便我们之后不会去对子进程进行写入,或者仅仅对部分页进行写入,毫无疑问,这会很大程度的浪费内存,所以,我们可以采取一个优化策略,就是写时复制(cow),当我们fork子进程的时候,我们的子进程会与父进程共享物理内存,所以这里,我们子进程的PTE直接指向父进程的物理内存,并且将父进程和子进程的PTE权限位设置为只读的。
而当我们在某一时间,对这部分内存进行写入操作,就会触发页错误,那么我们之前提过的需要的信息就起到了作用,我们可以根据出现页错误的地址所在的页拷贝,然后分配到新分配的物理页中,此时,将他的PTE设置成读写。
当然,这样的策略也会引发一个问题,那就是内存释放,我们并不能在其中一个进程退出就直接释放掉这部分内存,因为还有可能,这部分内存被其他进程所引用,所以还需要一个新的数据结构来维护它的引用数量,以便于内存释放。
除此之外,我们判断是否是写时复制的page fault的依据是需要在PTE中设置的一个copy-on-write权限位。
Demand paging
在原本的xv6中,我们在exec系统调用的时候,会一次性将需要执行的程序内存text,data区域一次性加载,并且将这些区域一次性加载进pagetable中,根据之前的优化,我们并不需要一次性将程序加载进内存,而是等到程序运行的时候再加载,而在exec执行的时候,我们会为text和data区域预分配地址段(预留空间),而对应的PTE设置为无效,当我们真正的访问,需要执行这段程序,才会将需要执行的程序的哪一页加载进去。
但是在这过程中,如何保持可以对这个文件持续进行读取呢?问了ai,他说是可以设置一个inode字段,通过文件的inode字段就可以持续的对其进行访问,正确与否先不论,现在这里留个悬念,容我学习完文件系统再重新写一下。
同时还有另一个问题,就是在lazy allocation过程中,内存耗尽了咋办?我们可以采取内存淘汰策略,比如LRU,除此之外,还可以进行一些小优化,比如在淘汰一个page的时候,需要在脏页和干净页中做选择,我们首选应该是干净页,因为dirty页必须先写回磁盘再释放,开销会比较大,而干净页可以直接回收,并且脏页在之后可能会被多次执行写入操作,开销就会被放大。
除此之外,为了实现这个LRU,我们还可以加一个标志位access bit来表示当前页在一定时间是否被访问过,一定时间意味着我们需要定期的去恢复它,而我们如果需要触发LRU,我们可以直接淘汰这个没有被访问过的页。
Memory Mapped Files
这是啥玩意?它的核心就是将文件的部分或者全部(大部分系统都不会直接使用的eager方式)加载进内存,以实现快速读写,为了实现这个功能,操作系统通常会提供mmap这个系统调用,而这恰恰是我们后续会写的lab。
我们会向mmap传入一些参数,包括文件描述符,文件描述符开始的偏移量,映射到的长度,映射到的虚拟地址,而在之后,我们完成了操作,会有一个对应的系统调用,unmap,参数是虚拟地址和映射长度,此时,我们还会将脏页写回内存,是否为脏页,只需要根据PTE的dirty bit来判断即可。
甚至,在我们调用mmap的时候,也不会真正的将这部分内容全部加载,而是懒加载!先会记录这个PTE属于这个文件描述符,对应的信息会在VMA结构体中保存,其中会记录这个文件的各种信息,包括fd,offset等。我们在遇到页错误的时候可以通过这个结构体去找到虚拟地址对应的实际内容在哪里,这样来加载到内存中。
最后更新于