文章目录
第六章、内存管理——给程序执行提供一个舞台
一、程序的重定位
在编译可执行程序时,用到的地址都是从0开始的相对地址。
1.程序时如何重定位的
这里只介绍最为合适的运行时重定位,运行时重定位就是程序在运行的时候,从PCB中取出程序的基址,与程序中代码语句等的逻辑地址结合,构成了一个运行时的物理地址,逻辑地址+物理地址的地址运算交由MMU寄存器执行,可以提高指令执行的效率。
2.MMU的运行方式
将要执行指令的基址放在一个约定的寄存器中,每条指令运行时,MMU就会自动地将指令中取出的逻辑地址和这个寄存器中的基址相加,形成物理地址后发送到物理总线上。
二、分段
1.什么是分段
用户常常要求程序能以分段的方式出现。就是将代码段、数据段、栈段、函数库这些分开实现,即数据存放在数据段中,操作部分对应代码段,函数嵌套在栈段中实现。这些段都从0地址开始单独编址。
我们看到,这些段都从0地址开始单独编址,但是0地址处最多只能有一个程序,所以一定是需要重定位的,段的重定位我们引出段表的概念。<mark>段表是在程序各个段申请内存空间时形成的</mark>。
2.段表构成
上图就是段表的示例,段号就是代表的哪个哪个段,0一般是代码段,1一般是数据段,基址就是对应的段在内存中的基地址,长度就是这一个段用了多少空间。
3.段表设置
我们知道,程序要想执行,一定要把程序放到内存中才可以取址——执行起来,那么该如何将这一个个段放到内存中去呢?
针对一个段请求,我们要在内存中分割出一段和请求尺寸大小相等的内存区域进行分配。这种依据尺寸大小的分配方式叫做可变分区分配,有对应的可变分区算法为他服务。
但是这就出现了问题,我们应该如何在内存中找到一块最合适的内存区域分配给段呢?要知道,现在的部分内存区域很可能已经被许多随机分布的段的分区占用,我们该如何在这些段与段之间的空隙中找到一个最佳的存储位置呢?这就引出了可变分区的适配算法
4.可变分区的适配算法
1.最先适配
在空闲分区中找到第一个匹配成功的分区即可
2.最佳适配
在空闲分区中找到一块满足要求且尺寸最小的空闲区域。
3.最差适配
在空闲分区中找到一块最大的分区
无论是哪一个适配方式,都或多或少的存在许多的内存碎片,也可以说是外部碎片(后边的分页还有内部碎片),这就降低了内存的利用率,造成空间的浪费。
如果要提高内存的利用率,可以将内存请求打散成固定大小的小片,并将物理内存打散成同样大小的小片,再将内存请求一片片的随机放入物理内存中的小片中,并将这些小片的页号和对应的页框号记录下来,就构成了页表,这也就是分页的机制。
5.分段机制下的地址转换
要想完成地址转换,最重要的就是MMU寄存器的启动,而这个寄存器是在保护模式下启动的,所以启动保护模式的实质就是启动MMU来完成运行时重定位。
我们假设这个时候已经构建好了段表,这里的段表指的是用户程序的段表,也就是LDT表,LDT表存储在PCB中,当然GDT表中也有指向各个进程的LDT表的表项。分段机制下的地址转换过程如下:
每个进程都有它自己的LDT表,这个之前的文章说过了
三、分页
1.为什么出现分页机制
因为适配算***带来明显的内存空间的浪费,如果要提高内存的利用率,可以将内存请求打散成固定大小的小片,并将物理内存打散成同样大小的小片,再将内存请求一片片的随机放入物理内存中的小片中,并将这些小片的页号和对应的页框号记录下来,就构成了页表,这也就是分页的机制。
2.什么是分页
分页就是将物理内存分割成大小相等的页框,将请求放入物理内存的段也分割成同样大小的页,最后将所有的页都映射在页框上,完成物理内存页框的使用。
3.分页机制的缺陷
为了避免内存空间的浪费,页面尺寸应该设置的较小**(这样可以减少内部碎片),但是由于页面尺寸小了,那么页表就会变得很大,通常的操作系统都将页面设置为4k大小,因此一个32位逻辑地址空间就有220**个逻辑页,每个逻辑页对应一个页表项,就有**220=1M个页表项。而通常一个页表项大小为4B**,因此一个进程的页表总共要占据4MB大小的空间,假如有一百个进程在工作,那么占用的内存将达到100MB,对于操作系统而言,存放页表是操作系统的内存管理代价,这么大的代价是不被允许的。
4.缺陷的解决方式——多级页表
首先可能有的人想,那么大的页表,又不是每一个页表项都可以用得到,那么,我们只把需要用到的页表项存到内存里面不就好了,去掉那些没有存放在物理页框中的逻辑页,这样就能省下来不少空间了吗?这也是一个办法,但是这样做会导致页号的不连续,也就是根据逻辑地址查表时,不能直接根据偏移来查了,而是要使用“二分法”、“顺序法”等等查找方法来查找逻辑地址对应的页号了,这样相较于,直接根据偏移查找的情况要慢了很多。这种时间换空间导致的大幅度时间效率下降也是不能接受的。
为了解决这个缺陷,就有了多级页表这个东西,多级页表是怎么工作的呢?我们可以打一个比方:
当我们想要看一本书的某某章,某某节的时候,我们的脑子当中是不是就出现了章的号,以及节的号,这个章、节号就好比逻辑地址里面的页号,然后我们就开始查找书的目录了,我们直接查找想看的章,然后直接查找想看的节即可,不想看的章里面的节全部都可以省略掉,书的目录就好像多级页表。
接下来看一张多级页表的图片辅助理解:
首先这个多级页表是在32位的操作系统下的,注意:页目录表中包含从2^10 个表项,每个表项4B大小,一个页目录表占4KB大小的逻辑内存空间(虚拟内存空间),每个页目录表项中存放“节“目录表的基地址(也就是页表的基地址),一个页表中包含2^10 个表项,每个表项4B大小,一个页表占据4KB大小的逻辑空间,图中一共用到了三个页表,也就是2^10*3个物理页框(3k个),加上一个页目录表的1k个表项,总共4k个表项,每个表项占4B的逻辑内存空间,一共占据16KB的逻辑内存空间,相比之前的4MB大大减少了。
再整理一下,多级页表,用来离散化存储页表,层级越多,占用的内存空间越少,以上图的多级页表为例,页目录表是完整连续的,每一个页表也是完整连续的,不同的是,这些分开的页表中存储的是原来的一级页表中所有的映射到物理页框中的页表项,将无用的页表项全部删除掉了,这也就是节省空间的原理。
5.多级页表的缺陷
多级页表也有缺陷,原来查表只用查一个页表,现在查表需要查两层页表,显然时间效率变慢了。
6.缺陷的解决方式——快表
我们可以将经常用到的逻辑页号和物理内存页框映射关系记到”脑子“里面,这样再次用到,地址转换就会快很多,计算机的”脑子“就是CPU的寄存器之一——TLB——变换旁查缓冲器,经常用到的映射关系记录在CPU的高速缓存中,而这个高速缓存就属于TLB寄存器。也就是我们常说的”快表“。
此时的地址转换过程就是:先查块表,如果命中,很快就能获得物理页框号,如果没有命中,就查找页目录表、查找页表,找到物理页框并更新快表。
由于块表中会存现在常用到的逻辑页,而程序局部性规律又说明最近使用的逻辑页通常都是现在常用的,再由于快表的查询速度非常快,所以引入块表后的多级页表结构会让地址转换速度变得很快。
四、段页式内存管理与虚拟内存
1.如何将段页结合在一起
这里我们引入虚拟内存空间的概念,顾名思义,这个空间是不存在的,只是一个看起来和物理内存一样,但是没有对应物理存储单元的虚拟内存。我们把这段空间作为中间体
首先将程序分为多个段,并从中间结构上分割出一些区和每个段建立映射,完成分段机制,再将中间结构分成页,将这些页放到物理内存的页框中,并建立这个页和页框的映射,完成分页机制。
2.段页机制的使用步骤
(1)在物理内存中割出一些分区,将程序的各个段”放入“,并不是真的放入,而是建立映射关系,假装放入。
(2)建立段表记录这个映射关系
(3)将虚拟内存分割成页,选择物理内存中的空闲页框,将虚拟内存中的”页内容“放到物理页框中,这里的页内容是曾经假装放入到虚拟内存中的程序段内容。
(4)建立页表来记录这种映射关系。
3.虚拟内存的意义
一旦经过了虚拟内存,从用户出发看到的视图是程序段被放到一个连续的”内存“区域上,即用户看到的是分段效果,而背后的操作系统将这个”内存“按照分页方式真正放到物理内存中,实现了分页机制。
4.放入到内存中的指令是如何执行的
以”call 40“为例,执行该指令时首先进行地址转换,先查找段表取出基址,可以根据偏移40算出虚拟内存中的位置,现在要根据虚拟地址查页表查出物理地址
例如:段表查表取出的虚拟地址为1040,那么通过这个虚拟地址查页表,10为虚拟页号,查出对应的物理页框号为5,所以最终的物理地址就是540,在地址总线中放入540后进行取址,取出的指令正好是call 40对应的指令。
<mark>只要将特定寄存器LDTR(段表初始地址寄存器)和CR3(页表初始地址寄存器)设置为正确的初始地址,执行每条指令时,MMU都会自动地、正确地完成上述地址转换的过程。</mark>