Linux0.11系统异常之页异常

前言

本文是基于Linux0.11源码来叙述该功能。本文就不贴Linux0.11的源码了,仅介绍一下逻辑,需要源码的可以在oldlinux.org上自行下载。

页异常介绍

当CPU开启页表功能后,若出现页访问权限不足或者页不存在,便会触发页异常,异常就是所谓的中断,在异常中断处理程序处理完后,返回原点重新执行先前触发异常的指令。

页异常功能

可能有人会认为,页异常发生后,系统不应该panic了吗?实际并非如此,Linux0.11下的页异常设计非常巧妙,利用页异常做到了进程的写时复制(页写保护异常),应用程序的代码或数据拷贝及堆栈的延伸分配(缺页异常),这一切都是被设计好的,并非如字面想象中的是一种故障异常。

页异常入口

函数set_trap_gate(14,&page_fault)将中断服务程序地址填入中断描述符表第14项,异常发生时,CPU会寻找中断描述符第14项,并进入该地址处理异常。

页异常流程

  • 异常发生瞬间,CPU会将错误码push到内核堆栈当中并将触发页异常的地址(逻辑地址)存入CR2寄存器,错误码用于区分是缺页异常(1)或是写保护异常(0)。
  • 随后CPU会跳转到page_fault函数处理异常,将数据段切为内核段后,根据堆栈中的错误码判断是缺页异常还是写保护异常,将错误码和CR2的异常地址写入堆栈作为调用参数,如果是缺页异常则执行do_no_page,否则是写保护异常执行do_wp_page

缺页异常

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在程序中称为逻辑地址的偏移地址所在页的指针。

    address &= 0xfffff000;
    tmp = address - current->start_code;
    
  2. 如果当前进程并非应用程序(即current->executable应用程序文件节点为空,内核进程)或异常地址所在页高于data段,那么申请一个空页,将空页置入页表,返回(这种情况一般是应用程序访问bss段或堆区或栈区占用新页需要申请物理内存,见下图所示)。
    在这里插入图片描述

  3. 函数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源码及注释,配合这段文字去看。

    static int try_to_share(unsigned long address, struct task_struct * p)
    {
    	unsigned long from;
    	unsigned long to;
    	unsigned long from_page;
    	unsigned long to_page;
    	unsigned long phys_addr;
    
    	from_page = to_page = ((address>>20) & 0xffc);//取偏移地址的页目录项地址
    	from_page += ((p->start_code>>20) & 0xffc);//源页目录项地址:偏移地址加上进程A段基址,即逻辑地址
    	to_page += ((current->start_code>>20) & 0xffc);//目的页目录项地址:偏移地址加上当前进程段基址,即逻辑地址
    /* is there a page-directory at from? */
    	from = *(unsigned long *) from_page;//取源页目录项
    	if (!(from & 1))//确认该页目录项是否存在
    		return 0;//不存在则返回,无法共享
    	from &= 0xfffff000;//取源目录项的页表地址
    	from_page = from + ((address>>10) & 0xffc);//源页表项地址
    	phys_addr = *(unsigned long *) from_page;//取源页表项值
    /* is the page clean and present? */
    	if ((phys_addr & 0x41) != 0x01)//如果源页表项值不是clean或无效
    		return 0;//返回
    	phys_addr &= 0xfffff000;//屏蔽标志位,获得页所在的物理地址
    	if (phys_addr >= HIGH_MEMORY || phys_addr < LOW_MEM)
    		return 0;
    	to = *(unsigned long *) to_page;//取目的页目录项值
    	if (!(to & 1))//页目录项是否无效
    		if (to = get_free_page())//无效则分配新页表,获取页
    			*(unsigned long *) to_page = to | 7;//填充目的页目录项,指向新页表
    		else
    			oom();//无法获取页,宕机
    	to &= 0xfffff000;//取目的页表地址值
    	to_page = to + ((address>>10) & 0xffc);//目的页表项地址
    	if (1 & *(unsigned long *) to_page)//目的页表项如果无效
    		panic("try_to_share: to_page already exists");//宕机
    /* share them: write-protect */
    	*(unsigned long *) from_page &= ~2;//源页表项取消写权限
    	*(unsigned long *) to_page = *(unsigned long *) from_page;//目的页表项=源页表项,映射向同一物理地址
    	invalidate();//同步
    	phys_addr -= LOW_MEM;//物理地址减去低端地址
    	phys_addr >>= 12;//计算出mem_map索引
    	mem_map[phys_addr]++;//记录该物理地址页引用计数+1
    	return 1;//返回1,代表share成功
    }
    
  4. 如果没能共享到其他进程的页,get_free_page()从物理内存中申请新的页,计算异常地址处于可执行文件的哪一个block(1个block为1024KB,exec可执行头部占用1个block,因此代码由第二个block开始存储这都是gcc编译程序自动规划好的),根据可执行文件节点获取到该block处于硬盘中的逻辑块号(一个逻辑块可以索引1个block),使用bread_page(参数1:新分配的页基址, 参数2:当前可执行所处的块设备号, 参数3:逻辑块数组)函数从硬盘中读取出一页(4096KB),最后使用put_page()将该page放入页表映射到发生异常的逻辑地址所在页。逻辑如下图所示。
    读取页并建立映射

  5. 中断处理返回,此时程序指针会指向异常地址重新执行,而此刻,因为页表中已经映射了相应的物理页,所以不会产生异常,程序正常执行。

缺页异常的好处:

  • 节省可执行文件加载时间,倘若可执行文件被执行后将整个可执行文件拷贝到内存当中,这个过程是很费时且浪费CPU资源的,缺页异常可以在用到的时候才进行复制,从而做到我需要,我复制。
  • 节省内存,可执行文件每次执行并不一定所有的代码都一定会被执行到,因此避免了一些不必要的复制,同样做到了我需要,我复制。

写保护异常

do_wp_page函数为写保护异常的主要处理程序,执行的操作如下:

  1. 先计算出发生异常的地址(逻辑地址)的页表项地址,作为参数调用un_wp_page()

    void do_wp_page(unsigned long error_code,unsigned long address)
    {
    	un_wp_page((unsigned long *)
    		(((address>>10) & 0xffc) + (0xfffff000 &
    		*((unsigned long *) ((address>>20) &0xffc)))));
    
    }
    
  2. 根据页表项地址取出其值,即得到发生异常的物理页地址old_page,如果物理页地址old_page高于LOW_MEM并且只有当前进程占用(这种情况),给予该物理页对应的页表项写权限,重载页表后退出中断处理,一般这种情况成立的话是由于另一个进程已经复制了新的页(另一个进程先于当前进程进行了写时复制操作,所以当前进程不需要复制,因为他独享该物理页所以直接开启权限即可)。

    ...
    old_page = 0xfffff000 & *table_entry;
    if (old_page >= LOW_MEM && mem_map[MAP_NR(old_page)]==1) {
    	*table_entry |= 2;
    	invalidate();
    	return;
    }
    ...
    
  3. 如果Step2中的条件未成立,那么分配一个新的物理页new_page,如果发生异常的物理页地址old_page大于LOW_MEM(内核代码及所使用的数据,如显存),那么将该物理页的引用计数减一(就是代表不共享物理页了,因为进程资源不共享,所以这个时候不能共享了,得分家),将新的物理页new_page的放入页表(异常地址所对应的页表项)并将读/写位与有效位置位,重载页表,并将old_page的整页内容复制到new_page之中。下图可以直观地看到,两个进程在其中一个进程需要写时,复制了新的物理页,这样就不会污染另一个进程的数据了。
    在这里插入图片描述

执行写保护异常(写时复制)好处是:

  • 节省CPU资源,避免在fork的时候占用CPU去进行复制
  • 节省物理内存资源,避免不必要的复制,例如应用程序的代码是不会改变的,改变的只有数据(甚至有些数据从头到尾都不会被写),如果一开始fork()时将代码也复制会造成物理内存的浪费。

总结

可以看到页异常巧妙地用在了应用程序的执行上,做到了能共享则先共享,若有需要,再进行复制,避免了不必要的浪费。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值