Linux页出错处理及需求加载,写时复制源码分析 (Linux OS 操作系统)

https://blog.csdn.net/darker0019527/article/details/103320942

https://blog.csdn.net/darker0019527/category_9482371.html

 

写时复制
若干个进程都是读一个页面数据时,则共享这一个页面,不需要复制这个页面。(节约内存并加快进程创建速度),当某个进程想要对这个页面进行写操作时(修改数据),会影响共享这个页面的其他进程,这时才把页面进行复制(分别持有不同的页面),这时进行写操作不会影响到其他进程。
只有在写的时候才复制。

需求加载
加载一个进程时,不会把这个进程的所有数据全部加载到内存,而是仅分配必要的内存,没有给代码段和数据段分配内存,那么程序运行起来后,由于在对应的地址中没有找到代码或数据就会引发缺页异常,这时操作系统会把相应的代码或数据加载到内存,然后执行。
只有才需要的时候才加载。

上面两个特性分别对应了两个异常处理(写时复制对应写保护异常,需求加载对应缺页异常),这两个异常是Linux的两种页出错情况。如下图:


在看源码之前先简单描述一下,写时复制及需求加载的处理过程:
写时复制: 当使用fork函数创建一个子进程时,父子进程共享父进程的内存(不为子进程复制一份父进程的内存),并且将父进程的内存区域访问权限置为只读(父子进程都是只读),这样只要父/子进程其中任何一个打算向这块内存中写数据时,就会引发写保护异常(权限是只读),然后操作系统处理这个异常,把页面复制一份给进程写操作的进程,然后重新执行写操作。
需求加载: 上边介绍需求加载时已经说过了,不再费话。

源码分析:
这里只描述大体过程,不会对细节进行过多的描述,本文主要是分析页出错的处理,即写保护异常及缺页异常的处理过程,不对其他函数(如fork()等)进行分析,只描述出这些函数执行完的结果。

现在有进程A(持有一块内存),执行fork函数后,出现子进程B,fork函数会把A持有的内存页面置为只读(即A,B都只读这个页面),那么不管谁朝这个页面中写数据都会引发异常(14号异常)。 同样,缺页也是引发14号异常。(写保护和缺页都属于页出错)

14号异常对应的处理程序如下图:

kernel/traps.c [代码已删减]

引发14号异常那么就会执行page_fault这个处理函数;

mm/page.s [代码已删减]
当引起异常时(写保护异常,缺页异常都执行page_fault),在栈顶上会存在出错码(用于判断是写保护异常还是缺页异常),把出错的地址(虚拟地址)放到cr2寄存器中。
15行把出错码放到eax寄存器中,17行把出错的虚拟地址放到edx中,然后压栈,当作参数。
然后根据eax的值,也就是出错码来判断这是写保护异常还是缺页异常,testl $1, %eax, 就是拿出错码的最后一行(页存在位)和1比较,如果该位为0,那么就是缺页了(不存在),执行do_no_page(缺页处理函数),否则为写保护异常,执行do_wp_page(写保护处理函数)。
加上18,19行的参数压栈,那么这两个异常处理函数的形式应该是:

1  do_no_page(err_code, addr);
2  do_wp_page(err_code, addr);

下面先看实现写时复制的do_wp_page函数,前边已经描述过场景了,进程A(持有一块内存),执行fork函数后,出现子进程B,fork函数会把A持有的内存页面置为只读(即A,B都只读这个页面),那么不管谁朝这个页面中写数据都会引发异常(14号异常)。
假设现在B进程写数据时引发了写保护异常。

进入do_wp_page处理:
如果不加说明,以下函数都在 mm/memory.c文件中,均已删减。

可见,调用了up_wp_page, 本文不对细节进行过多的分析(比如参数中对地址的计算),计算后的结果是address(出错的虚拟地址)对应的页表的物理地址。

先申请了一块内存(引发写保护后要为写操作的进程申请一块内存(准备复制),由于此时为B进程,所以*table_entry = new | 7, 是修改了B进程了表项(每个进程有自己的页表),或上7,也就是把倒数第1,2,3位全部置1, 页表项结构如下:

 

倒数第1,2,3位分别为U/S, R/W, P, 可见把R/W位置1,表示可写(进程B对新申请的这块内存可写),然后copy_page就是在搬运内容(复制,此时才复制;在创建B进程的时候不复制,直到B进程进行写操作时才复制),最后重新执行写指令。

在写操作之前,AB共享

当B的写保护处理完成,内存情况如下:


注意此时A进程对其内存仍然为只读,如果A进程进行写操作还会引发写保护异常,那么怎么处理呢,下面看完整的up_wp_page()函数:

mem_map数组记录了有几个进程对相应内存的引用。比如,A,B共享一块内存时,该内存在mem_map数组中的值为2,表示有两个引用,当B处理写保护异常时,不会进入226-230行,引用不为1, 为2,当到234行时,引用减1, 因为此时进程B申请了一块新的内存,不在继续引用与A共享的内存了,所以A的内存块此时引用减少为1.(mem_map相应位置值为1)。
那么A引发写保护异常后不会像B一样到231行申请内存然后复制,而是进入226-230,(因为此时mem_map引用值为1满足条件),那么就直接把页表项倒数第二位置1(R/W=1,表示可写),然后刷新,返回,继续使用原内存,而不用申请内存,因为此时只有一个进程A在引用这块内存。

以上就是写时复制的过程。

下面看需求加载:
过程:访问的页面不存在(页表项P位为0),要去文件中把代码或数据加载到内存。
和写时复制都需要页出错异常一样,都会进入page_fault那么汇编函数,然后压入参数,只不过调用的是do_no_page,处理函数,如下:

mm/memory.c [代码已删减]
首先申请一页内存(一页大小4KB),368行。
然后计算相应地址的代码或数据在硬盘上存放的位置(在哪个数据块中),
370-374,把代码和数据所在的块及后面三个数据块(一共四个,此版本使用的文件系统一个数据块为1K,而申请的一页为4K,所以可以读入4页),
然后put_page把物理地址缺页的虚拟地址address映射到物理地址page(get_free_page返回的是空闲页的物理地址),本来虚拟地址address是没有对应的物理地址(物理页,所以才引起缺页异常)的,现在把相应的代码或数据加载到内存后,与虚拟地址建立映射,那么就可以通过虚拟地址访问到这个物理页,就可以取出其中的代码和数据,从而继续执行了。只有在需要的时候(引起缺页异常,确实需要执行这部分代码的时候,不执行或不使用代码/数据时,代码/数据就老老实实躺在文件系统(硬盘)上)才加载。

如有错误和遗漏,还望指出。

参考:《Linux内核完全剖析》
源码:Linux 0.11
————————————————
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值