连续分配方式的缺点:
固定分区分配:缺乏灵活性,产生大量的内部碎片,内存的利用率较低
动态分区分配:会产生许多外部碎片,虽然可以用紧凑技术处理,但是紧凑技术的时间代价较高
基本分页存储管理
思想:把内存分为一个个相等的小分区,再按照分区大小将进程拆分成一个个小部分。
分区:页框/页帧/内存块/物理块
每一个页框有一个编号,叫做页框号/页帧号/内存块号/物理块号,从0开始
我们将用户进程的地址空间也分为与页框大小相等的一个个区域,成为“页”/“页面”,每个页面也有一个编号,叫做页号。也是从0开始。
操作系统以页框为单位为各个进程分配内存空间,进程的每个页面分别放入一个页框中。各个页面不必连续存放,也不必按先后顺序来,可以放到不相邻的各个页框中。
适用分页存储管理如何实现从逻辑地址到物理地址的转换呢?
- 算出逻辑地址对应的页号 页号=逻辑地址/页面长度(整除)
- 知道页号对应页面在内存中的起始地址 操作系统用某种数据结构记录各个页面的起始地址
- 算出逻辑地址在页面中的偏移量 页内偏移量=逻辑地址%页面长度
- 物理地址=页面地址+页面偏移量
在计算机中为了方便计算页号、页内偏移量,页面的大小一般为2的整数幂。
以32位逻辑地址为例,我们假设页面的大小位4KB(4096B=212B),那么页号就是前20位,页内偏移量就是后12位。即如果页面的大小为2kB,地址总共为n位,那么后k位是页内偏移量,前面的n-k位就是页号,这样就解决了1、3问题。
页内偏移量最大等于页面大小等于页框大小。一个进程最多有2n-k个页(因为内存就这么大)
那如何知道页号对应页面在内存中的起始地址呢?
为了能够知道进程的每个页面在内存中存放的位置,操作系统要为每个进程创建一张页表。
- 一个进程对应一张页表
- 进程的每一页对应一个页表项
- 每个页表项由页号(一般不用储存)和块号组成,这一页表项对应的起始地址=块号*内存块的大小+页表首地址
- 页表记录进程页面和实际存放内存块之间的对应关系
内存/页面大小=内存块号的最大值
每个页表项占字节数N=┌(log2内存块号最大值)/8┐N = \ulcorner (log_2内存块号最大值)/8 \urcornerN=┌(log2内存块号最大值)/8┐(因为每个字节是8B)
页表的首地址为XXX
页号YYY对应的页表项的地址为X+N∗YX+N*YX+N∗Y
基本地址变换机构
页表寄存器(PTR:存放页表的起始地址F和页表长度M)
进程未执行时,页表的起始地址和页表长度放在进程控制块(PCB)中,当进程被调度时,操作系统内核会将他们放入页表寄存器中。
页面大小为LLL,逻辑地址AAA到物理地址EEE的变换:
- 计算页号P=A/LP=A/LP=A/L 页内偏移量W=AmodLW=A \bmod LW=AmodL
- 比较页号PPP和页表寄存器PTRPTRPTR中页表长度M(存储了该进程有多少个页),如果P>=MP>=MP>=M,则发生越界中断,否则继续执行
- 页号P对应的页表项地址=页表起始地址F(存储在PTR中)+P*页表项长度(每个页表项占多大的空间),取出页表项内容b,即为该页表所对应的页框号/页帧号/内存块好。
- E=b∗L+WE=b*L+WE=b∗L+W(如果是二进制则直接拼接)
页内偏移量 = 页面大小
在分页储存管理(页式管理)的系统中,只要确定了每个页面的大小,逻辑地址结构就确定了。因此,页式管理中地址是一维的。即, 只要给出一个逻辑地址,系统就可以自动算出页号、页内偏移量两个部分,并不需要显式告诉系统这个逻辑地址中,页内偏移量占多少位。
上面计算出页表项长度就可以表示内存块号的范围,但是为了方便页表的查询,常常让一个页表项占2kB,使得每个页面恰好可以装得下整数个页表项也不至于当页表长度超过一个页框可以装下的范围的时候出现计算困难的问题。
例如:假设一个页面4KB,32位的操作系统。则共有220个内存块,即我们至少需要3个字节来保存内存块的地址,每个页表项长度为3B。页表也是储存在页框中的。那么一个页框可以存储4096/3=1365个页表项,最后会剩下一个字节。但是一个进程最多可以有220个页表项,因此我们有可能需要跨页框进行储存。可是上一个页框剩下的那一个字节我们不能够再适用了,那么第1365个页表的初始地址就不能简单的计算为页表起始地址+3页号,在这里应该是页表起始地址+3页号+1。为了解决这个问题我们不妨让每个页表项占4KB,这样就可以整除,虽然浪费了一些空间,但是计算上更加方便。
实际查询两次内存:第一次查询页表(相当于查找指针),第二次访问内存(指针对应的值)
具有块表的地址变换机构
局部性原理
时间局部性:如果在程序中执行了某条指令(访问了某个数据),那么不久后这条指令(这个数据)很可能再次被访问。(因为程序中存在大量的循环)
空间局部性:一旦程序访问了某个储存单元,不久之后,其附近的存储单元也有可能被访问。(因为很多数据在内存中都是连续存放的)
由于局部性原理,可能连续很多次查到的都是同一个页表项。
快表
快表,又称作联想寄存器(TLB),是一种访问速度比内存快很多的高速缓冲存储器,用来存放当前访问的若干页表项,以加速地址变换的过程。与此对应,内存中的页表常称为慢表。
- CPU给出逻辑地址,由某个硬件算得页号、页内偏移量,将页号与快表中的所有页号进行比较。
- 如果找到匹配的页号,说明要访问的页表项在快表中有副本,则直接从中取出该页对应的内存块号,再将内存块号与页内偏移量拼接形成物理地址,最后访问该物理地址对应的内存单元。因此,若快表命中,则访问某个逻辑地址仅需要一次访存即可。
- 如果在快表中没有找到匹配的页号,则需要访问内存中的页表,找到对应页表项,得到页面存放的内存块号,,再将内存块号与页内偏移量拼接形成物理地址,最后访问该物理地址对应的内存单元。(需要两次访存)在找到页表项以后,应同时将其存入快表,以便后面可能的再次访问。但若快表已满,则必须按照一定的算法对旧的页表项进行替换。
如果快表命中就可以节省很多时间。
快表和慢表同时查找:对于查询快表和慢表的操作是同时进行的,如果在快表中查询到,则会停止在慢表中的查询。
两级页表
单级页表存在的问题
问题1:如果计算机按字节寻址,支持32位逻辑地址,采用分页存储管理,页面大小为4KB(212),页表项长度为4B。那么一个进程最多有1M(220)个页面,那么页表的大小为222B,每一个页框大小同样为4KB,那么总共需要1K的页框存储这个页表。这就要求所有的页表都是连续的。这是比较困难的。
问题2:没有必要让整个页表常驻内存,因为进程在一段时间内可能只需要访问几个特定的页面
为了解决上面的问题,我们为页表建立一个页表,成为页目录表(外层页表、顶层页表),让每一个内存块存放一些页表项。
在上面的例子中,一个内存块(页面)可以存放1K个页表项。最多需要1K个这样的内存块,每个内存块的地址为4B,刚好可以放在一个页面中,而这个页面就是页目录表。
将逻辑地址转换为物理地址:
- 按照地址结构将逻辑地址拆分为三部分
- 从PCB中读出页目录表地址,再根据一级页号查页目录表,找到下一级页表在内存中的存放位置
- 再根据二级页号以及该页号所在的内存块得到逻辑地址对应的内存块
- 结合页内偏移量得到物理地址:内存块*内存块大小+页内偏移量
注意事项
- 若采用多级页表机制,则各级页表的大小不能超过一个页面
- 两级页表的访存次数分析(假设没有快表机构)
第一级访存:访问内存中的页目录表
第二次访存:访问内存中的二级页表
第三次访存:访问目标中内存单元
n级页表的访存次数是n+1次