首先操作系统的内存管理无非分为两个部分: 物理内存管理(连续分配管理和非连续分配管理:分页、分段、段页式)和 虚拟内存管理,下面的叙述也是围绕着这两个部分展开的。
内存使用的演变
在早期的系统中,没有引入内存抽象时,程序访问的都是物理内存,其管理也较为简单,除去系统使用的内存,其他内存都可操作。但是这样就会出现两个明显的问题:
- 当用户对内存可任意访问时,容易破化操作系统,造成系统崩溃;
- 多个程序并发运行时会变得困难和产生一些难以预料的错误产生。
为了避免这样的问题,操作系统对内存管理进行了升级,将物理空间进行了一层抽象——地址空间(Address Space),这个概念就像是进程的概念创造了一类抽象的CPU以运行程序,地址空间为程序创造一种抽象的内存,地址空间是一个进程可用于寻址内存的一套地址集合,每个进程都有一个自己的地址空间,并且这个地址空间独立于其他进程的地址空间。
不过地址空间只是满足了多进程的要求,很多情况下现有的内存无法满足仅一个大进程的内存要求,当物理内存不够的情况下我们应如何解决的:
- 覆盖:早期的操作系统用覆盖来解决物理内存不够用的情况,就是将一个程序分为多个块,然后分块加载到内存中,执行完后由后面的块覆盖当前的块执行。与下面的分区式存储管理配合使用。不过这个方法在分块部分很耗时,因此对此进行了优化也就是虚拟内存;
- 交换:将暂时不能执行的程序先加载到外存中,然后内存可用来加载新的程序,或者读取保存在外存中且处于就绪态的程序。该方法增加并发运行的程序项目,与覆盖技术的一个显著优势是交换不要求程序员给出程序段的覆盖结构;
- 虚拟内存:他通常使得应用程序认为拥有连续的可用内存(连续完整的地址空间),而实际上内存被分为多个大小的相同的内存碎片被称为页(Page),每个页都是连续的一段地址,其中一部分对应物理内存上的一部分被称为页框,当内存耗尽时电脑就会自动调用硬盘来充当内存,在需要时进行数据交换。
而操作系统是怎样将虚拟地址和物理地址联系起来的呢:这需要通过CPU芯片中的内存管理单元(MMU)映射,其可以将虚拟地址转变为物理地址然后再通过物理地址让问内存:
物理内存的连续分配管理
连续分配管理是指为一个用户分配连续的内存空间,其中包括单一连续存储管理和分区式储存管理两种方式:
单一连续存储系统
在该种管理方式下,内存被分为两个区域,用户区和系统区,当应用程序加载到用户区可使用用户区的全部空间,该种方式适合单用户、单任务的操作系统,虽其管理方式简单,但也存在一些问题,例如针对使用内存少的应用程序,将其全部加载到内存中,会使得很小部分的程序占用大数量的内存,使得内存浪费。
分区式存储管理
当然我们实际的系统中,单用户、单任务的情况较少,而为了支持多程序并发运行和分时系统,我们引入了分区式存储管理,该方法时将内存分为大小相等或不等的块(分区),操作系统和应用程序分别占据不同的块,虽然该方法在单一连续存储有所改进:可支持程序并发但却使块与块之间的数据交换变得困难。且引入了两个新的问题:内碎片和外碎片。
- 内碎片:是占用分区内未被利用的空间;
- 外碎片:占用分区间难以利用的空闲空间(通常为小空间)。
实现分区式存储管理,操作系统应维护一个分区表或分区链表,该表中通常包含了分区的起始地址,大小及分配状态。为了实现分区式存储管理我们需要引入一个概念内存紧缩(compaction):该方法是处理内存碎片化得方法,当分区释放后,或内存分配找不到满足条件的空闲分区时,系统会将各个占用分区向内存的一端移动,然后将各个空闲分区合并为一个空闲分区,该方法也存在一些问题:CPU对数据搬移时所耗费的时间、分区释放后对下一部分内容进行重定位等问题。下图分别为紧缩前和紧缩后的内存状态:
物理内存的非连续分配管理
前面介绍的集中存储管理方法,为进程分配的空间是连续的,使用的地址也是物理地址,如果我们可将一个进程分散到不连续的空间做存储,这样就可减少内存碎片,从而尽量避免内存紧缩。因此我们引入了进程的逻辑地址,他可将进程地址空间与实际存储空间分离,使存储更加灵活。
- 地址空间:上面所提到的地址空间的概念类似于进程,抽象出一个CPU以程序运行。程序经编译和连接后转变为相对地址编址形式,以0为基址,相对地址也叫逻辑地址或虚地址,地址空间是逻辑地址的集合;
- 存储空间:计算机系统实际的内存地址时绝对地址,绝对地址又叫物理地址或实地址,存储空间时物理地址的集合。
段式存储管理
在段式存储管理中,将程序的地址空间划分为若干个段(segment)(代码段、数据段、堆栈段)。每个段分配一个连续的分区,进程中的各个段可不连续的存放在内存的不同分区中,而借助硬件实现逻辑地址到物理地址的映射。
段式存储管理的优点是:可以分别编写和编译源程序的一个文件,并且针对不同类型的段采取不同的保护,也可按段为单位进行共享,但是容易产生碎片。
段式管理的实现:在被系统所分配的段中,每个段都有自己的名字,为了实现简单起见,通常可用一个段号来代替段名,该段号作为段表的索引,而段表里又保存着该段的段基地址(段的开始位置)和段的界限(段长)等信息:
逻辑地址(虚拟地址)中的段偏移量的大小是处在0~段界限之间,分段会把程序的虚拟地址分为四个段(代码段、数据段、堆段、栈段)而每个段在段表中有一个表项,在该项中可找到段基址,加上偏移量后就可找到物理内存中的地址,如下所示:
页式存储管理
将程序的逻辑地址(虚拟地址)空间划分为固定大小的页(Page)而物理内存中划为同样大小的页框(page frame),当程序加载时,将任意页中的数据放入任意页框中,这样就实现了离散存储管理;在硬件的支持下实现了逻辑地址到物理地址的映射。借鉴段式存储我们不难想出页式存储中的内部由两部分构成——页号、页内地址(位移量):
相较于上面的段式存储管理其虽然没有外碎片,但其内碎片也不会超过一个页的大小(Linux下默认一个页为4KB),该方式也便于改变程序占用空间的大小:随着程序的运行,地址空间的增长。如果内存不够时,系统会将运行进程中最近没被使用的内存页面释放(写入到硬盘上)——换出(Swap Out),需要的时候再加载进来——换入(Swap In),一次性写入磁盘或内存只有几个页,这样使得内存交换的效率相对较高。
页式存储的实现:在分页机制下,逻辑地址就被分为两部分,一个是上图所示的页号可通过其找到页表中对应的页内地址,还有一个就是页内偏移量,那么我们可通过如下步骤找到逻辑地址到物理地址的映射:
- 逻辑地址分为页号和偏移量;
- 根据页号,在对应的页表中找到对应的物理页号;
- 在物理页号的基础上加上偏移量得到物理内存地址:
页式存储和段式存储的区别
这两种方式作为系统非连续分配管理方法,自然是有很多相似之处,读通过地址映射来实现逻辑地址到物理地址的变换,两者也有一些区别:
- 需求:段是信息的逻辑单位,它包含相对完整的意义,分段可更好地满足用户的需求,而页是信息的物理单位,分页为了实现离散分配方式,减少内存碎片而提高内存利用率,其可满足系统管理的需求,一条指令或操作数可跨越两个页分界处但不会跨越两个段分界处;
- 大小:页的大小固定且有系统觉得,将逻辑地址划分为页号和页内地址两部分,由硬件实现,而段的大小不固定,决定于用户所编写程序,通常会根据编译系统对源程序进行编译时根据信息的性质划分;切段比页大,因此段表比页表短,缩短了查找时间,提高了访问速度;
- 逻辑地址表示:页式系统地址空间是一维的,单一的线性地址空间,用户可通过标识符表示一个地址,分段的地址空间是二维的,程序员在标识地址时需要给出段名和段内地址;
段页式存储管理
上述分段存储和分页存储并不是独立存在的,他们可组合使用称为段页式存储管理,其实现方式如下:
- 将程序划分为多个有逻辑意义的段——分段机制;
- 在将每个段划分出来的连续空间再划分为固定大小的页。
这样物理地址可由段号、段内页号、页内位移所组成,其映射流程如下:
映射过程需要三次内存访问:
- 第一次访问段表得到页表起始地址;
- 第二次访问页表得到物理页号;
- 第三次将得到的物理页号与页内位移组合的到物理地址。
通过分段和分页的结合提高了系统资源的利用率。