程序运行时,CPU访问的用户空间的虚地址,Linux采用请页机制来节约物理内存(物理内存对于磁盘来说是相当小的),也就是说它仅仅把当前所要使用的用户空间的少量页装入物理内存。若虚地址有效,但其对应的页并不在当前的物理内存,就是缺页异常,这时就要从磁盘中或者交换文件中将其装入物理内存。但是如果是要访问在用户空间(3GB)之外的地址,则该地址无效,并且会被终止运行。
1.却页异常处理程序
当一个进程执行时莫若果CPU访问到一个有小的虚地址,但是这个地址对应的页没有在内存中,则CPU产生一个却页异常。
当发生却页异常时,将调用却页异常处理函数do_page_fault()。该函数将先读取一起缺页异常的虚地址。如果没找到,则说明访问了非法的虚地址(即找到用户空间意外的地址),Linux会发信号终止进程,如果能找到该虚地址,也要检查缺页类型,若果是非法类型(越界错误,段权限错误等)同样要发信号终止进程。(缺页异常只能发生在内核态,如果发生在用户态,则必然是错误的)。
对有效的虚地址,如果发生缺页异常,必须要判断页是在交换文件中,还是可执行映像中。如果该页的页表项非空,但对应的的页不在内存,则说明该页处于交换文件中,操作系统要从交换文件装入页。如果错误由写访问引起,该函数检查这个虚存区是否可写。如果不可写,则对这种错误进行相应的处理;如果可写,则采用写时复制技术。如果错误由读或执行访问引起,该函数检查这一页是否已经存在于物理内存中,如果在,错误的发生就是由于进程试图访问用户态下的一个有特权的页面引起的。因此函数跳转到相应的错误处理代码处(实际上这种情况从不发生,因为内核根本不会给用户进程分配有特权的页面)。如果不再物理页面,函数还将检查这个虚存区是否可读或可执行。如果这个虚存区的访问权限与引起缺页异常的访问类型相匹配,则调用handke_mm_fault()函数,该函数确定如何给进程分配一个新的物理页面如下。
(1)如果访问的页不在内存(这个页还没有存放在任何一个物理页面中),那么内核分配一个新的页面并适当的初始化,这种技术称为请求调页。
(2)如果访问的页在内存中,但被标为只读,也就是说,他已经被存放在一个页面中,那么,内存分配一个新的空间,并把旧的数据拷贝到新叶面来初始化它,这种技术称为写时复制。
2.请求调页(无需分配时绝不分配)
请求调页指的是一种动态内存分配技术,它把页面的分配推迟到不能在推迟为止,也就是说,一直推迟到进程要访问的页不再物理内存为止,由此引发一个缺页异常。进程开始运行时并不访问其地址空间中的全部地址。有一部分地址也许进程永远没有使用。程序的局部性原理(是指程序在执行时呈现出局部性规律,即在一段时间内,整个程序的执行仅限于程序中的某一部分。相应地,执行所访问的存储空间也局限于某个内存区域。)保证了在程序执行的每个阶段,真正使用的进程页只有一小部分,因此临时用的页无需调入内存。相比全局分配来说,请求调页增加了系统中的空闲页面的平均数,从而更好的利用了内存的空闲内存。
但是系统也要为此付出额外的开销,因为请求调页引发的缺页异常必须由内核处理,这将浪费CPU的周期。但是局部性原理保证了一旦进程开始在一组页上运行,则接下来很长一段时间他就会停留在这些页上而不去访问其他的页。
基于以下两种原因,被寻址的也可以不再内存中
(1)进程永远也没有访问到这个页。内核能够识别这种情况,这是因为页表相应的表项被填充为0,宏pte_none用来判断这种情况,如果页表项为空,则返回1,否则返回0。
(2)进程已经访问过这个页,但是这个页的内容被临时保存在磁盘上。内核也能够识别这种情况,这是因为页的表相应的表象没有被填充为0(然而由于页面不存在物理内存中存在为P为0)。
在其他情况下,当页从未被访问时则调用do_no_page()函数。有两种方法可以装入所缺的页,采用那种方法取决于这个页是否与磁盘文件建立了映射关系。如果页与文件建立了映射关系,则nopage域就指向一个函数,该函数把所缺的页从磁盘装入到内存。
(1)nopage域不为NULL。在这种情况下,说明某个虚存区映射了一个磁盘文件,nopage域指向从磁盘进行读入的函数。
(2)onpage域为NULL。在这种情况下虚存区没有映射磁盘文件,也就是说他是一个匿名文件的映射。因此do_no_page()调用do_anonymous_page()(该函数分别处理写请求和读请求)函数获得一个新的页面。
当处理写访问时,该函数调用__get_free_page()分配一个新的页面,并把新页面添为0.最后,把页面相应的表项置为新页面的物理地址,并把这个页面标记为可写和脏两个标志。
相反,当处理读访问时。给进程一个填充为0的页面要比给它一个由其他进程填充的旧页面更为安全。Linux在请求调页方面做的更深入一些。没有必要立即给进程分配一个填充为0的新页面。我们可以给他一个现有的称为“零页”的页,这样可以进一步推迟页面的分配。“零页”在内核初始化期间被静态分配,并存放在empty_zero_page变量中(以俄国由1024个长整数的叔祖,并用0填充),因此页表象被设为零页的物理地址。由于零页被标记为不可写,如果进程试图写这个页,则写时复制机制被激活。当且仅当在这个时候,进程才获得一个属于自己的页面并对其进行写。
3.写时复制
写时复制是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程空间,而是让父进程和子进程共享一个拷贝,只有在需要写入的时候数据才会复制,从而使各个进程拥有自己的拷贝。也就是说,资源的复制只有在需要写入的时候才进行,在此之前,以只读方式共享,这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候。有时共享页根本不会被写入,例如fork后直接调用exec,就无需复制父进程的页了。fork的实际开销就是复制父进程的页表以及给子进程创建唯一的PCB。