缺页异常需要什么
当发生缺页异常时,内核需要以下信息才能响应这个异常:
- 出错的虚拟地址(引发缺页异常的源)
当一个用户程序触发了缺页异常,会切换到内核空间,将出错的地址放到STVAL寄存器中,这是第一个信息 - 出错的原因
因为对于不同场景的缺页异常有不同的响应。例如由于load指令触发的缺页、由于store指令触发的缺页、由于jump指令触发的缺页等等。不同的原因都会有一个代码表示,这个代码就会作为第二个信息,存储在SCAUSE寄存器中。 - 触发缺页时的程序计数器值
其表明了缺页这一事件在用户空间发生的位置,因为在操作系统处理完缺页后,CPU需要重新执行发生缺页的那一条指令,也就是PC必须回到原先的那一个值继续往后执行。这个地址作为第三个信息存放在SEPC寄存器中
copy-on-write(COW) fork
Xv6对异常的响应较为简单:若用户空间中某个进程发生了异常,则内核直接终止该进程。若内核空间发生了异常,则the kernel panics。
而实际的操作系统则会利用页缺失异常完成许多功能。例如许多内核会用页缺失来完成copy-on-write(COW) fork。
fork使得子进程共享父进程的内容,即先调用uvmcopy来为子进程分配物理内存,然后将父进程的内容拷贝进去。
由此可见比这种方法更简单的一种方式是,让子进程和父进程直接共享同一片物理内存,就省去了分配新的内存再拷贝内容的麻烦。但是这种方法有种缺点,就是子进程和父进程同时写共享的堆栈的话,会导致彼此干扰。
Copy-on-write fork就可以保证父子共享同一片物理内存而不出现上述干扰。
主要思想是,一开始让父子只读地共享所有物理内存。只有当发生写操作时(即执行到存储指令时)才会复制地址空间,且复制的只是写操作所在的那一页。这时子进程和父进程分别独立拥有这一页的地址空间,且是读写权限。
Page fault在这个过程中的作用就是当发生写操作时,CPU会发起一个页缺失异常(页就是要写的地址所在的那一页),然后内核才会复制这一页的读写版本给父子进程。
一些比较有意思的问题
- 对于一些没有父进程的进程,比如系统启动的第一个进程,它会把自己的PTE设置成只读的吗,还是先设置成可读写的,然后在fork的时候再修改为只读的?
这取决于开发者。操作系统的开发者可以自己选择实现方式。当然最简单的方式就是将PTE设置成只读的,当你要写这些page时,会得到一个page fault,然后再按照上面的流程处理 - 当发生page fault时,我们其实是在向一个只读的地址执行写操作。内核如何能够分辨现在是一个copy-on-write fork的场景,而不是应用程序在向一个正常的只读地址写数据?
内核有能够识别copy-on-write场景的机制。几乎所有的页表页表硬件都支持了这一点。内核可以通过页表项的某一个标识位来分辨当前是否是一个copy-on-write,或者内核也可以通过维护一些进程信息来实现copy-on-write的标识。具体实现是非常自由的。
关于页表项中的标识位,如下图,在MIT的Xv6实验中,学生可以使用RSW在PTE中设置一个copy-on-write标识位