现代操作系统——内存管理

进程内存模型

在这里插入图片描述
首先进程的内存进行分段(segment),从下至上的介绍一下:
代码段(text segment):用来存放程序的代码,具有可执行,可读权限。程序不可能在运行的过程中更改自己的代码。
数据段(data segment):用来存放全局变量和静态变量(用static声明的)。
堆空间(heap storage):堆空间的大小是动态变化的,用来做动态内存分配。
栈空间(stack segment):栈空间用来保存局部变量,函数调用的实参,返回地址等。
环境变量:由系统设置的。

重定位

静态重定位

在进程装入(内存)时,对进程的地址进行修改。
缺点是会减慢装载的速度,同时一旦地址确定就不可再更改。

动态重定位

动态重定位需要借助CPU配置的两个特殊的硬件基址寄存器界限寄存器来实现。

基址寄存器保存了进程对应的物理内存空间的起始地址,界限寄存器保存进程程序的长度。

每次程序在运行时,程序的相对地址(0-程序长度)被发送到内存总线,在相对地址到达内存总线之前,CPU会自动地把基址寄存器的值加在相对地址前,就得到了程序在物理内存中的实际地址。而界限寄存器是为了防止访问地址越界,在转换成实际的物理地址之前还会与界限寄存器的值进行比较,如果越界了就直接终止访问。

所以动态重定位是在程序运行时进行重定位的技术。
它的优点在于,如果要改变进程在内存中的位置,不需要将进程的地址一个一个修改(静态重定位会导致进程的地址全部固定),只需要修改基址寄存器的值就行了。

分区式内存管理

分区式管理要求进程必须分配在一块连续的地址空间,一个区只能装入一个进程。问题是当可用内存比较零散的时候没法装入较大的程序。

连续内存分配管理的方式是分区式管理,而分区式管理主要有三种形式:单一分区固定分区可变分区

单一分区(Single-Partition)

单一分区只适用于操作系统只能处理单一进程的情况,通常只要将内存分为两部分,一部分给操作系统,一部分给用户即可;如果还有其他外部设备需要使用,只要分成三块就好。但是已经不适用于现代操作系统了。

固定分区(Fixed-Partition)

固定分区的示例:

在这里插入图片描述
■操作系统将内存划分为大小不尽相同的分区,一个分区只能装入一个连续的进程
■由于进程大小和分区大小不一定恰好适配,所以会有一部分不可以在使用的碎片空间叫内碎片(internal fragmentation)。
■在选择分区的时候选用Best-fit策略(最优),就是在所有能够装下进程的分区中选择最小的(因为这样产生的内碎片最小,对空间的浪费最小)。

ps:选择分区时除了Best-fit还有另外两种策略,但是并不好用First-fit和Next-fit。

可变分区(Variable-Partition)

可变分区的示例:
在这里插入图片描述
■可变分区不会在一开始就确定分区,而是随着操作系统的运行动态地确定分区。
■上图中随着进程释放和分配,会在进程之间留下一些可以再利用外碎片(external fragmentation)。
■由于进程不断在进程之间的空闲区(hole)分配,有可能会导致外碎片越来越小,从而不可再利用。
第一个解决办法是在分配策略上我们可以使用Worst-fit策略(选择最大的外碎片进行分配,这样之后产生的外碎片才有更大的可能在利用);
第二个解决办法是进行内存紧缩(memory compaction),如上面最右边的图,将所有空闲区合并,但是一般并不使用,因为在进行内存紧缩的过程会导致系统停下所有工作。

空闲内存管理

空闲内存就是上面所说没有进行进城分配的内存区域,又可以称为空闲区或空洞(hole)。

位图(bitmap)

在这里插入图片描述
使用位图,内存被分为被分为固定大小的分配单元(unit),每个内存单元用一个比特位表示,0表示空闲,1表示占用。

这种方法的主要问题在于,在装入进程时需要搜索位图找到一块连续大小足够的分配单元,这样的操作比较耗时。

链表(linklist)

使用链表,系统维护一个记录了进程和空闲区的链表,链表结点表示一个进程或是一块空闲区,结点信息包括标志位(P表示进程,H表示空洞),起始地址,长度,指针域。

使用链表时要注意链表结点的变化,一个进程结束,所占用的空间变为空闲区时要考虑能否跟临近的空闲区进行合并。

内存扩展

内存扩展是指用较小的空间装下较大的进程。主要有两中方式,覆盖(overlaying)和交换(swapping)。下面讨论的都是分区式管理,即进程必须在内存中一块区域内。

覆盖(overlaying)

举个例子:下图中表示一个程序可以分成一块主程序和三块子程序(或者扩展程序),主程序需要长期驻留在内存空间中,而子程序可以随时使用随时装入。
在这里插入图片描述
覆盖技术就是说,系统在为进程分配空间时,只需要分配一块空间给主程序,分配一个扩展空间给子程序,然后在子程序需要运行的时候轮流装入,这样就实现了用较小的内存,装入较大的进程。

但是这样的技术对程序员来说时不透明的,需要程序员十分了解进程对内存的使用情况。只有在早期才会由程序员手动地来进行内存管理。

交换(swapping)

在这里插入图片描述
交换技术是指在内存中的程序进入阻塞态,暂时无法运行时,系统将进程暂时换出到外存(磁盘),这个时候内存中就有更多的空间来装入其他的进程。

分页式内存管理

虚拟内存

事实上进程也并不需要把所有部分完整的装入到内存中,只需要保证运行需要的部分在实际的物理内存中就行。

虚拟内存的主要思想:每个进程有自己独立的地址空间(这个地址空间是操作系统“许诺”给进程,但实际上并不存在的),这个空间足够大,被分成很多块,这些块被称为页面(page),每一页由连续的地址范围。

在这里插入图片描述

如上图就是再具体一些的实现,部分需要的虚拟页被映射到实际的物理页框中,当进程运行到物理内存中实际存在的部分时,硬件会立即做出映射;当进程运行到物理页框中不存在的部分时,由操作系统负责把缺失的部分装入物理内存,然后重新执行失败的指令。(后面会介绍到这就是缺页中断以及缺页中断的处理方式)

分页(paging)

分页是将程序的逻辑地址空间(虚拟地址空间)分为大小合适的(page),而实际的物理内存地址被分为大小和页一致页框(page frame,也叫页帧),通过一个页表,将虚拟地址中的部分页(程序运行的部分)映射到实际物理地址的页框。

分页的说明:
在这里插入图片描述

请求式分页是在程序运行到的时候,向页框中装入相应的页,这样就可以实现用较小的地址空间装下较大的程序。

地址重定位

在没有虚拟内存的计算机上,系统直接将程序的逻辑地址送到内存总线上,然后读写具有同样地址的物理内存字;在有虚拟内存的计算机上,有一个从虚拟地址到实际物理地址的过程就是地址重定位。

在有虚拟内存的计算机上,逻辑地址被送到内存管理单元(MMU,Memory Management Unit),MMU把虚拟地址映射成物理内存地址再送到内存总线进行读写。实现这一过程的重要数据结构就是页表

■页表
在这里插入图片描述
在进行地址重定位之前,一定要先知道页面大小是多少,例如上图中页面大小为4KB(212),于是程序提供的逻辑地址中低12位表示页内偏移量,剩下的位表示页号,在页表中查到页号对应的页框号(假设在页框中),将其拼接在页内偏移之前,就得到实际物理内存地址,这就是重定位的具体过程。

ps:小技巧,在提供的逻辑地址为十进制是,十进制转二进制比较麻烦,而且在得到物理地址之后还得算回去,这时可以把逻辑地址除以页面大小,得数就是页号,余数就是页内偏移。

■页表的结构
下图是一个页表项的具体结构:
在这里插入图片描述
◉保护位(protection):用来设置权限,简单一点的办法是用一个比特位,0表示读/写,1表示只读来进行保护;更先进一点的,用三位表示读,写,执行的权限。

为了记录页面的使用情况而引入的修改位(modified)和访问位(referenced)。
◉修改位(modified):有时也被称为脏位(dirty bit),表示页在页框中是否被修改,如果被修改过,在进行页面置换的时候要向磁盘进行写回(更新),如果没修改过只需要直接丢弃就好(磁盘中还有一模一样的副本)。
◉访问位(referenced):用来记录页面的访问情况,在进行页面置换的时候用来做参考的依据。不常使用的页比频繁使用的页更适合被置换出去。

另一个十分重要的标记为是“在/不在”位(present/absent bit)。
◉“在/不在”位(present/absent bit):表示当前页是否在页框内,即是否在物理内存中,如果在就可以在访问的过程中直接进行地址的重定位,如果不在就会引起缺页错误(page fault),又叫缺页中断。

缺页中断
缺页中断是当程序运行到的逻辑段不在物理内存中,而导致CPU陷入操作系统的一中陷阱。当发生缺页中断时,系统找到一个使用最少的页框进行回收,然后把需要用的页装入页框内,修改映射关系后,重新执行引起陷阱的指令

ps:处理缺页中断后与处理常规中断后的步骤不同,处理完缺页中断,程序要从当前指令(引起陷阱的指令)开始执行,但一般中断会在发生中断的时候将当前指令执行完,在处理完中断后都是从下一指令开始执行(程序计数器)。

在任何分页式系统中,主要考虑两个问题:
◉(速度)虚拟地址到物理地址的映射必须非常快。
由于有时执行指令需要访问到内存,而访问内存必须要进行地址重定位,那么对映射速度的要求就是不能在一条指令的执行时间中占比太大,否则就会称为一个制约。
◉(容量)如果虚拟地址空间很大,页表也会很大。
每个进程都有自己的页表,虽然虚拟地址空间让大进程在小内存上的装载成为可能,但是如果页表本身就大的离谱,页表本身就会称为主要矛盾。

下面是针对两个问题的解决方案:

加速分页

进行加速分页主要是因为,采取了分页机制后,两次内存访问才能实现一次内存访问(先访问内存中的页表获得物理内存地址,再对物理内存地址进行访问),这样无疑会使得程序性能下降一半。

通过观察发现:程序对页的访问也有一定程度上的“偏爱”,对于少量页面会进行多次访问,而其他页面可能很少被访问到

转换检测缓冲区(TLB)

转换检测缓冲区(Translation Lookaside Buffer)又称为相联存储器(associate memory)或快表(相对应的,原始页表也可以叫做慢表)。它是通过硬件来实现的。

TLB通常在MMU中,保存有少量的页表项(就是经常访问的页表项),在虚拟地址进入MMU进行重定位,查表的时候先查快表,如果快表中有相应的页表项(称作命中,hit),就直接将页框号取出用就行了(前提是不能违反保护位,TLB保存的页表项中也是有相关信息的,如果企图对一个只读的页面进行写操作会引起保护错误);如果TLB中没有(称作不命中,miss),再去查找慢表,然后从TLB中淘汰一个表项(并将该表项的访问位和修改位更新到内存中的页表项中)。

处理大的虚拟地址空间

操作系统为每个进程都许诺了非常大的地址空间,但在很多情况下程序根本就用不到那么大,甚至大多数时候只用到很小一部分,那也就意味这,没必要将所有的页表项都放在内存中。

多级页表

多级页表的示例如下:
在这里插入图片描述
将全部的页表项分块并添加一个索引值,然后取间隔大小固定的页表项合并进一个新的页表(上一级页表),这样可以使得直接存储在内存中的页表变小。重点在于应用多级页表时,逻辑地址中的页表项会有一些有趣的变化。

举一个之前的例子,在32位系统中,如果页面大小为4KB(212),一个页表项是4个字节,也就是说逻辑地址的低12位都是页内偏移量,高20位是页表的索引。
在采用多级页表的方式下,用一个页面记录一个低一级的页表,也就是说一个高一级的页表可以放下212/22 = 210个低一级页表,所以就可以将高20位的页表索引拆成2部分,一部分作为高一级页表的页内偏移,就可以找到对应的低一级页表,另一部分作为低一级页表的页内偏移。(可能没描述清楚,回头再整理)

对于64位系统,理论上我们可以将页表分成更多级,但事实上一般不这么做,因为分级越多,意味着访问一次内存需要进行更多次的内存访问。

倒排页表

倒排页表又叫转置页表。事实上,真实的物理地址也并不会完全跟理论上的那样大(比如64位系统的物理内存不会真的由264),因此我们用倒排页表表示一个页框对应的是哪一个页表项。倒排页表只有一个,与真实的内存大小一致。

在这里插入图片描述

但是相应的就会出现一些问题,倒排页表的方式与逻辑地址向物理地址转换的过程是相反的,倒排页表以物理地址做索引,逻辑地址做内容,因此会使得地址重定位变得很困难,每次进行逻辑地址转换的时候需要吗遍历整个倒排页表。
如果想使用倒排页表就要将其和TLB还有通过散列表(看数据结构)这一数据结构对虚拟页面进行散列计算。(不细说)

页面置换算法

缺页中断并不一定会引起页面置换,页面置换也并不只有在发生缺页中断的时候才开始。系统会给空闲内存空间设定好一个阈值,当空闲空间大小较小的时候,操作系统会“偷偷的”将不常使用的页面清理出去,以至于在程序真的迫切需要空间时不影响程序运行,这个过程叫偷页

置换策略有两种:
■局部置换和全局置换
局部置换是指一个进程只能使用固定数量的页帧,全局置换是指所有的进程共享所有的页帧。

■抖动
发生缺页中断,而内存空间已满的时候,操作系统会根据一定的规则选择页面置换出去来装入新的页面,但如果刚刚置换出去的页面又要置换回来,这样频繁进出的现象叫做抖动
抖动无疑会使得程序性能下降,因此要选用合适的规则,也就是合适的页面置换算法。

最优页面置换算法(OPT)

最有页面置换算法很好描述,但是实际上是不可实现出来的,因为没法对未来的情况了解的十分清楚,所以不存在这样的算法可以百分百保证做出的选择都是最优的。最优页面置换算法的意义是拿来做参考。

最近未使用页面置换算法(NRU)

最近未使用页面置换算法是根据页表项的R位和M位来构造的,其中R位会被操作系统定时清零(一般是一次时钟中断),用来区分最近一段时间内没有被访问的页面。而根据R位和M位的值可以将页面分成下面4类:

R位W位备注
第一类00没有被访问没有被修改
第二类01没有被访问已被修改
第三类10已被访问没有被修改
第四类11已被访问已被修改

根据最近未使用页面置换算法,第一类到第四类页面被置换出去的优先级越来越低,操作系统从类编号最小的非空类中随机挑选一个页面置换出去。

NRU的优点是已于理解和实现,虽然性能不是最好的,但是已经够用了。

先进先出页面置换算法(FIFO)

先进先出,操作系统维护一个链表来记录装入内存中的页面,发生页面置换的时候把表头的换出,新来 的放在表尾。
FIFO很难保证程序的性能,因此很少使用纯粹的FIFO。

第二次机会页面置换算法(SC)

在这里插入图片描述

第二次机会算法是在FIFO的基础上改进而来的,目的是找到一个最近时间间隔内没有被访问到的页面。
具体的实现与FIFO大致相同,根据R位,如果R位为1,说明页面有一定的用处,将该页面的R位清零,重新放到队尾并修改装入时间(就像新装入的一样);但R位如果是0,直接将页面置换出去即可。

时钟页面置换算法(Clock)

在这里插入图片描述

时钟页面置换是对第二置换的一个改进,SC经常要对链表进行删除和添加的操作,时钟页面直接将链表实现成环形链表,时钟的指针指向当前最老的进程,其他的实现方法与SC都一样,当页面R位为1时只要将R位清零然后指针后移就可以了。

最近最少使用页面置换算法(LRU)

我们并不能得知页面在未来的使用情况如何,但是可以根据当前的页面使用情况来进行预测:可以做出一个合理的假设,就是目前频繁使用的页面随后也会被频繁使用,而目前很少使用的以后也可能很少会使用。

这样的算法理论上是可以实现的,而且效果非常不错,但是实现的代价太大。

首先我们需要根据每个页面近期的使用次数维护一个频率递减的链表,问题在于,每次访问内存的时候都要对链表进行更新,这是十分耗时的。
另一个办法是通过一个特殊的硬件来实现,计数器来记录页面的最近使用次数。且不说硬件以及将硬件记录进页表项的代价,为了找到使用次数最少的页表,我们需要对所有的页表项进行遍历,这个过程也是非常耗时的。

最不常用页面置换算法(NRU)

用软件计数器的方式来实现LRU,在每次时钟中断时,R位清零前,将R位加到计数器上,这样可以记录一个页面的访问次数。

但是这会导致另一个严重的问题,就是这个软件计数器并不随着时间更新,一个曾经应用很频繁,现在不怎么需要的页面可能会将新装入但是访问很频繁的页面挤出去。

老化算法(Aging)

老化算法是对NRU的一个改进,针对NRU计数器的问题,每次发生始时钟中断,更新计数器时,先将计数器右移一位,并将R位的值加到最高位上,如下图所示:
在这里插入图片描述
在必须进行页面置换的时候,对各个页面的计数器从最高位向最低位开始比较,就能选出最近最不常使用的页面。

老化算法是非常近似LRU的算法。

工作集页面置换算法(Working set)

■局部性访问
程序的局部性访问是指在进程的任何阶段,进程都之访问较少的一部分页面。

■颠簸
若每执行几条指令就发生一次缺页中断,那就称这个程序发生了颠簸

■工作集
一个进程当前正在使用的页面集合称为它的工作集。

■请求调页(demand paging)
进程在启动时内存中什么都没有,在执行第一条指令开始,缺页中断就会接连发生。这个策略叫请求调页,页面是在需要的时候调入。

■预先调页(prepaging)
系统设法跟踪进程运行的工作集,以确保进程在运行前,工作集就已经在内存中,这样可以大大减少缺页中断率。在进程运行前预先装入工作集页面称为预先调页

■当前实际运行时间
一个进程从开始执行到当前实际使用的CPU时间总数通常称作当前实际运行时间

在这里插入图片描述

算法的工作方式:
◉设置一个值τ用来定义工作集模型(令在时间τ内使用过的页面集合为当前进程的工作集),并为每一个页表项添加一个位记录“上次使用时间”。
◉当发生缺页中断时扫面页表。如果R位为1,把“上次使用时间”更新为当前时间;如果R位为0,检查程序的生存时间(当前实际运行时间减上次使用时间),只要选择生存时间最长的页面置换出内存就好。对于生存时间大于阈值的页面,该页面已经不在工作集中;如果所有页面都在,又必须选出一个置换出去,就选择生存时间最长的。

工作集时钟页面置换算法(WSClock)

和时钟页面置换算法一样,需要一个以页框为元素的循环表,每个表项包含工作集算法的上次使用时间和R位以及M位。

在这里插入图片描述

工作方式:
◉检查指针之指向的当前页面,检查R位。
◉如果R位为1,说明页面近期使用过,将R位改为0,继而检查下一个页面。(如上图a,b)
◉如果R位为0,当生存时间大于阈值,说明页面干净且不在工作集中,非常适合置换出去;当生存时间小于阈值,暂且放过,继续检查后面的指针。(如上图c,d)

工作集页面置换算法需要遍历整个页面,工作集时钟算法相对来说效率更高。

ps:其他的没看懂

页面置换算法小结

下表是上面涉及到的所有页面置换算法:
在这里插入图片描述
在一开始讨论页面置换的时候说过“偷页”的概念,即系统在空闲内存空间较小的时候就开始进行页面置换。那是不是空闲内存空间增大,缺页中断发生的次数也会降低或者至少维持不变?是的。

■Belady异常
当增加物理地址空间的页帧数,同一进程序列和使用同一页面置换算法时,反而使得发生缺页中断的次数变多,说明页面置换算法选择不当(只有FIFO会产生这种现象),这种现象称作Belagy异常。

分页式内存管理小结

分页对于程序原来说时透明的,分页是对地址空间的切片处理,但是这种处理比较机械,并不考虑内容之间的逻辑关系。

分段式内存管理

分离地址空间

进程对内存空间的应用主要分为两部分,程序(code)和数据(data)。我们可以将原来的单个地址空间划分成两个独立的空间用来存放进程的代码和数据,称为I空间和D空间,如下图所示:
在这里插入图片描述
在这样的设计中,两种地址空间都可以进行分页处理而且相互独立。I空间和D空间各自有自己的页表,在完成虚拟页面到物理内存的映射时相互独立。

这样的实现方式有利于对权限的保护和页面的共享,除此之外还可以使可用的地址空间翻倍。对于指令也就是程序的权限是可读可执行,而数据的权限是可读可写不可执行

共享页面和共享库

共享页面主要是避免在内存中又饿同一页面的不同副本,这样就平白浪费了空间。
但也并不是所有的页面都适合做共享,对于只读的页面(比如程序正文)就能,但是数据段就不行(因为可以更改)。

■写时复制
对于共享的数据来说,如果数据只读,那共享起来也没有什么问题,可以在内存中只有一个副本然后映射到不同的进程中;但一旦发生了对数据的写操作就会触发只读保护,随后引发操作系统陷阱,随后就会进行复制,给两个进程各一个副本。这种策略表明,那些始终不需要写的页面是不需要进行复制的,只有实际需要修改的页面需要复制。

分段

目前我们见过的都是对许多内容连续的存放在地址空间中,而随着程序的运行,会有一些内容逐渐增长而导致和其他内容重叠,如下图所示:
在这里插入图片描述
对这种问题的解决方法就是提供多个相互独立的称为“” 的地址空间,每个段的地址都是从0开始(这就像为系统中的各个进程提供独立的地址空间是一样的),段的长度是在运行期间可以动态改变的。

在这种分段的存储器中指明一个地址,需要提供两部分地址:段号和段内地址。

分段的好处:
◉分段有利于对进程不同类型的内容进行保护(因为不同类型的内容反在不同的段),而且有利于共享(与页面共享相同)。
◉简化对经常变动的数据结构进行管理。像上面说的,在一维的存储器中,随着程序运行,有些内容的增长可能会导致和奇特部分的重叠。
◉简化对各个单独的过程进行链接的过程。在一维的存储器中,由于程序某些部分大小的变化可能会导致其他不相关部分的起始地址发生变化。

纯分段的实现会导致和可变分区类似的外碎片的现象。随着段的换入换出,会导致在段与段之间留下空闲区。这样的外碎片也可以通过内存紧缩来解决。

段页式内存管理

把每个段看成一个虚拟内存并进行分页处理,意图结合分页式(统一的页面大小和在只使用段的一部分式不全部调入内存)和分段(易于编程,模块化,保护和共享)的优点。

每个进程维护一个段表,段表的表项称为段描述符,段描述符的内容如下:
在这里插入图片描述

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值