当一个用户应用程序触发了page fault,page fault会使用与Robert教授上节课介绍的相同的trap机制,将程序运行切换到内核,同时也会将出错的地址存放在STVAL寄存器中。
在SCAUSE(注,Supervisor cause寄存器,保存了trap机制中进入到supervisor mode的原因)寄存器的介绍中,有多个与page fault相关的原因。比如,13表示是因为load引起的page fault;15表示是因为store引起的page fault;12表示是因为指令执行引起的page fault。所以第二个信息存在SCAUSE寄存器中,其中总共有3个类型的原因与page fault相关,分别是读、写和指令。
lazy allocation
核心思想非常简单,sbrk系统调基本上不做任何事情,唯一需要做的事情就是提升p->sz,将p->sz增加n,其中n是需要新分配的内存page数量。但是内核在这个时间点并不会分配任何物理内存。之后在某个时间点,应用程序使用到了新申请的那部分内存,这时会触发page fault,因为我们还没有将新的内存映射到page table。所以,如果我们解析一个大于旧的p->sz,但是又小于新的p->sz(注,也就是旧的p->sz + n)的虚拟地址,我们希望内核能够分配一个内存page,并且重新执行指令。
所以,当我们看到了一个page fault,相应的虚拟地址小于当前p->sz,同时大于stack,那么我们就知道这是一个来自于heap的地址,但是内核还没有分配任何物理内存。所以对于这个page fault的响应也理所当然的直接明了:在page fault handler中,通过kalloc函数分配一个内存page;初始化这个page内容为0;将这个内存page映射到user page table中;最后重新执行指令。比方说,如果是load指令,或者store指令要访问属于当前进程但是还未被分配的内存,在我们映射完新申请的物理内存page之后,重新执行指令应该就能通过了。
在某个时间点,应用程序可能会用光了物理内存,之后如果应用程序再访问一个未被分配的page,但这时又没有物理内存,这时内核可以有两个选择,我稍后会介绍更复杂的那个。你们在lazy lab中要做的是,返回一个错误并杀掉进程。因为现在已经OOM(Out Of Memory)了,内核也无能为力
zero-fill-on-demand
当你查看一个用户程序的地址空间时,存在text区域,data区域,同时还有一个BSS区域(注,BSS区域包含了未被初始化或者初始化为0的全局或者静态变量)。当编译器在生成二进制文件时,编译器会填入这三个区域。text区域是程序的指令,data区域存放的是初始化了的全局变量,BSS包含了未被初始化或者初始化为0的全局变量。
BSS段
我有如此多的内容全是0的page,在物理内存中,我只需要分配一个page,这个page的内容全是0。然后将所有虚拟地址空间的全0的page都map到这一个物理page上。这样至少在程序启动的时候能节省大量的物理内存分配。
我们不能允许对于这个page执行写操作,因为所有的虚拟地址空间page都期望page的内容是全0,所以这里的PTE都是只读的。之后在某个时间点,应用程序尝试写BSS中的一个page时,比如说需要更改一两个变量的值,我们会得到page fault。
Copy On Write Fork
当Shell处理指令时,它会通过fork创建一个子进程。fork会创建一个Shell进程的拷贝,所以这时我们有一个父进程(原来的Shell)和一个子进程。Shell的子进程执行的第一件事情就是调用exec运行一些其他程序,比如运行echo。现在的情况是,fork创建了Shell地址空间的一个完整的拷贝,而exec做的第一件事情就是丢弃这个地址空间,取而代之的是一个包含了echo的地址空间。这里看起来有点浪费。
优化
当我们创建子进程时,与其创建,分配并拷贝内容到新的物理内存,其实我们可以直接共享父进程的物理内存page。所以这里,我们可以设置子进程的PTE指向父进程对应的物理内存page。
在某个时间点,当我们需要更改内存的内容时,我们会得到page fault。因为父进程和子进程都会继续运行,而父进程或者子进程都可能会执行store指令来更新一些全局变量,这时就会触发page fault,因为现在在向一个只读的PTE写数据。
在得到page fault之后,我们需要拷贝相应的物理page。假设现在是子进程在执行store指令,那么我们会分配一个新的物理内存page,然后将page fault相关的物理内存page拷贝到新分配的物理内存page中,并将新分配的物理内存page映射到子进程。这时,新分配的物理内存page只对子进程的地址空间可见,所以我们可以将相应的PTE设置成可读写,并且我们可以重新执行store指令。实际上,对于触发刚刚page fault的物理page,因为现在只对父进程可见,相应的PTE对于父进程也变成可读写的了。
Demand Paging
对于exec,在虚拟地址空间中,我们为text和data分配好地址段,但是相应的PTE并不对应任何物理内存page。对于这些PTE,我们只需要将valid bit位设置为0即可。
应用程序是从地址0开始运行。text区域从地址0开始向上增长。位于地址0的指令是会触发第一个page fault的指令,因为我们还没有真正的加载内存。
首先我们可以发现,这些page是on-demand page。我们需要在某个地方记录了这些page对应的程序文件,我们在page fault handler中需要从程序文件中读取page数据,加载到内存中;之后将内存page映射到page table;最后再重新执行指令。
内存耗尽了,如何处理
对于demand paging来说,假设内存已经耗尽了或者说OOM了,这个时候如果得到了一个page fault,需要从文件系统拷贝中拷贝一些内容到内存中,但这时你又没有任何可用的物理内存page,这其实回到了之前的一个问题:在lazy allocation中,如果内存耗尽了该如何办?
如果内存耗尽了,一个选择是撤回page(evict page)。比如说将部分内存page中的内容写回到文件系统再撤回page。一旦你撤回并释放了page,那么你就有了一个新的空闲的page,你可以使用这个刚刚空闲出来的page,分配给刚刚的page fault handler,再重新执行指令。
以上就是常见操作系统的行为。这里的关键问题是,什么样的page可以被撤回?并且该使用什么样的策略来撤回page?