本文是基于Linux0.11源码来叙述该功能 取之于互联网 用之于互联网
页异常中断处理程序(中断 14),主要分两种情况处理。
- 是由于缺页引起的页异常中断, 这需要通过调用 do_no_page(error_code, address)来处理;
- 是由于页面写保护引起的页异常,此时调用页写保护处理函数 do_wp_page(error_code, address)进行处理。 函数参数中的出错码(error_code)是由CPU 自动产生并被压入堆栈的,出现异常时访问的线性地址是从控制寄存器 CR2 中取得的 CR2 是专门用来存放页面出错时的线性地址
系统调用栈
缺页异常
do_no_page()是页异常中断过程中调用的缺页处理函数。它首先判断指定的线性地址在一个进程空间中相对于进程基址的偏移长度值。如果它大于代码加数据长度,或者进程刚开始创建,则立刻申请一页物理内存,并映射到进程线性地址中,然后返回;接着尝试进行页面共享操作,若成功,则立刻返回;否则申请一页内存并从设备中读入一页信息;若加入该页信息时,指定线性地址+1 页长度超过了进程代码加数据的长度,则将超过的部分清零。然后将该页映射到指定的线性地址处
- 计算了发生异常的地址的所在页address&0xfffff000,并将该地址转换为在文件中的地址(存在段基址),需要减去段基址,基址存于current->start_code。Linux0.11中应用程序的起始地址为0,但在linux当中的逻辑地址空间中,并不是0起始而是0+段基址,其中0是偏移地址,CPU识别的是逻辑地址(逻辑地址=段基址+偏移地址),每个进程的段基址是64M的倍数(linux0.11中允许有64个进程,每个进程的逻辑地址占用64M,64M*64=4G),那么减去这个段基址后,就可以获得这个程序在磁盘中的实际地址(方便用于读取可执行文件),这个tmp在程序中称为逻辑地址的偏移地址所在页的指针
- 如果当前进程并非应用程序(即current->executable应用程序文件节点为空,内核进程)或异常地址所在页高于data段,那么申请一个空页,将空页置入页表,返回(这种情况一般是应用程序访问bss段或堆区或栈区占用新页需要申请物理内存,见下图所示
- 函数share_page(tmp),遍历进程列表,如果有其他进程(后面称为进程A)与当前进程是同一个可执行文件产生(例如可执行文件a.out里面fork出了一个子进程或a.out被N个进程分别执行)那么执行try_to_share()函数。例如下图所示,进程A、进程B及进程C都是由a.out生成,他们共用同一个文件节点(同一代码),进程A、B、C都会被轮询并作为参数执行try_to_share。
try_to_share()检查进程A是否曾经复制过出当前异常的页,如果复制过,那么将进程A的页表项复制到当前进程的页表项当中(可以理解为两个进程的同一偏移地址映射到同一物理地址),并且剥夺其写权限(因为进程是独立的资源并不共享,因此要写的时候会产生写保护异常,写保护异常会复制出同样的页,而后便可以修改数据,异常后两个进程的同一偏移地址映射到不同的物理地址,这就是写时复制),并且将页map表的引用计数mem_map[phys_addr]++(代表有几个进程在使用该页面)。 成功share_page分享页表的话,则页异常处理完毕,直接返回,否则继续处理。以下是try_to_share源码及注释,配合这段文字去看 - 如果没能共享到其他进程的页,get_free_page()从物理内存中申请新的页,计算异常地址处于可执行文件的哪一个block(1个block为1024KB,exec可执行头部占用1个block,因此代码由第二个block开始存储这都是gcc编译程序自动规划好的),根据可执行文件节点获取到该block处于硬盘中的逻辑块号(一个逻辑块可以索引1个block),使用bread_page(参数1:新分配的页基址, 参数2:当前可执行所处的块设备号, 参数3:逻辑块数组)函数从硬盘中读取出一页(4096KB),最后使用put_page()将该page放入页表映射到发生异常的逻辑地址所在页。逻辑如下图所示。
- 中断处理返回,此时程序指针会指向异常地址重新执行,而此刻,因为页表中已经映射了相应的物理页,所以不会产生异常,程序正常执行
缺页异常的好处:
- 节省可执行文件加载时间,倘若可执行文件被执行后将整个可执行文件拷贝到内存当中,这个过程是很费时且浪费CPU资源的,缺页异常可以在用到的时候才进行复制,从而做到我需要,我复制
- 节省内存,可执行文件每次执行并不一定所有的代码都一定会被执行到,因此避免了一些不必要的复制,同样做到了我需要,我复制。
写保护异常
- 先计算出发生异常的地址(逻辑地址)的页表项地址,作为参数调用un_wp_page()
- 根据页表项地址取出其值,即得到发生异常的物理页地址old_page,如果物理页地址old_page高于LOW_MEM并且只有当前进程占用(这种情况),给予该物理页对应的页表项写权限,重载页表后退出中断处理,一般这种情况成立的话是由于另一个进程已经复制了新的页(另一个进程先于当前进程进行了写时复制操作,所以当前进程不需要复制,因为他独享该物理页所以直接开启权限即可。
- 如果Step2中的条件未成立,那么分配一个新的物理页new_page,如果发生异常的物理页地址old_page大于LOW_MEM(内核代码及所使用的数据,如显存),那么将该物理页的引用计数减一(就是代表不共享物理页了,因为进程资源不共享,所以这个时候不能共享了,得分家),将新的物理页new_page的放入页表(异常地址所对应的页表项)并将读/写位与有效位置位,重载页表,并将old_page的整页内容复制到new_page之中。下图可以直观地看到,两个进程在其中一个进程需要写时,复制了新的物理页,这样就不会污染另一个进程的数据了。
执行写保护异常(写时复制)好处是: - 节省CPU资源,避免在fork的时候占用CPU去进行复制
- 节省物理内存资源,避免不必要的复制,例如应用程序的代码是不会改变的,改变的只有数据(甚至有些数据从头到尾都不会被写),如果一开始fork()时将代码也复制会造成物理内存的浪费