内存管理
内存管理
为什么需要内存管理,而不选择直接将内存空间暴露给用户?
1、如果用户可以直接寻址到内存的每个字节,那么其可以很容易的破化操作系统。
2、使用用户直接访问内存空间的话,多个程序是十分困难的。
因此主要的问题在于保护与重定位,使用地址空间就可以达到这个目的,地址空间为程序创造了一种抽象的内存,其一个进程寻址内存的一套地址集合,每个进程都有一个自己的地址空间,并且这些地址空间独立于其他进程的地址空间。
1、基址寄存器和界限寄存器
经典的方法是使用动态重定位,通过给每个CPU配置两个特殊的硬件寄存器,基址寄存器和界限寄存器,当一个进程运行的时候程序的起始地址装入到基址寄存器,长度装入界限寄存器,程序的实际地址通过基地址+偏移地址取得。动态重定位是最简单的实现保护与重定位的方法,但是主要的缺点是每次访问内存都需要执行加法和比较运算,这都是会浪费时钟周期的。
2、交换技术
在内存中不可能会同时保存所有的进程,内存的大小是无法满足条件的,为了处理这种情况,有两种技术可以使用:
1、交换技术(swapping):把一个完整的进程调入到内存,运行一段时间后,将其存盘。
2、虚拟内存:将程序的部分调入内存。
2.1交换技术(swapping)
交换技术的最大问题在于,将进程调入到内存中时,进程时动态的增加其大小的,通常会预留一个增长空间来进行动态内存的分配,当分配不足的时候,可以通过将进程置换出内存,等到有足够的空间再置换回来来解决这个问题。如何知道有足够的空间与如何进行内存空间的分配是操作系统的责任。常见的用于管理内存的方式有:
1、位图:将内存分为多个相同大小的分配单元,作为位图的行,对分配单元继续等大小切割,使用位图的列对应分割的区域,如图
2、使用链表
主要的思想是维护一个记录一个已经分配的内存段和空闲的内存段的链表,可用的方法主要有:
1、段链表
链表的每一个表项都包含了:空闲区[H]或者进程[P]的指示标志,其实起始地址、长度与指向下个节点的指针,如图3.6 c所示表示的是3.6b的内存分布情况。使用这种内存管理方式的优点是在进程终止的时候进行内存合并会十分便捷,在进程表中通常含有指向其段链表节点指针,因此段链表使用双向链表对于内存合并更有好处,其可用检查是否可用向上合并。
按照这种内存管理的方式,那么内存分配的方式有:
首次分配:找到第一个满足进程大小的空闲区立即分配,超过的部分划为一个新的空闲区
下次适配:在首次找到空闲区后记录该位置,当下次再被换出换入到内存中的时候从上次的位置向下找空闲区,实际上效果不好,我觉得应该是有进程再前面进程终结释放处理合适的内存空间,而这种方法会直接跳过
最佳适配:扫描全局的链表,找到最合适于当前进程大小要求的空间,时效性太差,同时实际效果会产生一堆小的内存碎片,实际内存芯片的大小更大。
最差适配:在可用空间查找最大的空间分配给当前的进程,为了避免内存碎片的情况,但是也不是个好主意。
快速适配:将可用空闲内存段分为N个档次,等比的从小到达排列如1k、2k、4k、8k.....组成数组,数组的内容为一个指针,指向满足大小条件的内存区域,具体的实现可用看看STL的内存分配机制,采用的就是这种方式,但是这种方式虽说能够高效的查询到可用的内存块,但是合并内存就不容易了。使用这种方法即为快速适配。
2.2 虚拟内存
虚拟内存的需求是由于随着时间的增长,在内存中驻留一个或者有限进程是可以做到的,但是现在的进程一般超过50个,内存是不够装下所有的进程的,因此虚拟内存的主要思想是:每个进程拥有自己的地址空间,这个空间被分割成多个块,每一个块称为页或者页面,当某些页面需要使用的时候才会将需要的页面调入到内存中。
因此分页技术需要使用,由程序产生的地址是虚拟地址,其构成了虚拟地址空间,在没有虚拟内存的计算机上,其直接将虚拟地址作为实际地址,在有虚拟内存的机器上,其地址需要被送到内存管理单元(MMU)进行地址映射。虚拟地址空间按照固定大小划分成称为页面的若干单元,在物理内存中对应的单元称之为页框,页面和页框通常是一样大的。
MMU的主要原理工作原理为:进程按照顺序提交需要访问的虚拟内存地址addr,MMU首先将addr除以页面大小确定虚拟页面号n,在页表中查询虚拟页号对应的内存实际页号m,查到就确定实际地址m*page_size+addr%page_size。如果查不到就产生一个缺页中断,将需要的页面调入到内存中,同时将内存中的某些页面调出,调入页面后需要修改页表的内容,此时退出中断重新执行即可完成查找。其实二进制地址本身的高位就是页面号,低位就是偏移地址,也就是说产生缺页中断的 时候才会陷入操作系统。
在虚拟内存中由于使用了MMU查询页表需要一定的时间,主要有两个问题:
1、虚拟地址到实际地址的映射必须要快。
2、如果虚拟地址空间很大,那么页表也会很大。
1、加速分页的过程
每个进程需要维护自己的页表。相对于直接访问实际内存的过程,使用分页多了一个访问页表的步骤,但是这一步消耗了大量的时间,为了加速这个过程可用在MMU中内置一个与页表类似的转换检测缓冲区(TLB)或者说块表。
此时查询页表的过程就变为了当虚拟地址送到的时候MMU并行的与TLB中的所有的表项进行比对,找到就直接将地址发布到地址总线上。若没有则再查询页表,同时将该表项同步到TLB中,以防下次使用,这样的依据是:大多数程序总是对少量的页面做多次的访问,而不是相反。
在使用页表和TLB进行页面访问的时候会产生两种失效:
软失效:当一个页面访问不在TLB,但是在内存中时,此时不产生IO操作。
硬失效:当一个页面访问不在内存中和不在TBL中,产生IO操作,时间很长,较为严重。
通过TLB就可以有效的减低访问页面的时间!
2、大内存页表
上节缓解了访问页表的时间问题,这节来处理大内存的页表问题。主要可以使用多级页表和倒排页表
1、多级页表
将虚拟地址转化为三部分,例如10位的PT1、10位的PT2、12位的偏移地址,那么页面大小就是4k,使用多级页表的原因是,当内存太大的时候根据大多数程序总是对少量的页面做多次的访问,而不是相反。我们可以将部分页表存入到内存中,这样就会衍生出多级页表的操作。
多级页表的计算过程位,输入逻辑地址addr确定逻辑上层PT1的页表号n_pt1=n/(PT2_size*PT1_size),以及在PT2的偏移号码n_pt2%(PT2_size*PT1_size),地址偏移可以得到为add_d=addr%page_size。具体的寻址的过程与一级页表一样。
2、倒排页表
在传统的页表中所有的进程都有一张自己维护的页表,当页表设置太大的时候虽说会降低缺页中断的风险,但是浪费了存储空间,为了处理这种浪费,使用内存中的页框作为查找的依据来对应一个表项,这样所有的进程共享一张页表,也便不存在虚拟内存与页表之间的对应关系,当索引一个页的时候需要结合逻辑地址addr和进程ID id(addr id)来查找对应的页框号,节省了空间。我是这样觉得的,书上说明是比按照虚拟地址直接分页要强,怎么可能按照虚拟地址直接分页呢!主要的问题在于使用虚拟地址查询页框地址比较困难,使用散列和TLB可以对其进行一定的加速。