内存管理
计算机操作系统
版权声明:本文章参考了塔嫩鲍姆的《现代操作系统》、汤子瀛的《 计算机操作系统》。未经作者允许,严禁用于商业出版,否则追究法律责任。网络转载请注明出处,这是对原创者的起码的尊重!!!
分层存储体系:高速缓存—>内存—>外存,造价从高到低,速度由快到慢,容量由小到大,这样的存储体系。
存储管理器:OS中管理分层存储体系的部分,任务是管理内存,记录已使用和未使用的内存空间,分配内存,回收内存。
1 无存储器抽象
无存储器抽象就是由程序直接访问物理内存。其缺点:
- 通常不可装入多道程序。
- 程序出错时或者恶意攻击时,可能摧毁操作系统。
- 无存储器抽象中可以实现多线程并发,但是不能满足同时运行无关程序的要求。
1.1 运行多道程序
- 无存储器抽象,也可运行多道程序,方法如下:
- 把当前内存的所有内容保存到磁盘,再读入下一个程序,保证任意时刻只有一个程序在内存中。
- 将内存分块,每块内存设置保护键,程序具有PSW,当程序访问保护键与其PSW不匹配的内存时,则出错。其缺点是:使用了绝对物理地址,导致程序中的逻辑地址与装入内存后的物理地址不同。解决办法是:静态重定位。
- 在嵌入式中缺少内存抽象很常见,因为要运行的程序是确定的。
- 要想多个程序同时存在内存中,必须要解决保护和重定位两个问题。
重定位:即装入时对目标程序的指令和数据地址进行修改。
静态重定位:重定位在程序装入时一次完成,以后不再改变。
动态重定位:重定位不是在装入时完成,而是在要执行时。
2 地址空间
地址空间就是内存可以用于寻址内存的一套地址集合。每个进程都有自己的地址空间。进程是抽象CPU以运行程序,地址空间则是抽象内存。
2.1 动态重定位
动态重定位就是把每个进程的地址空间映射到物理内存的不同部分。。
在CPU中配置基址寄存器和界限寄存器。程序的起始物理地址放在基址寄存器中,程序的长度放在界限寄存器中。当进程访问内存时,CPU硬件会在地址发送到内存总线之前,将基地址加上,同时检查地址是否越界。缺点:每次访问内存都需要做加法和比较。
2.2 交换技术
提出原因:内存超载,内存不能同时载入所有进程。
解决方法:
- 交换技术:将一个进程完整调入内存,运行一段时间后存回磁盘。空闲的进程存在磁盘上。
- 虚拟内存:每个程序都有自己的地址空间,这个空间被分为许多块,每一块称为一个页面。每一页都是连续的地址范围。这些页面被映射到物理内存。但并不是所有的页面都必须装入内存才能运行。当程序引用了在物理内存的地址空间时,由硬件执行映射,当引用不在物理内存的地址时,引发缺页中断,由操作系统装入缺失的页面并重启指令。
交换技术在换入进程时,要通过软件或硬件对其地址重定位。
内存紧缩:交换技术会在内存中产生多个空闲区,通过把所有进程向下移动,可以合并空闲区。但是代价非常高。
进程所需内存空间不确定时,换入时要分配一些额外的空间。而换出时则只换出实际使用的空间。
2.3 空闲内存管理
主要分为使用位图和使用链表进行管理。
2.3.1 使用位图管理
内存按照一定大小划分为若干单元。每个单元用一位0或1表示占用或空闲。缺点:当分配内存时,必须在位图中找到连续的空闲单元,查找操作是耗时的。
2.3.2 使用链表管理
维护一个记录已分配内存和未分配内存段的链表。每个节点包含:空闲或占用的标志、起始地址、长度、指向下一个节点的指针。如果是双链表则还包括指向上一个节点的指针。分配内存的方法包括:
- 首次适配法:沿链表搜索,直到找到一个大于或等于所需大小的区域。速度非常快。
- 下次适配法:工作方式和首次适配法一样,不同的是找到合适空闲区时记录下来,以便下次从上次结束处开始搜索。性能略低于首次适配法。
- 最佳适配法:搜索整个链表,找到最接近所需大小的区域。由于要搜索整个链表,所以性能更差,且会差生大量无用的小空闲区。
- 最差适配法:总是分配最大的空闲区,从而使新的空闲区比较大而可以继续使用。
- 快速适配法:为每个常用的不同大小的空闲区维护一个单独的链表。
- 伙伴系统:
- 哈希算法:
方案改进:
- 为空闲区和占用区维护不同的链表。缺点:复杂度增加,由于增删节点内存释放速度也变慢。
- 在1的基础上,对空闲区链表排序。此时首次适配与最佳适配一样,而下次适配毫无意义。
- 将利用空闲区来存放空闲区链表的节点信息。
快速适配和按大小排序的空闲区链表都有一个缺点:当进程换出或终止,寻找其相邻块,看是否可以合并,是非常费时的,如果不合并,则会有较多的外部碎片。
3 虚拟内存
覆盖技术:有程序员将程序分隔成片段,称为覆盖。开始执行时由覆盖管理模块装入覆盖0,执行完后由覆盖0通知管理模块装入覆盖1到覆盖0的上方或占用覆盖0。缺点是容易出错。
虚拟内存基本思想:每个程序都有自己的地址空间,这个空间被分为许多块,每一块称为一个页面。每一页都是连续的地址范围。这些页面被映射到物理内存。但并不是所有的页面都必须装入内存才能运行。当程序引用了在物理内存的地址空间时,由硬件执行映射,当引用不在物理内存的地址时,引发缺页中断,由操作系统装入缺失的页面并重启指令。操作系统重新转入页面时要做两处修改,把换出页面标记为“不在”,把换入页面标记为“在”。
3.1 分页
虚拟内存技术中,程序所用的地址是虚拟地址,其集合称为虚拟地址空间。虚拟地址要通过内存管理单元MMU映射为物理地址。
虚拟地址空间按照固定大小分为若干页面。物理内存中对应的块叫做页框。内存与磁盘的交换总是以页面为单位进行。
用一个“在/不在”位来记录页面是否在内存中。
MMU原理:虚拟地址被分为高位的虚拟页号(页表索引)和低位的偏移量。当虚拟地址送到MMU时,先通过虚拟页号找到页框号,然后复制到输出寄存器的高位,再将虚拟地址的偏移量复制到寄存器的低位。然后将输出寄存器的内容作为物理地址送往内存总线。
3.2 页表
页表:页面与页框的对应关系。
页表项:页表中的一个记录。包括页框号、有效位、保护位、修改位、访问位、高速缓存禁止位。
3.3 加速分页过程
快速硬件寄存器:开始执行时将页表装入寄存器,运行中不需访问内存。缺点:页表大时,代价高。并且每次上下文切换都需重载页表。
TLB理论:大多数程序总是对少量页面进行多次访问。
硬件TLB:通常在MMU中,记录的少量的常用的页表项,当虚拟地址送入MMU时,先在TLB中查询。如果有则取出页框号。如果没有,则进行正常的页表查询,接着从TLB中淘汰一个表项,将淘汰的表项中的修改位和访问位复制到页表中对应页表项,其他值舍弃,之后将内存中的找到的新的页表项装入TLB。
软件TLB:TLB由操作系统显式的装载,当TLB访问失效,不再由MMU去页表中查询,而是交给操作系统解决。优点是MMU变得简单。缺点是TLB可能不再内存中,带来额外的TLB失效。解决方法是在内存的固定位置维护一个大的TLB表项的软件高速缓存。当页面在内存中而不再TLB中时叫做软失效。当页面不再内存中时叫做硬失效。软件TLB管理策略:减少TLB失效的同时降低TLB失效的开销。
多级页表:原因是避免将页表全部保存在内存中,特别是从不需要的页表。原理:32位虚拟地址被划分为10位的PT1,10位的PT2和12位偏移量。MMU先用PT1找到二级页表,再用PT2从二级页表中找到页框号。虽然程序有上百万的页面,但实际需要的只有顶级页表以及04M(正文段)、4M8M(数据段)和顶部的4M(堆栈段)的二级页表。其其余1021表项设置为“不在”,当访问它们时引发缺页中断。
倒排页表:物理内存的页框对应一个表项。缺点是转换慢。解决办法是:使用TLB,事先对页表建立一张散列表,当TLB失效时,进行快速搜索。
4 页面置换算法
当发生缺页中断时,操作系统必须在内存中选择一个页面将其换出,并换入所需的页面。
最优页面置换算法:每个页面用该页面首次被访问时所需执行的指令数目作为标记。选标记最大的页面换出。评价:唯一问题是无法实现,但可作为基准。
最近未使用页面置换算法(NRU):系统检查每个页面的访问位和修改位,分为4类:
第0类:没有被访问,没有被修改
第1类:没有被访问,已修改(由第3类在时钟中断后清除访问位产生)
第2类:已被访问,没有被修改
第3类:已被访问,已被修改
然后随机的从类编号最小的非空类中跳一个页面换出,评价:易于理解和实现,但是性能不是最好,是LRU很粗糙的近似。
先进先出页面置换算法(FIFO):由OS维护一个当前内存中页面的链表,最早的在表头,最新的在表尾。发生缺页中断时,淘汰表头的页面,并把换入的页面加在表尾。评价:可能抛弃重要的页面,在链表中移动页面效率低。
第二次机会页面置换算法:在FIFO算法中,换出页面时,检查访问位,如果没有访问过则换出,否则,清除访问位,并放入链表尾部,继续搜索直到找到一个页面换出。评价:是FIFO的改进,在链表中移动页面效率低。
时钟页面置换算法:将页面放在环形链表中,一个指针指向最老的页面。缺页中断时,检查指针所指页面的访问位,如果未访问,则换出该页面,指针前移一个位置,否则,清除访问位,指针前移一个位置,评价:现实的。
改进的时钟页面置换算法:在时钟页面置换算法中选择淘汰页面时,采用NRU算法。
最近最少使用页面算法(LRU):基于一个理论:在前几条指令中频繁使用的页面,在后面的几条指令很可能用到。因此,在缺页中断时,置换未使用时间最长的页面。评价:很优秀,但实现代价很高。
硬件实现:
第一种:设置一个64位硬件计数器,每条指令执行后就加1,每次访问内存后,将计数器的值保存在页面中的对应域。缺页中断时,操作系统选择计数器值最小的页面换出。
第二种:对N个页框,设置初值为0的N*N的矩阵,当访问页框K时就,将K行设为1,k列设为0.却也中断时,选择二进制数最小的行号的对应页面换出。
第三种:用一个特殊的栈保存当前使用的页面的页面号。每当进程访问某页面时,若页面号在栈中,则取出,压入栈顶;若不在栈中则直接压入栈顶。因此,栈底是LRU所选择的淘汰页面的页面号。
软件实现:
最不常用页面置换算法(NFU):为每个页面设置一个初值为0的软件计数器,每次时钟中断,由OS将访问位加到计数器上,缺页中断时,置换计数器值最小的页面。评价:可能受前面的影响而换出有用的页面,是LRU相对粗略的近似软件模拟。
老化算法:在NFU的基础上,每次加访问位时,计数器先右移一位,再将访问位加载计数器的最左端。与LRU的区别,一是只记录时钟周期而不是页面访问次数;二是计数器的位数有限,可能有溢出归0的情况。评价:非常近似LRU的有效算法。
工作集页面置换算法:原理就是缺页中断时,淘汰不在工作集中的页面。
请求调页:页面在需要时调入,而不是预先装入。程序开始执行时,内存中没有任何页面。
颠簸:每执行几条指令就发生一次缺页中断。
工作集定义:第一种定义:一个进程当前正在使用的页面叫做工作集。第二种定义:在任一时刻,最近k次内存访问所访问过的页面集合。第三种定义:在一段时间t内所访问过得内存页面的集合。如果工作集大于内存,就会发生大量缺页中断。
基于第二种定义的WS实现:设置长为k的移位寄存器,每次内存访问就左移一位,然后插入访问的页面号,寄存器的内容就是工作集,缺页中断时,对其排序和去重。
基于第三种定义的WS实现:如果页面被访问则写入当前时间,如果没被访问则计算其生存时间并与t作比较,若大于t则不再工作集中,若小于等于t则选择生存时间最长的页面。评价:第二种定义,维护移位寄存器,从未被使用。第三种定义的实现开销也很大。
工作集时钟页面置换算法(WSClock):将页面装入循环链表中。每个表项包括上次使用时间、访问位、修改位。缺页中断时,检查指针所指页面的访问位,如果访问过则前移一个位置,并清除访问位;如果没访问过且生存时间大于t并且是干净的则换出,若是脏的则前移一个位置。如果指针回到起点且有脏的页面换出遇到的第一个干净页面,如果指针回到起点且所有页面都是干净的则置换当前页面。评价好的有效的。
5 分页系统中的设计问题
5.1 局部与全局分配策略
在多道程序中,如果某个进程缺页中断,若换出的页面只是在进程的页面中选择则是部分配策略,若是在内存中的所有页面中选择则时全局分配策略。一般说来,全局策略优于局部策略。原因是局部算法,当工作集增大即使有大量空闲内存仍然会导致颠簸,当工作集缩小又浪费了内存。
另一种途径是为进程分配页框:定期确定进程数目并分配相等数目的页框,多余的作为公用的优先换出的页面。其改进方法是按照进程大小的比例分配页框,并规定一个进程的最小页框数。
缺页中断率算法(PFF):一种依据每秒缺页中断数来管理内存动态分配的算法, 只控制分配集的大小,不涉及具体的页面置换。
注意:一些页面置换算法可以适用于局部和全局算法,如FIFO、LRU;一些只能适用于局部,如工作集和WSClock算法。
5.2 负载控制
一点所有进程的组合工作集超出内存,就可能发生颠簸。唯一的办法是减少内存中的进程数,将一些进程交换到磁盘中,直到颠簸停止。在选择交换到磁盘的进程时需要考虑进程特性(CPU密集或IO密集)以避免CPU空闲。
5.3 页面大小
页面大小不受硬件限制。可将多个页框分配给一个页面。
内部碎片:进程拥有但却未装满的页面。外部碎片:进程间无法分配利用的内存空间。
最佳页面大小需要考虑的因素:
- 小页面意味着更少的内部碎片。
- 程序炫耀的内存更小。
- 页面越小,页表越大,装入相同数量页面的时间越长,尤其是使用硬件寄存器存放页表的时候。
- 小页面更能充分利用TLB的空间。
,s为进程平均大小,p为页面大小,e为页表项大小。当S=1MB,e=8B时,最优页面大小为4KB,商用计算机页面大小为512B~64KB。以前典型值为1KB,现在常见的是4KB或8KB。
5.4 分离的指令空间和数据空间
当地址空间很大时,将指令和数据放一起,当地址空间较小时,则需要将指令和数据放在不同的独立的地址空间中,叫做I空间和D空。这使可用的地址空间加倍。
5.5 共享页面
当同一用户运行同一程序时,为了提高效率,一些页面可以共享,比如只读页面,一些页面则不可共享,如数据页面(但利用特殊的机制也可共享)。
不论是否支持分离的I空间和D空间,都可以共享指令或库,但如果支持,则会更加简单。
问题:当两个进程共享某些页面,而其中一个进程终止或切换时,会导致另一进程颠簸。解决办法:用一些专门的数据结构来记录共享页面。在清除页面时对其做检查。
数据共享的方法,在Unix的fork函数中采用写时复制,即调用fork时,子进程共享父进程的进程空间,但拥有自己的页表,指向同一页面集合,当要对某页面修改时,触发只读保护,这时才真的复制该页面。
5.6 共享库
若多个程序共享一些代码段。则可以将这些代码段提取出来,叫做库。
静态库:链接器在链接时将调用了的未定义的外部函数加载到二进制文件中,库中存在但是未调用的不会加载,之后可脱离静态库运行。共享库(动态库):与静态库的区别是,链接器在链接时不是加载调用的函数而是加载一小段能在运行时绑定被调函数的存根程序,同样未调用的函数不会加载,之后程序不能脱离动态库独立运行。评价:减小程序的大小,便于更新库和debug。问题:同一个共享库被不同程序定位在不同位置,如果使用绝对地址,内核将采用写时复制技术为每个共享的进程创建一个该库的副本,并进行重定位。解决办法:使用相对地址,也叫做位置无关代码,gcc中使用-fPIC选项。
5.7 内存映射文件
共享库是内存映射文件的特例。内存映射文件:进程可以通过一个系统调用,将一个文件映射到映射到其虚拟地址空间的一部分。此后可以将文件当做一个大的内存中的数组来访问,而不用进行传统IO读写。此外如果不同进程映射同一文件,则可以进行高带宽通道的进程间通信。
5.8 清除策略
如果每个页框都被占用且修改,则换入页面时必须将其写回磁盘。
分页守护进程:定期唤醒检查空闲页框数,如果过少则使用预置的页面置换算法选择一些页面,换出内存,或写回磁盘(被修改过),再移入空闲页框缓冲池,以便紧接着的访问能够快速恢复。评价:保留一定数量的空闲页框供给比使用所有页框并在需要时寻找一个页框有更好的性能,空闲页框是干净的,可以减少写回磁盘的开销。
采用双指针时钟来实现:再时钟算法基础上增加一个指针,由分页守护进程控制,如果是脏页面则写回磁盘,前移一个位置,否则只前移一个位置。
5.9 虚拟内存接口
程序员可以对内存映射进行控制,目的是为了多个进程共享部分内存,从而实现高带宽的进程间通信。
分布式共享内存:允许网络上的多个进程共享同一页面集合。
#6有关实现的问题
对前面的设计中遇到问题,在多方案中选择。
6.1 与分页有关的工作
OS与分页有关的工作,进程创建时,进程执行时,缺页中断时,进程终止时。
进程创建时:
- 确定指令和数据的大小,创建页表,并为页表分配
空间并初始化
- 进程换出时,页表不需在内存,进程执行时,必须在内存。
- 在磁盘交换区分配空间,以便进程换出,用指令和数据初始化磁盘交换区,以便缺页时调入。
- 某些OS直接从磁盘的可执行文件对程序正文分页。
- 将页表和磁盘交换区的信息存入进程表。
进程执行时:
- 为新进程重置MMU,刷新TLB。
- 将新进程页表置为当前页表。(通过复制页表或者将其指针存入寄存器)
缺页中断时:
- 读寄存器确定引发缺页中断的虚拟地址,计算并在磁盘定位所需页面。
- 确定页框和换出页面,将页面装入页框。
- 回退程序计数器,重新执行引发缺页中断的指令。
进程终止时:
- 释放页表、页面和页面在硬盘上所占空间。
- 对共享某页面的进程都终止时,才释放内存和磁盘上的页面。
6.2 缺页中断处理
- 硬件陷入内核,在堆栈中保存程序计数器。
- 启动汇编代码例程以(将操作系统作为一个函数)保存通用寄存器和易失信息。
- OS通过读取计数器或者分析指令来确定需要的虚拟页面。
- 检查引发缺页中断的虚拟地址是否有效,并检查存取与保护是否一致。如果不一致则发信号或杀死进程,如果地址有效且保护一致,检查是否有空闲页框,如果没有空闲页则则根据页面置换算法淘汰一个页面。
- 若选择的页框是脏的,则要写回磁盘,引发一次上下文切换,让其它进程运行直到写完成。该页框被标记为锁定,以免被其它进程占用。
- 一旦页框干净,OS在磁盘上定位页面并装入,而进程仍然挂起。
- 磁盘中断发生表明页已被装入,页表更新,页框标记为正常。
- 恢复缺页中断指令以前的状态,程序计数器重新指向该指令。
- 调度引发缺页中断的例程,操作系统返回调用它的汇编例程。
- 汇编例程恢复寄存器和其它信息,返回用户空间。
6.3 指令备份
缺页中断恢复后,OS需要重启引发中断的指令。但一条指令包含操作码和操作数,缺页中断可能发生在取操作码或者取操作数的时候,当发生在取操作数时,OS无法判断指令到底从哪里开始。解决办法:设置隐藏内部寄存器,每条指令 执行前将程序计数器存入寄存器中。
6.4 锁定内存
当一个进程挂起以等待IO完成,而正在运行的进程发生缺页中断,如果采用全局算法,IO缓冲区的页面可能被换出。解决办法:锁住IO缓冲区的页面或者在内核缓冲区中完成IO操作,再复制到用户页面。
6.5 后备存储
静态交换分区:页面被换出时,事先在磁盘分配一个交换区,每个页面在交换区有固定的位置。写回地址=偏移量+交换区起始地址。问题是进程可能增大。解决办法:为正文、数据、堆栈分别保留交换区,预留多余的空间。初始化交换分区的办法:一是将整个进程映像复制到交换区,二是将整个进程装入内存,在需要时换出。
动态备份:不预先分配交换区,换出页面时分配,换入页面时回收。问题是每个进程必须有一个记录页面所在磁盘位置的表。
6.6 策略与机制分离
机制:提供某种功能。策略:决定如何调用各种功能。
存储管理系统分为:MMU处理程序、缺页中断处理程序、外部页面调度程序。其中前两者属于机制,最后的是策略。
7 分段
分段:将虚拟地址空间分为多个连续的段,每个段作为一个虚拟地址空间。特点:
- 段无固定大小,可在运行期改变;
- 每个段可以独立增大或减小而不影响其他段;
- 段是一个逻辑实体,程序员知道;
- 有助于进程间共享过程和数据;
- 简化链接单独的过程的操作,方便寻址。
7.1 纯分段的实现
分段产生外部碎片,分页产生内部碎片。分段与分页本质区别:分页定长。
7.2 分段与分页结合:MULTICS
MULTICS将每个段看做一个虚拟地址空间来进行分页,兼由分页的优点(定长和不用全部调入内存)和分段(易于编程、模块化、保护和共享)的优点。
MULTICS的地址分为两部分段和段内地址,段内地址又分为页号和页内偏移量。访问过程如下:
- 根据段号找到段描述符。
- 检查该段页表在/不在内存中,如果在则找到,如果不在产生断错误,如果违反保护位,则产生越界错误。
- 检查页表项,如果在则取出页面起始地址,如果不在则产生缺页中断。
- 偏移量加上起始地址
- 进行读/写操作。
7.3 分段与分页结合:Intel X86
X86处理器的虚拟内存的核心有两张表。LDT与GDT,每个进程有自己的LDT,用来描述程序的各段,所有进程共享GDT。用来描述系统段,包括操作系统自身。
每个段有一个选择子,包含索引(13位)、局部/全局位(1位)、特权级(2位)。CS寄存器存放代码段的选择子,DS保存数据段的选择子。
访问一个段时,把选择子装入段寄存器中,根据局部/全局位选择LDT或者GTD,然后将选择子复制到内部擦出寄存器中清除局部/全局位(1位)、和特权级,然后加上LDT或GDT的的地址,得到对应描述符的指针,找到对应的描述符(8字节)后装入微程序寄存器中。若段不存在或换出则发生一次陷阱。若段在,硬件随后根据Limit域检查偏移量是否超出段位,如果超出则发生陷阱。否则,将描述符中的32位基址与偏移量相加得到线性地址。如果禁止分页则线性地址解释为物理地址,如果允许分页,线性地址就解释为虚拟地址并通过两级页表映射到物理地址。为了加快速度,同样设置TLB。
版权声明:本文章参考了塔嫩鲍姆的《现代操作系统》、汤子瀛的《 计算机操作系统》。未经作者允许,严禁用于商业出版,否则追究法律责任。网络转载请注明出处,这是对原创者的起码的尊重!!!