内存的基础知识
内存可以存放数据。程序在执行之前需要将其先放到内存中才能被CPU处理 —— 缓和CPU与硬盘之间的速度矛盾
内存中有一个个的“存储单元”,并且·每个存储单元都有一个地址(一个地址对应一个存储单元)
如果计算机采用字节编址,则每个存储单元大小为1字节,即8个二进制位
如果字长为16位的计算机采用按字编址,则每个存储单元大小为1个字,每个字的大小为16个二进制位
指令的工作原理
使用高级语言编写的代码经过编译之后会形成等价的机器语言指令
程序需要运行时,系统会为其建立相应的进程,进程在内存中会有一片区域 —— 程序段,用来存放机器语言指令;还会有一片区域 —— 数据段,用来存放程序需要处理的数据
CPU在执行程序段中的指令,如指令1时,会根据指令的操作码部分(指令1的红色部分)判断该指令是要进行什么动作(指令1的操作码指明了让CPU进行数据传送),再结合指令中其他部分(指令1中的白色部分)来完成该指令的动作(如执行指令1就是把内存地址为01001111号内存单元中的数据复制一份,并放入到寄存器编号为00000011的寄存器中)
执行完指令1后,CPU就会执行下一条指令(指令2),同样是根据指令操作码判断指令要进行什么操作,再根据指令的其他部分来完成该操作(执行指令2就是让数字“1”与寄存器编号为00000011的寄存器1中的内容进行加法运算,运算结果再存入00000011号寄存器中)
执行完指令2后执行指令3,...
CPU在执行一条条指令的过程,其实就是根据指令的地址信息来处理存放在某个寄存器或内存中某个区域中的一个个数据的过程
在此例中,默认进程在内存中的起始地址为#0,因此指令中的逻辑地址信息就是实际数据所在的物理地址
C语言程序经过编译、链接处理后,会生成装入模块(即可执行文件),将装入模块装入到内存中后,就可以执行这个程序了
初始时,可执行文件中的指令所指明的地址可能逻辑地址(相对地址),即相对于进程的起始存放地址而言的地址
可以理解为逻辑地址就是一个偏移量,而偏移量所相对的地址就是进程的起始地址
若装入模块被装入到内存中并形成对应的进程后,如果给其分配的实际内存地址空间是连续的且内存起始地址为#0,那么指令中的逻辑地址和程序实际运行时所需的物理地址信息就是等价的,不进行地址转换直接执行也不会出现问题
若给进程分配的内存起始地址为#100(同样是连续的内存空间),如果不对指令中的逻辑地址进行转换,则在执行指令时会将数据写入(也可以是其他类型的操作)到错误的存储单元中
将指令中的逻辑地址转换为物理地址可以在装入模块“装入”内存时进行,装入的方式有3种:
- 绝对装入
- 可重定位装入(静态重定位)
- 动态运行时装入(动态重定位)
绝对装入
在编译时,如果知道程序将存放到内存中的哪个位置,编译程序(编译器)将产生只包含绝对地址的目标代码(即编译后形成的可执行文件中包含的不再是逻辑地址,而是实际的物理地址)。程序运行时直接根据装入模块中的绝对地址,将数据装入内存
即装入模块中的指令使用到的是就物理地址
即在编译时就将逻辑地址转换为物理地址(由编译器负责转换)
也可由程序员直接编写指令中的地址为绝对地址,之后编译器无需对指令中的地址进行处理就可以直接生成包含有效物理地址指令的文件,这种方式也属于绝对装入方式
如果编译程序提前知道装入模块要从地址为100的地方开始存放,则:
绝对装入方式只适用于单道程序环境(此时操作系统还没有产生);模块一旦装入内存后就不能移动
采用绝对装入方式后,若想让在本机上生成的要在100号地址处开始存放的可执行文件运行在另一台电脑上,那么如果另一台电脑的100号内存或其后的连续空间已被占用,那么该可执行文件就不能运行在这一台电脑上,因此绝对装入方式的可移植性差(灵活性低)
可重定位装入(静态重定位)
编译、链接后形成的装入模块的地址仍然是从0开始的(仍然是逻辑地址),即指令中使用的地址、数据存放的地址都是相对于起始地址而言的逻辑地址。装入时可根据内存的当前情况,将装入模块装入到内存的适当位置。装入时对地址进行“重定位”,将逻辑地址变换为物理地址(地址变换是在装入时一次完成的)
即在装入模块装入内存时将逻辑地址转换为物理地址(由装入程序负责完成转换)
可重定位装入常用于早期的多道批处理系统
静态重定位的特点是在一个作业装入内存时,就必须分配其要求的全部内存空间,如果没有足够的内存,就不能装入该作业。作业一旦进入内存后,就不能再移动,也不能再申请内存空间
因此可重定位装入不能和虚拟内存机制搭配使用
动态运行时装入(动态重定位)
编译、链接后的装入模块的地址都是从0开始的。装入程序把装入模块装入内存后,也并不会立即把逻辑地址转换为物理地址,而是把地址转换工作推迟到程序真正要执行时才进行。因此装入内存后指令中的所有地址依然是逻辑地址。这种方法需要一个重定位寄存器的支持
重定位寄存器中存放着装入模块在内存中的起始物理地址,当CPU执行指令时,会将指令中的逻辑地址与重定位寄存器中的内容相加,结果即为物理地址
可将装入模块分页,因此在基本分页存储管理的系统中,重定位寄存器中保存的就是页面所在内存块的起始地址
现代操作系统常用
采用动态重定位方式后允许程序在内存中发生移动(移动后只需要修改重定位寄存器的值即可)
优点:
- 可将程序分配到不连续的存储区中(之后重定位寄存器中就需要存入当前程序段所在内存块的起始地址)
- 在程序运行前只需装入它的部分代码即可投入运行,然后在程序运行期间,根据需要动态申请分配内存(虚拟内存)
- 便于程序段的共享,可以向用户提供一个比存储空间大得多的地址空间(虚拟内存)
从写程序到程序运行
程序员通过编辑器编写的程序,经过编译后会形成目标模块文件,一个源代码文件对应一个目标模块,目标模块中包含了相应的机器语言指令,指令中的地址信息都是逻辑地址,每个目标模块彼此之间相互独立,因此每个目标模块文件的逻辑地址都是从0开始的
链接的作用就是将目标模块文件以及模块中用到的库函数文件组装为一个完整的装入模块,即可执行文件。装入模块中拥有完整的逻辑地址
有了装入模块后,就可以将它装入内存然后开始运行
编译:由编译程序将用户的若干个源代码文件编译成相应的一个个目标模块,每个目标模块的逻辑地址都是从0开始的(即把高级语言翻译成机器语言)
链接:由链接程序将编译后形成的一组目标模块以及所用到的库函数链接在一起,形成一个完整的装入模块(装入模块中包含了完整的逻辑地址,该逻辑地址是之后进行地址转换时要用到的逻辑地址)
装入(装载):由装入程序将装入模块装入内存运行
链接的三种方式
静态链接:在程序运行之前,先将各目标模块及它们所需的库函数链接成一个完整的可执行文件(装入模块),之后不再拆开
在形成装入模块后,就确定了装入模块中完整的逻辑地址,之后不可修改
装入时动态链接:将各目标模块(即所有目标模块)装入内存时,边装入边链接的链接方式
只有当目标模块被放入内存时才会进行链接
进程的完整逻辑地址是一边装入一边形成的
运行时动态链接:在程序执行的过程中需要该目标模块时,才对它进行链接。其优点是便于修改和更新,便于实现对目标模块的共享;灵活性更高,提高了内存空间的利用率
只有在使用到某个目标模块时,才将它装入内存,装入的同时进行链接,用不到或还没用到的模块既不会被装入,也更不会被链接
总结
内存管理的概念
操作系统作为系统资源的管理者,需要对内存进行管理:
- 操作系统负责内存空间的分配与回收
- 操作系统需要提供某种技术从逻辑上对内存空间进行扩充
- 操作系统需要提供地址转换功能负责程序的逻辑地址与物理地址的转换
为了使编程更方便,程序员写程序时应该只需要关注指令、数据的逻辑地址。而逻辑地址到物理地址的转换(这个过程称为地址重定位)应该由操作系统负责,这样就保证了程序员写程序时不 需要关注物理内存的实际情况
操作系统一般会使用可重定位装入或动态运行时装入方式将逻辑地址转换为物理地址,而绝对装入方式适用于单道程序阶段,那时还没有出现操作系统
- 操作系统需要提供内存保护功能,保证各进程在各自存储空间内运行,互不干扰
内存保护
内存一般会被划分为操作系统使用的内存区域和用户程序(用户进程)使用的内存区域,各个用户进程都会分配到属于自己的一块内存空间,各个进程只能访问属于自己的内存空间,而不能访问其它进程或操作系统的内存空间
实现内存保护可以采取两种方法:
- 在CPU中设置一对上、下限寄存器,存放进程的上、下限地址。进程中的指令要访问某个地址时,CPU需要检查访问的地址是否越界
- 采用重定位寄存器(又称基址寄存器)和界地址寄存器(又称限长寄存器)进行越界检查。重定位寄存器中存放的是进程的起始物理地址。界地址寄存器中存放的是进程的最大逻辑地址(最大偏移量)
假设进程1想要访问逻辑地址为80的单元,首先需要用逻辑地址80与界地址寄存器的内容进行对比,如果发现没有超过界地址寄存器的内容,则认为该逻辑地址合法(否则抛出越界异常),于是将逻辑地址80与基址寄存器中的起始地址相加得到实际要访问的物理地址
总结
覆盖与交换
覆盖技术和交换技术都可以实现对内存空间的“扩充”
此外还有虚拟存储技术
覆盖技术
覆盖技术用于解决“程序大小超过实际物理内存总和”的问题
思想:将程序分为多个段(多个模块),常用的段常驻内存,不常用的段在需要时调入内存
内存会被划分为一个“固定区”和若干个“覆盖区”,需要常驻内存的段放在“固定区”中,调入后就不再调出(直到运行结束为止);不常用的段放在“覆盖区”,需要用到时调入到其中,用不到时调出从中调出
假设现存在一个程序X,它的调用结构如下所示
A模块可能会调用到B模块或C模块,但调用B模块的操作与调用C模块的操作是分开进行的(一个时间段内只会出现正在调用B模块或正在调用C模块的情况),B模块中又可能会调用到D模块,而C模块中又可能会调用到E或F模块
对于程序X的调用结构,采用覆盖技术后,就可以这样设置:
- 把A模块放到内存的一个固定区中,该固定区的大小为A模块的大小,即8K
- 由于B模块和C模块不可能同时被访问,因此可以让这两个模块共享一个覆盖区,该覆盖区的大小就为B和C模块大小的较大者,即10K
- D、E和F模块也不可能被同时访问,因此可以让这三个模块共享一个覆盖区,该覆盖区的大小就为这三个模块大小的较大者,即12K
覆盖技术的实现是以操作系统知道程序的调用结构(或称覆盖结构)为前提的,而在程序运行前操作系统不可能获取到程序的调用结构,因此调用结构必须由程序员来声明,之后操作系统自动完成覆盖
缺点:对用户不透明(即用户知道操作系统要对自己编写的程序分段存储),增加了用户编程的负担(编程人员编写代码时还必须向操作系统指明程序段之间的关系)
覆盖技术只用于早期的操作系统,现在已成为历史
交换技术
思想:内存空间紧张时,系统将内存中的某些进程(的程序和数据)暂时换出外存,把外存中某些已具备运行条件的进程(的程序和数据)换入内存(进程在内存与磁盘间动态调度)
系统中有许多进程正在并发运行,若某一时刻内存空间变得紧张时,就可以把内存中某些进程(的程序和数据)换出到外存中,但这些被换出的进程的PCB仍然在内存当中,但是会被插入到挂起队列里。当内存空间充足时,又会把处在外存中的进程(的程序和数据)将它们换入内存
注意:不管进程(的程序和数据)是否被换出到外存,进程的PCB都需要常驻内存,当进程(的程序和数据)被换出外存后,进程的PCB就会记录该进程在外存中的存放位置,之后将进程(的程序和数据)换入内存时就需要使用到这个信息
中级调度(内存调度),就是要决定将哪个处于挂起状态的进程重新换入内存
Eg1:被换出的进程应该存放在外存的什么位置?
在具有交换功能(对换功能)的操作系统中,通常把磁盘空间划分为文件区和交换区。文件区主要用于存放文件,主要追求存储空间的利用率,因此对文件区空间的管理采用离散分配方式;交换区空间只占磁盘空间的小部分,被换出的进程数据就存放在交换区。由于进程交换的速度直接影响到系统的整体速度,因此交换空间的管理主要追求换入换出速度,因此通常交换区采用连续分配技术。总之,交换区的I/O速度比文件区的速度更快
Eg2:什么时候应该交换?
交换通常在许多进程运行且内存吃紧时进行,而系统负荷降低时就暂停交换。例如:在发现许多进程运行时经常发生缺页,就说明内存紧张,此时可以换出一些进程;如果缺页率明显下降,就可以暂停换出
Eg3:应该换出哪些进程?
可优先换出阻塞进程;可换出优先级低的进程;为了防止优先级低的进程在被调入内存后很快又被换出,有的系统还会考虑进程在内存的驻留时间
总结
连续分配管理方式
连续分配:指为用户进程分配的必须是一个连续的内存空间
与连续分配方式相对应的就是非连续分配管理方式,即给用户进程分配的可以是连续的内存空间,也可以是离散的内存空间,例如:分页存储、分段存储、段页式存储
连续分配管理方式可分为三种:
- 单一连续分配
- 固定分区分配
- 动态分区分配
单一连续分配
在单一连续分配方式中,内存被分为系统区和用户区。系统区通常位于低地址部分,用于存放操作系统相关数据;用户区用于存放用户进程相关数据
在采用单一连续分配方式的系统中,内存(的用户区)中最多只允许有一道用户程序,也就是(一道)用户程序会独占整个用户区空间
在这种方式下,即使用户进程所需的实际内存空间(绿色部分)很小,系统也会把整个用户区空间分配给该用户进程
优点:实现简单;无外部碎片;可以采用覆盖技术扩充内存;不一定需要采取内存保护
缺点:只能用于单用户、单任务的操作系统中(不支持多道程序并发运行);有内部碎片;内存空间利用率极低
在分配给某进程的内存区域中,如果某些部分没有用上,那么这部分内存区域就是“内部碎片“(上图的蓝色部分)
固定分区分配
为了能在内存中装入多道程序,且这些程序之间又不会相互干扰,于是将整个用户空间划分为若干个固定大小的分区,在每个分区中只装入一道作业(一道程序),这样就形成了最早的、最简单的一种可运行多道程序的内存管理方式
固定大小意味着划分完分区后,分区的大小将不再改变,而不是指所有分区的大小都一样
固定分区分配方式有可分为分区大小相等和分区大小不等两种
如果采用分区大小相等的策略,系统会把用户空间分割为若干个大小相等的区域
如果采用分区大小不等的策略,系统会把用户空间分割为若干个大小不等的区域
在分区大小不等策略下,系统通常是根据常在系统中运行的作业的大小情况进行分区划分,比如:划分出多个小分区、适量中等分区、少量大分区
分区大小相等特点:缺乏灵活性,但是很适合用于用一台计算机控制多个相同对象的场景
对于只需要很少内存空间的小进程,也只能为它提供一个固定大小的分区,造成了内存空间浪费(即会产生内部碎片);而对于需求空间很大的进程,可能一个分区的大小不足以满足进程的需求,因此需要使用覆盖技术从逻辑上扩充分区大小,这就增加了系统开销,因此分区大小相等的固定分区分配方式灵活性差
分区大小不等特点:增加了灵活性,可以为不同大小的进程提供空间大小更合适的分区
该方式相比于分区大小相等方式,虽然可以为小进程找到更合适的分区,但依然可能会出现内存空间浪费的情况,只是情况没那么严重
此方式在出现系统中最大的分区也不能满足进程需求时,也会需要使用到覆盖技术来“扩充”分区容量
在采用固定分区分配的操作系统中,系统需要建立一个数据结构 —— 分区说明表,来实现各个分区的分配与回收。每个表项对应一个分区,通常按分区大小排列。每个表项包括对应分区的大小、起始地址、状态(是否已分配)等信息
当某个用户程序要装入内存时,由操作系统内核程序根据用户程序大小检索该表,从中找到一个能满足大小且未被分配的分区,将之分配给该程序,然后修改状态为“已分配”
优点:实现简单,无外部碎片
缺点:当用户程序太大时,可能所有的分区都不能满足需求,此时不得不采用覆盖技术来解决,但这又会降低系统性能;会产生内部碎片,内存利用率低
动态分区分配
动态分区分配又称为可变分区分配,这种分配方式不会预先划分内存分区,而是在进程装入内存时,根据进程的大小动态地建立分区,并使分区的大小正好适合进程的需要。因此系统分区的大小和数量是可变的
系统需要使用一种数据结构来记录内存空间的使用情况,常用的数据结构有:
- 空闲分区表
- 空闲分区链
空闲分区表:每个空闲分区对应一个表项。表项中包含分区号、分区大小、分区起始地址等信息
空闲分区链:每个分区的起始部分和末尾部分分别设置前向指针和后向指针。起始部分处还可记录分区大小等其它信息
把一个新作业装入内存时,需按照一定的动态分区分配算法,从空闲分区表(或空闲分区链)中选出一个分区分配给该作业。分配算法会对系统性能有很大影响
如进程5可以装入到20MB的连续空闲空间的某个区域中,也可以装入到10MB的连续空闲空间的某个区域中,还可以装入到4MB的连续空闲空间中,具体分配在哪片空闲区域由分配算法来确定
当用户进程进入或退出内存时都需要修改空闲分区表或空闲分区链中的相关记录信息
假设系统采用空闲分区表来记录相关信息
若此时将进程5装入到拥有20MB空闲空间的区域的起始位置处,需要修改表的情况如下:
此分配方式不会改变用户空间中空闲分区的数量,因此只需要对表中的某一表项进行修改即可
若此时将进程5装入到拥有4MB空闲空间的区域处,需要修改表的情况如下:
此分配方式会改变用户空间中空闲分区的数量,因此需要删除表中的某一表项
当发生内存空间回收时也需要修改空闲分区表
假设系统中用户空间的分配情况以及相应的空闲分区表如下所示:
若某一时刻进程4运行结束,需要把进程4所占用的内存空间回收,而在回收前进程4所在区域的后面就存在一片的空闲区域,因此回收空间后,只需要修改表中某一表项的信息即可
假设系统中用户空间的分配情况以及相应的空闲分区表如下所示:
若某一时刻进程3运行结束,需要把进程3所占用的内存空间回收,而在回收前进程3所在区域的前面就存在一片的空闲区域,因此回收空间后,只需要修改表中某一表项的信息即可
假设系统中用户空间的分配情况以及相应的空闲分区表如下所示:
若某一时刻进程4运行结束,需要把进程4所占用的内存空间回收,而在回收前进程4所在区域的前面和后面都存在空闲区域,因此回收空间后,就需要合并表中某两个表项(注意合并处理对原有空闲分区大小进行相加,还需要增加进程回收后的分区空间大小)
假设系统中用户空间的分配情况以及相应的空闲分区表如下所示:
若某一时刻进程2运行结束,需要把进程2所占用的内存空间回收,而在回收前进程2所在区域的前面和后面都不存在空闲区域,因此回收空间后,就需要增加一个表项到表中
注意:虽然在上面4个例子中,各个表项的顺序是按照空闲分区的起始地址顺序依次排列,但在实际应用中,各表项的顺序不一定按照地址递增顺序排列,具体的排列方式需要依据动态分区分配算法来确定
动态分区分配没有内部碎片,但是有外部碎片
内部碎片:分配给进程的内存区域中(在进程运行的)从始至终都没有使用到的部分
外部碎片:内存中的某些空闲分区由于太小而难以利用
如果内存中空闲空间的总和本来可以满足某进程的需求,但由于进程需要的是一块连续的内存空间,因此这些“碎片”可能无法满足进程的需求,可以采用紧凑(拼凑,Compaction)技术来解决外部碎片问题
假设系统中依次进入了进程1、进程2、进程3,它们所占内存空间大小如图所示
此时系统中只剩下一片大小为4MB的内存区域,某一时刻进程2因为某些原因无法运行,可以先将它调出内存,于是内存中就又出现了一片大小为14MB的空间
某一时刻进程4进入内存,它被分配在了大小为14MB的空闲区域的起始位置处,所以此时内存中就剩下一块4MB和一块10MB的空闲空间
如果进程1也暂时不能运行,也可以将进程1暂时换出到外存,此时内存中又多出一块大小为20MB的空间
某一时刻进程2可以执行了,又进入了内存,它被分配在了大小为20MB的空闲区域的起始位置处,所以此时内存中就剩下一块6MB和一块10MB以及一块4MB的空闲空间
某一时刻进程1可以执行了,它需要进入内存,但此时系统中已经没有一块能够容纳20MB大小的连续空闲空间,因此进程1无法进入内存
在这种情况下,这三块空闲空间就是所谓的外部碎片,因为它已经不能满足进程分配的要求了
这三块空闲区域一共拥有了20MB大小空闲空间,采用紧凑技术就可以让这三块不连续的空间合并为一块连续的空闲空间,紧凑技术就是将进程在内存中进行“挪位”
注:
- 在三种装入方式中,动态重定位方式是最容易实现进程在内存中移动位置的
- 进程移动后,需要把进程在内存中的起始地址进行修改,进程的起始地址会记录在进程的PCB中,进程在上CPU运行之前,会从PCB中取出起始地址并送到重定位寄存器中
总结
动态分区分配算法
动态分区分配算法用于解决“在动态分区分配方式中,当很多个空闲分区都能满足需求时,应该选择哪个分区进行分配”的问题
动态分区分配算法包括:
- 首次适应算法(First Fit)
- 最佳适应算法(Best Fit)
- 最坏适应算法(Worst Fit)
- 邻近适应算法(Next Fit)
首次适应算法
思想:每次都从内存的低地址处开始查找,找到第一个能满足大小的空闲分区
实现方式:将空闲分区以起始地址递增的次序排列,每次分配内存时顺序查找空闲分区链(或空闲分区表),找到大小能满足要求的第一个空闲分区
以空闲分区链为例
假设进程5需要进入内存,进程5需要内存空间为15MB,采用首次适应算法时会从空闲分区链的链头处开始依次向后查找,当查找到第一个空闲分区(拥有20MB)时发现能够满足需求,因此进程会分配到这20MB的空间内,之后这块区域就会剩余5MB的空闲空间,所以需要修改此分区的分区大小信息为5(还需要修改分区起始地址等)
最佳适应算法
思想:由于动态分区分配是一种连续分配方式,为各进程分配的空间必须是连续的一整片区域,因此为了保证当“大进程”到来时能有连续的大片空间,可以尽可能多地留下大片的空闲区,即优先使用更小的空闲区
实现方式:将空闲分区按容量递增的次序连接,每次分配内存时顺序查找空闲分区链(或空闲分区表),找到大小能满足要求的第一个空闲分区
假设进程6需要进入内存,进程6需要内存空间为9MB,采用最佳适应算法时会从空闲分区链的链头处开始依次向后查找,当查找到第二个空闲分区(拥有10MB)时发现能够满足需求,因此进程会分配到这10MB的空间内,之后这块区域就会剩余1MB的空闲空间,所以需要修改此分区的分区大小信息为1(还需要修改分区起始地址等)。此外,修改分区相关信息后,还需要对整个空闲分区链重新排序(为了能满足算法的要求),因此此算法的开销较大
缺点:每次都选取(能满足需求且)最小的分区进行分配,这会使得内存中留下越来越多的、很小的、难以利用的内存块,因此这种方法会产生很多外部碎片
最坏适应算法
又称最大适应算法(Largest Fit)
思想:为了解决最佳适应算法的问题 —— 留下太多难以利用的碎片,可以在每次分配时优先使用最大的连续空闲区,这样分配后剩余的空闲区就不会太小,更方便使用
实现方式:将空闲分区按容量递减的次序连接,每次分配内存时顺序查找空闲分区链(或空闲分区表),找到大小能满足要求的第一个空闲分区
假设进程5需要进入内存,进程5需要内存空间为3MB,采用最坏适应算法时会从空闲分区链的链头处开始依次向后查找,当查找到第一个空闲分区(拥有20MB)时发现能够满足需求,因此进程会分配到这20MB的空间内,之后这块区域就会剩余17MB的空闲空间,所以需要修改此分区的分区大小信息为17(还需要修改分区起始地址等,但此时不需要对空闲分区链进行重新排序)
假设进程6需要进入内存,进程6需要内存空间为9MB,采用最坏适应算法时会从空闲分区链的链头处开始依次向后查找,当查找到第一个空闲分区(拥有17MB)时发现能够满足需求,因此进程会分配到这17MB的空间内,之后这块区域就会剩余8MB的空闲空间,所以需要修改此分区的分区大小信息为8(还需要修改分区起始地址等)。此外,修改分区相关信息后,还需要对整个空闲分区链重新排序(为了能满足算法的要求),因此此算法的开销较大
缺点:每次都选最大的分区进行分配,虽然可以让分配后留下的空闲区更大,更容易得到使用,但这种方式会导致较大的连续空间区域被迅速地用完。如果之后有“大进程”到达,就可能没有合适的内存分区可用了
邻近适应算法
思想:首次适应算法每次都从链头处开始查找,这就可能导致低地址部分出现很多小的空闲分区,而每次分配查找时,都要先经过这些分区,因此也增加了查找的开销。如果每次都从上次查找结束的位置开始继续向后检索,就能解决这个问题
实现方式:将空闲分区按起始地址递增的顺序排列(可排成一个循环链表),每次分配内存时从上次查找结束的位置开始查找空闲分区链(或空闲分区表),找到大小能满足要求的第一个空闲分区
假设进程5需要进入内存,进程5需要内存空间为5MB,采用邻近适应算法时,第一次查找会从空闲分区链的链头处开始依次向后查找,当查找到第二个空闲分区(拥有6MB)时发现能够满足需求,因此进程会分配到这6MB的空间内,之后这块区域就会剩余1MB的空闲空间,所以需要修改此分区的分区大小信息为1(还需要修改分区起始地址等)
假设进程6需要进入内存,进程6需要内存空间为5MB,采用邻近适应算法时会上一次查找的结束位置处开始依次向后查找,具体为先查找之前结束位置处的结点,发现它对应的分区大小为1MB,不能满足进程需求,于是继续查找下一个结点,下一个结点拥有10MB大小的空间,满足需求,因此进程会分配到这10MB的空间内,之后这块区域就会剩余5MB的空闲空间,所以需要修改此分区的分区大小信息为5
虽然邻近适应算法不需要像首次适应算法那样每次都先查找低地址处的小分区(这些小分区可能无法满足进程需求,因此会增加查找的开销),但这不意味着邻近适应算法比首次适应算法优秀:
- 首次适应算法每次都要从头查找,每次都先检索低地址处的小分区。但是这种规则也决定了当低地址部分有更小的分区可以满足需求时,会更有可能用到低地址部分的小分区,也就更有可能把高地址部分的大分区保留下来(最佳适应算法的优点)
- 邻近适应算法的规则可能导致无论低地址还是高地址部分的空闲分区,都有相同的概率被使用,也就导致了高地址处的大分区更可能被使用,并将其划分为小分区,最后导致无大分区可用(最坏适应算法的缺点)
综合来看,四种算法中,首次适应算法的效果反而更好
总结
算法 | 算法思想 | 分区排列顺序 | 优点 | 缺点 |
---|---|---|---|---|
首次适应 | 从头到尾找适合的分区 | 空闲分区以地址递增次序排列 | 综合来看性能最好。算法开销小,回收分区后一般不需要对空闲分区数据结构进行重新排列 | |
最佳适应 | 优先使用更小的分区,以保留更多大分区 | 空闲分区以容量递增次序排列 | 会有更多的大分区被保留下来,更能满足大进程需求 | 会产生很多很小的、难以利用的碎片;算法开销大,回收分区后可能需要对空闲分区数据结构重新排列 |
最坏适应 | 优先使用更大的分区,以防止产生太小的不可用的碎片 | 空闲分区以容量递减次序排列 | 可以减少产生难以利用的小碎片 | 大分区容易被用完,不利于大进程;算法开销大,回收分区后可能需要对空闲分区数据结构重新排列 |
邻近适应 | 由首次适应演变而来,每次从上次查找结束的位置开始查找 | 空闲分区以地址递增次序排列,形成一个循环队列 | 不用每次都从低地址的小分区开始检索。算法开销小,回收分区后一般不需要对空闲分区数据结构进行重新排列 | 会使得高地址处的大分区也被用完(相比于首次适应算法而言) |
基本分页存储管理
分页存储的概念
将内存空间分为一个个大小相等的分区(比如:每个分区为4KB),每个分区就是一个“页框”(页框 = 页帧 = 内存块 = 物理块 = 物理页面)。每个页框都有一个编号,即“页框号”(页框号 = 页帧号 = 内存块号 = 物理块号 = 物理页号),页框号是从0开始的
进程的逻辑空间也会被分为与页框大小相等的一个个部分,每个部分称为一个“页”或“页面”。每个页面也有一个编号,即“页号”,页号也是从0开始的
操作系统以页框为单位为各个进程分配内存空间。进程的每个页面放入一个页框中。也就是说,进程的页面与内存的页框存在一一对应的关系
各个页面在内存中不必连续存放,它可以放到不相邻的各个页框中
页表
由于进程的页面在内存中可以离散地存放,因此为了能知道进程的每个页面在内存中的存放位置,操作系统要为每个进程建立一张页表
页表起始地址通常存在PCB中
一个进程对应一张页表
进程的逻辑地址空间会被分为一个个页面,每个页面就对应了页表当中的一个页表项
每个页表项由“页号”和“块号”组成
页表记录着进程中的页面和实际存放的内存块之间的映射关系
每个页表项的长度是相同的
Eg1:每个页表项要占多少个字节?
假设某系统物理内存大小为4GB,页面大小为4KB:
块号:
内存块大小 = 页面大小 = 4KB = 2^12B 因此4GB的是内存总共会被分为2^32 / 2^12 = 2^20个内存块 内存块号的范围应该是0 ~ 2^20 - 1,也就至少需要20个bit来表示 但计算机分配空间至少是以字节为单位的,因此至少需要24bit(3B)来表示块号
页号:
由于页表中的各个页表项是连续存放的,因此页号可以隐含,不占存储空间(类比数组的索引)
例如,已知每个页表项占3B且都是连续存放的,假设页表中的各页表项从内存地址为X的地方开始连续存访,那么页号为i的页表项的起始地址就为
X + 3 * i
一个页表项在逻辑上包含了页号和块号两个信息,但在物理上,一个页表项只需要记录块号这一个信息
由于页号是隐含的,因此每个页表项的大小就是页表项中块号部分的大小,本例中为3B,若进程被划分为0 ~ n号页面,则存储整个页表至少需要
3 * (n + 1)B
注意:页表记录的只是内存块号,而不是内存块的起始地址,
J号内存块的起始地址 = J * 内存块大小
Eg2:如何实现地址转换(即逻辑地址转换到物理地址)?
类比进程在内存中的连续存放方式(假设系统采用动态重定位方式进行地址转换),操作系统将逻辑地址转换为物理地址的过程就是每次将重定位寄存器的值(即进程整体在内存中的起始物理地址)与目标逻辑地址相加后得到物理地址
在分页存储方式中,虽然进程的各个页面在内存中是离散存放的,但是每个页面内部是连续存放的
所以在分页存储方式中,根据逻辑地址A确定物理地址的步骤如下:
确定逻辑地址A对应的逻辑页号P
根据页表找到P页面在内存中的起始地址(即P页面所在的页框的起始地址)
确定逻辑地址A的页内偏移量W
逻辑地址A对应物理地址 = P号页面在内存中的起始地址 + 页内偏移量W
如何确定一个逻辑地址对应的逻辑页号以及页内偏移量?
假设某计算机系统中,页面大小为50B。某进程逻辑地址空间大小为200B,则逻辑地址110对应的页号、页内偏移量是多少?
页号 = 逻辑地址 / 页面长度 (取运算结果的整数部分)
页内偏移量 = 逻辑地址 % 页面长度 (取运算结果的余数部分)
因此 页号 = 110 / 50 = 2,页内偏移量 = 110 % 50 = 10
之后就可以通过逻辑页号查询页表,进而知道页面在内存中的起始地址,再将该起始地址与页内偏移量相加即可得到逻辑地址对应的物理地址
在计算机内部,地址是用二进制表示的,如果页面大小刚好是2的整数幂,则计算机硬件可以很快的把逻辑地址拆分成页号和页内偏移量:
假设计算机采用字节编址,用32个bit表示逻辑地址,页面大小为4KB = 2^12B = 4096B
结论:采用按字节编址的系统中,如果每个页面的大小为2^K B,用二进制数表示逻辑地址,则逻辑地址末尾K位即为页内偏移量,其余部分就是页号(页面号)
在此策略下,计算机不再需要使用除法和取余的操作来计算逻辑地址对应的页号和页内偏移量,而是直接取逻辑地址的前面几位作为页号,后面几位作为页内偏移量,因此计算机的可以更快速地得到页号和页内偏移量
假设物理地址也用32个bit来表示,则由于内存块的大小 = 页面大小,因此:
假设通过页表查询到1号页面存放在内存块9(1001)处,则:
结论:如果页面大小刚好是2的整数幂,则只需要把页表中记录的物理块号拼接上逻辑地址的页内偏移量就能得到对应的物理地址
如果页面大小不是2的整数幂,那么J号内存块的起始地址就需要通过
J * 内存块大小
来计算得出,(相比之下)这会导致硬件的效率降低
总结:页面大小刚好为2的整数幂有什么好处?
- 逻辑地址的拆分更加迅速 —— 如果每个页面的大小为2^K B,用二进制表示逻辑地址,则逻辑地址的末尾K位即为页内偏移量,其余部分就是页号。因此,如果让每个页面的大小为2的整数幂,计算机硬件就可以很方便地得出一个逻辑地址对应的页号和页内偏移量,而无需进行除法、取余等运算,从而提升了运行速度
- 物理地址的计算更为迅速 —— 根据逻辑地址得到页号,根据页号查询页表从而找到页面存放的内存块号,将二进制表示的内存块号和页内偏移量拼接起来,就可以得到最终的物理地址
因此现代采用分页存储方式的系统中,通常将页面大小设置为2的整数幂
逻辑地址结构
在采用了页面大小刚好为2的整数幂的策略下,分页存储管理的逻辑地址结构就为:
地址结构包含两个部分:前一部分为页号P(或称逻辑页号),后一部分为页内偏移量W(或称页内地址)
如果有K位表示“页内偏移量”,则说明该系统中一个页面的大小为2^K个内存单元
如果有M为表示“页号”,则说明在该系统中,一个进程最多允许有2^M个页面
如果页面大小不是2的整数幂,上面的方法就不再适用,还是得使用原始的方式:
- 页号 = 逻辑地址 / 页面长度 (取运算结果的整数部分)
- 页内偏移量 = 逻辑地址 % 页面长度 (取运算结果的余数部分)
总结
基本地址变换机构
基本地址变换机构是用于实现逻辑地址到物理地址转换的一组硬件机构,基本地址变换机构可以借助进程的页表将逻辑地址转换为物理地址
通常系统会设置一个页表寄存器(PTR),里面存放着页表在内存中的起始地址F和页表长度M
进程还未执行时,进程对应的页表的起始地址和页表长度放在进程控制块PCB中,当进程被调度时,操作系统内核会把它们放到页表寄存器中
假设系统的页面大小为L(2的整数幂),CPU对逻辑地址A到物理地址E的变换过程如下:
操作系统会把内存划分为系统区和用户区,系统区会存放一些操作系统对整个计算机软硬件进行管理的一些数据结构,其中就包括进程控制块PCB
当进程被调度,并上处理机运行时,进程切换相关的内核程序就会把该进程相关的运行环境恢复(即把PCB中存放的相关数据存放到CPU内的寄存器中),如把PCB中记录的页表起始地址和页表长度存放到页表寄存器中
除了恢复进程的相关运行环境外,还需要把进程的下一条将要执行指令的逻辑地址,即A保存到程序计数器PC中
接下来,CPU就需要把程序计数器PC中存放的逻辑地址转换为实际的物理地址,进而找到存放在内存中的指令
在采用分页存储管理的系统中,逻辑地址的结构是固定不变的,逻辑地址中页号占多少位,页内偏移量占多少位,这些信息操作系统都是清楚的
首先,系统会检查逻辑地址中页号部分的信息是否超过了页表寄存器中记录的页表长度,如果发现超出,则认为该逻辑地址非法,便会发出一个越界中断(内中断)
- 页表中有n个页表项时,页表长度M就为n,页表长度是从1开始的
若页号合法,则系统会根据每个页表项的大小(此信息操作系统清楚),以及页号P、页表寄存器中保存的页表起始地址F三者进行计算得到页号对应的页表项的起始地址,进而找到页号所在内存块的内存块号
在得道了内存块号信息后,就可以直接将内存块号与页内偏移量进行拼接得到指令所在的物理地址,然后CPU就可以顺利地找到指令并开始执行
具体步骤(默认系统按字节编址):
计算页号P和页内偏移量W
如果用十进制数手算,则P = A / L,W = A % L。但在计算机实际运行时,逻辑地址结构是固定不变的,因此计算机硬件可以更快地得到二进制表示的页号、页内偏移量
比较页号P和页表长度M,若P ≥ M,则产生越界中断,否则继续执行
注意:P = M时也会越界,因为页号是从0开始的,页表长度则是从1开始的
页表中页号P对应的页表项起始地址 = 页表起始地址F + 页号P * 页表项长度,取出该页表项内容b,即为内存块号
注意:区分页表项长度、页表长度、页面大小的区别。页表长度指的是这个页表中总共有多少个页表项,即进程总共有几个页面;页表项长度指的是每个页表项占多大的存储空间;页面大小指的是一个页面占多大的存储空间
计算物理地址E = 内存块号b * 页面大小L + 页内偏移量W,用得到的物理地址E去访存
如果系统中页面大小为2的整数幂,并且内存块号、页面偏移量使用二进制表示的,那么把二者拼接起来就是最终的物理地址
若页面大小L为1KB,页号2对应的内存块号b = 8,将逻辑地址A = 2500转换为物理地址E
等价描述:某系统按字节寻址,逻辑地址结构中,页内偏移量占10位,页号2对应的内存块号b = 8,将逻辑地址A = 2500转换为物理地址E
计算页号、页内偏移量
页号P = A / L = 2500 / 1024 = 2
页内偏移量W = A % L = 2500 % 1024 = 452
根据题目条件可知,页号2存在,没有越界,其存放的内存块号b = 8
物理地址E = b * L + W = 8 * 1024 + 425 = 8644
在分页存储管理(页式管理)的系统中,只要确定了每个页面的大小(页面的大小是由操作系统决定的),逻辑地址结构就确定了。因此,页式管理中地址是一维的。即只要给出一个逻辑地址,系统就可以自动地根据逻辑地址计算出页号、页内偏移量两个部分,而并不需要显式地告诉系统在这个逻辑地址中页内偏移量占多少位
所谓一维,是指要让CPU根据逻辑地址找到物理地址,只需要告诉CPU逻辑地址这一个信息即可
从CPU得到逻辑地址到CPU访问了实际物理地址的内存单元的整个过程里,总共需要进行两次访存:第一次访存 —— 查询页表,第二次访存 —— 访问实际物理地址对应的内存单元
扩展
每个页表项的长度是相同的,页号是“隐含”的
假设某系统物理内存大小为4GB,页面大小为4KB
内存总共会被分为2^32 / 2^12 = 2^20个内存块,因此内存块号的范围应该是0 ~ 2^20 - 1
因此至少要20个bit才能表示这些内存块号,但计算机是按字节分配空间的,因此至少需要3个字节来表示这些内存块号
正常情况下,页表中的各页表项会按顺序连续地存放在内存中,如果该页表在内存中存放的起始地址为X,则M号页面对应的页表项是存放在内存地址为X + 3 * M
由于一个页面的大小为4KB,因此每个页框可以存放4096 / 3 = 1365个页表项,但是这个页框会剩余4096 % 3 = 1B的页内碎片
因此,1365号页表项存放的地址就是X + 3 * 1365 + 1
一个完整的页表项必须全部存放在同一个页框中,不能跨页框存储
如果整数个页表项不能装满一个页框时,则查找页表项时就比较麻烦
如果每个页表项占4个字节,则每个页框刚好可存放1024个页表项,就不会出现页内碎片,这样不管查询几号页表项,都可以直接根据X + 4 * M
来找到
结论:理论上,页表项长度为3B即可表示内存块号的范围,但是,为了方便页表的查询,常常会让一个页表项占用更多的字节,使得每个页框恰好可以装得下整数个页表项而不产生页内碎片
当题目要求算出页表项最少需要多少个字节时,按照3字节的处理方法计算即可
具有快表的地址变换机构
快表(TLB)
快表,又称联想寄存器(TLB,translation lookaside buffer),是一种访问速度比内存快很多的高速缓存,用来存放最近访问过的页表项的副本,它可以加速地址变换的速度。与此对应,内存中的页表常称为慢表
注意:快表是一个计算机硬件,而不是一个数据结构,快表不能像慢表那样隐含页号,因为页表中页表项可能是随机被存放到快表当中的,因此在查找快表时就必须对快表中页表项的页号字段与要查询的页号进行匹配
计算机中的存储设备是分层级的
由于硬盘的数据读写速度很慢,而CPU处理数据的速度很快,因此CPU不可能直接从硬盘中存取数据,这会导致CPU处理数据的速度被硬盘的数据读写速度给拖累,导致系统整体性能的降低
因此CPU要处理的数据时,都会先从硬盘将数据读入内存时,内存的速度比硬盘要快上几十倍,把CPU要处理的数据先从硬盘存放到内存中,就可以缓和CPU与硬盘之间的速度矛盾
虽然内存的数据读写速度已经很快了,但还远远不及CPU处理数据的速度,因此系统又引入了高速缓存来进一步缓和CPU与内存之间的速度矛盾,高速缓存中保存了内存中最近可能被频繁访问到的数据
CPU在取数据时,会先访问高速缓存,如果在高速缓存中找到自己想要的数据则不必再访问内存,否则再访问内存,从内存中查找自己想要的数据
虽然层级越高的存储设备存取数据的速度越快,但其造价也越来越高,所以计算机为了兼顾系统整体的运行效率,同时还要考虑硬件成本,便采用了多层级的存储结构
引入快表后的地址转换过程
-
CPU给出逻辑地址,由某个硬件算得页号、页内偏移量,将页号与快表中的所有页号进行比较
-
如果在快表中找到了匹配的页号,说明要访问的页表项在快表中有副本,之后直接从中取出该页对应的内存块号,再将内存块号与页内偏移量进行拼接形成物理地址,最后,访问该物理地址对应的内存单元。因此若快表命中,则访问某个逻辑地址对应的物理地址内存单元只需要一次访存即可(即访问实际物理地址对应的内存单元)
-
如果快表中没有匹配的页号,则需要访问内存中的页表,找到对应页表项,得到页面存放的内存块号,再将内存块号与页内偏移量拼接形成物理地址,最后,访问该物理地址对应的内存单元。因此,若快表未命中,则访问某个逻辑地址对应的物理地址内存单元就需要两次访存(一次是访问内存中的页表,另一次是访问物理地址对应的内存单元)
注意,如果是在页表中找到页表项后,应同时将该页表项复制一份到快表中,以便该页表项在之后可能再次访问。若快表已满,则必须按照一定的算法对快表中旧的页表项进行替换
假设某进程在执行过程中要依次访问(0, 0)、(0, 4)、(0, 8)这几个逻辑地址,CPU访问一次TLB需要1μs的时间,访问内存需要100μs的时间:
注:逻辑地址(0, 4),0表示页号,4表示页内偏移量
当进程上处理机运行时,系统会清空快表当中的内容
- 快表是一个专门存储页表项的硬件,当发生进程切换时,快表中的内容就需要被清除
执行逻辑地址为 (0, 0) 的指令:
首先,系统会检查逻辑地址 (0, 0) 中的页号是否越界,即将逻辑地址中的页号与页表寄存器中记录的页表长度进行比较,如果发现页号未越界,则认为该逻辑地址合法,否则抛出越界异常
在该例中,要访问的逻辑地址均合法,因此接下来CPU就会查询快表,由于该进程刚刚上处理机运行,所以此时快表的内容是空的,在快表中找不到页号为0对应的页表项,即快表没有命中
由于快表没有命中,因此CPU不得不访问内存中的页表,具体过程为:CPU根据页号、页表寄存器中的页表起始地址以及页表项的长度得到页号对应页表项的起始地址,查询到页表项后,就得知0号页面所在的内存块号为600。在CPU访问该页表项的同时,还会将该页表项复制一份到快表中
最后,CPU在得知0号页面所处的内存块号为600后,就可以将其与逻辑地址中的页内偏移量进行拼接得到物理地址,然后CPU就可以访问该物理地址对应的内存单元了
执行逻辑地址为 (0, 4) 和 (0, 8) 的指令:
同样,先判断页号是否越界,发现没有越界,所以接下来根据页号在快表中查找有没有相应的页表项,由于快表中已经记录了0号页面对应的页表项,所以此次快表命中
快表命中后,系统就可以知道0号页面所在的内存块号为600,因此接下来系统就不需要再次查询内存中的页表,而是直接将快表中页表项里的内存块号信息与页内偏移量进行拼接得到物理地址,然后进行访存
总之,系统在引入了快表后,系统在进行地址变换时会优先查询快表,只有当快表未命中时才会继续查询页表
也有的系统会采用快表页表同时查询的方式
由于查询快表的速度比查询页表的速度快很多,因此只要快表命中,就可以节省很多时间
基于局部性原理,一般来说快表的命中率可以达到90%以上
注意:快表的容量很小,快表中存放的是页表的一部分副本
某系统使用基本分页存储管理,并采用具有快表的地址变换机构。访问一次快表耗时1μs,访问一次内存耗时100μs。若快表的命中率为90%,那么访问一个逻辑地址的平均耗时是多少?
分析:在采用先查询快表再查询页表的方式下,系统在访问一个逻辑地址时,会先查询快表,因此会消耗1μs的时间,如果快表命中,系统就可以直接得到最终想要访问的物理地址并访问该物理地址对应的内存单元,访问一次内存单元需要耗时100μs的时间,因此在快表命中的情况下,访问一次物理地址对应的内存单元需要101μs的时间,快表的命中率为90%,因此需要将101 × 0.9;如果快表没有命中(此时已经消耗了1μs),系统还需要查询内存中的页表,访问页表需要消耗100μs的时间,之后访问物理地址对应内存单元也需要消耗100μs的时间,因此在快表未命中的情况下,访问一次物理地址对应的内存单元需要201μs的时间,快表未命中的概率为10%,因此需要将201 × 0.1
平均耗时 = (1 + 100) * 0.9 + (1 + 100 + 100) * 0.1 = 111μs
有的系统支持快表和页表同时查询,如果是这样,平均耗时应该是(1 + 100) * 0.9 + (100 + 100) * 0.1= 110.9μs
若系统没有采用快表机制,则访问一个逻辑地址需要100 + 100 = 200μs的时间
局部性原理
int i = 0;
int a[100];
while(i < 100){
a[i] = i;
i++;
}
假设对于该程序,它对应的指令序列都存放在10号页面中,程序中定义的变量都存放在23号页面中,因此该程序在执行时,会很频繁地访问10号页面和23号页面
时间局部性:如果执行了程序中的某条指令,那么不久后这条指令很有可能会再次被执行;如果某个数据被访问过,那么不久之后该数据很可能再次被访问(因为程序中存在大量的循环)
空间局部性:一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也很有可能被访问(因为很多数据在内存中都是连续存放的)
根据局部性原理,某个程序在接下来的一段时间内很可能访问的是同一个页面,因此在地址转换过程中,只要访问的是同一个页面,那么查找的其实也是同一个页表项,所以只要把慢表当中的页表项复制一份到快表中,就可以让地址变换的速度快很多
总结
地址变换过程 | 访问一个逻辑地址的访存次数 | |
---|---|---|
基本地址变换机构 | 1. 算出页号、页内偏移量 2. 检查页号合法性 3. 查页表,找到页面存放的内存块号 4. 根据内存块号与页内偏移量得到物理地址 5. 访问目标内存单元 | 两次访存 |
具有快表的地址变换机构 | 1. 算出页号、页内偏移量 2. 检查页号合法性 3. 查快表。若命中,即可知道页面存放的内存块号,可直接进行5;若未命中则进行4 4. 查页表,找到页面存放的内存块号,并且将页表项复制到快表中 5. 根据内存块号与页内偏移量得到物理地址 6. 访问目标内存单元 | 快表命中,只需一次访存 快表未命中,需要两次访存 |
注意:TLB与Cache的区别 —— TLB中只有页表项的副本,而普通Cache中可能会有其他各种数据的副本
两级页表
单级页表存在的问题
假设某计算机按字节寻址,支持32位的逻辑地址,采用分页存储管理,页面大小为4KB,页表项长度为4B
分析:
4KB = 2^12B,因此页内地址要用12位表示,剩余20位表示页号
因此,该系统中用户进程最多可以有2^20个页面。相应的,一个进程的页表中,最多会有2^20 = 1M = 1048576个页表项,所以一个页表最大需要2^20 * 4B = 2^22B,共需要2^22 / 2^12 = 2^10个页框存储该页表
根据页号查询页表的方法:K号页对应的页表项存放位置 = 页表起始地址 + K * 4,但这种方法是以“所有的页表项在内存中都是连续存放”为前提得
因此在该系统中要想使用该方法得到任一页表项的起始地址,最坏的情况下就必须给进程专门分配2^10 = 1024个连续的页框来存放它的页表。而要为一个页表分配这么多连续的页框是不现实的,并且这也丧失了离散分配管理方式的优势
根据局部性原理可知,很多时候,进程在一段时间内只需要访问某几个页面就可以正常运行了,因此没有必要让整个页表都常驻内存,只需要让进程此时需要使用到的页面对应的页表项在内存中保存就可以了
问题一:页表必须连续存放(为了能节省页号字段以及方便顺序查找),因此当页表很大时,需要占用很多个连续的页框
参考解决“进程在内存中必须连续存储”的问题的思路 —— “将进程地址空间分页,并为其建立一张页表,记录进程的各页面的存放位置”,也可以用于解决“页表必须在内存中必须连续存放”的问题,把必须连续存放的页表再分页
可将长长的页表也进行分组,使每个内存块刚好可以放入一个分组(比如上例中,页面大小为4KB,而每个页表项4B,所以每个页面中就可以存放1K个页表项,因此每1K个连续的页表项为一组,每组刚好占一个内存块,再将各组离散地放到各个内存块中),分组之间可以离散地存放
另外,要为离散分配的页表项分组再建立一张页表,以确保这些离散存放在各个内存块的分组在今后还能够知道它们在内存中的位置和先后顺序,该页表就称为页目录表,或称外层页表,或称顶层页表
两级页表的原理、地址结构
假设系统采用32位的逻辑地址,页表项大小为4B,页面大小为4KB
由于页面大小为4KB,因此页内地址占12位,剩余20位为页号,采用单级页表结构时,逻辑地址结构如下:
因为页号占20位,因此一个进程最多可以有2^20个页面,页表也就最多可以有2^20个页表项,用20位二进制刚好可以表示0~2^20 - 1个页表项
拥有2^20个页表项的页表长度过大,按照之前的思路,可以将该页表的页表项进行分组,在本例中,每个页面的大小为4KB,因此一个内存块就可以存放4K / 4 = 1K = 2^10 = 1024个连续的页表项
最终,把拥有2^20个页表项的页表拆分为1024个子页表,并为每个子页表分配一个编号(0 ~ 1023),每个子页表中保存了1024个连续的页表项(1024 * 1024 = 2^20)
注意:每个子页表中的页表项都是从0开始编号的
拆分出子页表后,由于每个子页表的大小刚好为一个内存块的大小,因此每个子页表都可以被放入到不同的内存块中
为了记录这些子页表之间的相对次序以及子页表所在的内存块的编号,需要再为这些子页表们建立上级页表,该上级页表就叫做页目录表
相应的,那些被拆分而形成的子页表就叫做(一个个的)二级页表
高级页表和低级页表的结构一样,同样是隐含了一个字段(顶级页表隐含了低一级页表的页表号,最低级页表隐含了页面的页面号),然后记录物理块号(高级页表中的物理块号字段记录的是低一级的页表所在的内存块号,最低级页表中的物理块号字段记录的是页面所在内存块号),因此高级页表的页表项大小与低级页表的页表项大小是一样的,都与内存中物理块的数量有关
页目录表中记录了二级页表的页表号与二级页表所在内存块号之间的映射关系
采用两级页表结构后,逻辑地址结构也需要发生相应变化,具体变化如下所示:
原本占20位的页号需要被拆分为两个部分,前一部分(共10位)用来表示一级页号(对应二级页表的页表号),后一部分(共10位)用来表示二级页号(对应二级页表中的页号)
- 一级页号的最大取值代表了进程在内存中有多少个二级页表
- 二级页号的最大取值代表了一个二级页表中包含了多少个页表项
- 页内偏移量代表了一个内存块中包含多少个内存单元
注意:在具有两级页表的系统中,逻辑地址能这样划分的前提还是页面的大小为2的整数幂(只有页面大小为2的整数幂时,一个物理块才能存放2的整数幂个页表项)
例:将逻辑地址 (0000000000, 0000000001, 1111111111) 转换为物理地址,用十进制表示:
步骤:
按照地址结构将逻辑地址拆分成3个部分
从PCB中读出页目录表的起始地址,再根据一级页号查页目录表,找到下一级页表(二级页表)在内存中的存放位置
根据页目录表中记录的信息可知,0号二级页表存放在3号内存块中
根据二级页号查询二级页表,找到最终想访问的内存块号
根据二级页表中记录的信息可知,1号页面存放在4号内存块中
将得到的内存块号与页内偏移量结合得到物理地址
已知最终要访问的内存块号为4,并且每个内存块的大小为4096B,因此4号内存块的起始地址为4 * 4096 = 16384,页内偏移量转换为十进制后为1023,因此最终要访问的物理地址为16384 + 1023 = 17407
问题二:没有必要让所有页面都常驻内存,因为进程在一段时间内可能只需要访问某几个特定的页面
可以在需要访问页面时才把页面调入内存(虚拟存储技术)
可以在页表项中增加一个标志位,用于表示该页表项对应的页面是否已经被调入内存。若想访问的页面不在内存中,则产生缺页中断(内中断),然后操作系统将要访问的目标页面从外存调入内存
缺页中断是在执行一条指令,但该指令要访问的页面暂时还不在内存的情况下产生的,所以该中断信号与当前执行的指令有关,属于内中断
需要注意的几个细节
- 若采用多级页表机制,则(单个)各级页表的大小不能超过一个页面的大小
如果单个各级页表的大小超过了一个页面,那么就违背了采用多级页表策略的初衷
例:某系统采用按字节编址,采用40位逻辑地址,页面大小为4KB,页表项大小为4B,假设采用纯页式存储,则需要采用几级页表结构,页内偏移量占多少位?
页面大小为4KB = 2^12B,系统按字节编址,因此页内偏移量为12位
页号 = 40 - 12 = 28位
页面大小为2^12B,页表项大小为4B,则每个页面中可存放2^12 / 4 = 2^10个页表项
因此每一个各级页表最多可以包含2^10个页表项,需要10位二进制位才能映射到2^10个页表项,因此每一级的页表对应页号最多为10位
总共28位的页号至少需要划分为三级
一级页号占8位,二级页号占10位,三级页号占10位,页内地址占12位
- 两级页表的访存次数分析(假设没有采用快表机制)
第一次访存:访问内存中的页目录表
第二次访存:访问内存中的二级页表
第三次访存:访问目标内存单元
所以采用两级页表结构时,访问一个逻辑地址对应的内存单元需要三次访存
两级页表机制相比与单级页表机制,在访问一个逻辑地址时,需要多进行一次访存操作
因此,两级页表机制虽然解决了单级页表中存在的两个问题,但在内存空间利用率上升的同时,付出的代价是每一次逻辑地址变换时都需要多进行一次访存,这就导致访问逻辑地址时要花费更长的时间
规律:在没有快表机构的前提下,访问n级页表结构的逻辑地址对应内存单元时需要进行n+1次的访存操作