修改物理页面内存属性_现代操作系统-内存管理

内存管理

9e2328a75ecdaba8921da7afc5bac0aa.png

地址空间

概念

  1. 地址空间是,一个进程可用于寻址内存的,一套地址,的集合.每个进程都有自己的地址空间,并且这个地址空间独立于其他进程的地址空间(除非有共享).
  2. 在这里介绍一下基址寄存器和界限寄存器.他们的主要作用是给出实际地址的地址偏移量和地址范围.程序的起始物理地址装载进基址寄存器中,程序的长度放在界限寄存器里.缺点在于,每次访问内存都需要进行加法和比较运算.

交换技术

主要原理就是:把一个进程完整的调入内存,运行一段时间,再存回磁盘,空闲进程主要在磁盘上.

但是交换空间可能出现多个小内存空闲区的情况,这时可以使用内存压缩技术,这个技术比较吃CPU.

进程需要增长,所以可以通过把它移动到更大的空间来实现,或者移除它相邻的进程.如果一个进程在内存中不能增长,并且磁盘空间也满了,那么只能把它挂起直到有可用空间.

不过,可以预先分配足够大的内存,供后期增长;在移出时,仅仅移除已使用的内存.

在一个进程中,具体来说一般都是堆栈和数据会增长,正文一般不会动,那么可以把堆栈放程序空间的上面,数据放下面,中间是可以供增长使用的空闲空间.

b5a28bf589d3210fac6ab4a35a38ea9f.png

空闲内存管理

  1. 使用位图的存储管理.这种方式类似邻接矩阵,用0和1记录某个内存块是否被占用.分割的内存块的大小决定了位图的大小.缺点很明显,在内存很大时,需要很大的位图来保存,这本身就很占用内存;而且在查询可用的位置时,需要遍历整个图,这是一个耗时的操作.

9ddd538799f815f9a030feea3dacca8a.png
  1. 使用链表的存储管理.简单来说就是,每个节点包含一个指向前面节点的指针和指向后面节点的指针,一个记录当前位置是空闲还是使用的标志,起始地址(内存块的地址,但内存块是好几个字节组成的,这个起始地址就是这个内存块最左边的地址),长度(此内存块多大).

对于改变某个节点状态,会产生四种结果,如果当前节点被置为空闲且相邻节点也是空闲的,可以把他们合并,所以四种操作如下所示:

a2cf120ce34d00c6a9b1680e1c7cda62.png

那么如何找到需要大小的内存呢?来看几个算法:

首次适配算法,会沿着链表一直找,直到找到足够的空闲区.

下次适配算法,在首次适配的基础上记录一下位置,方便下次使用,看起来很好,可是实际性能还不如首次适配.

最佳适配算法,遍历整个链表,找到最小可用空闲区,虽然其本意在于减小内存使用,但实际上,它会产生大量小的空闲区,反而浪费了内存.

最差适配算法,每次找最大可用的空闲区,但实际使用也不咋地.

如果分离进程和空闲区,分出两个链表,各自维护,可能会高效,但是代价是增加了复杂度和内存释放(两个链表均实行了操作).

还可以对链表排序;或者,打表,就是为那些常用大小的空闲区维护一个链表,每次需要直接获取.这个叫快速适配算法

这些算法都有一个缺点,就是在进程换出或终止时,需要进程合并操作,这是一件耗时的事.

虚拟内存

虚拟内存的基本思想是,每个程序都有自己的地址空间,这些地址空间被分割成许多块,每一块被称为页面(页),每一页有连续的地址范围,但是页与页之间不一定;这些页被映射到物理内存中,但是不是全部页都被映射到了.当程序访问未映射的页时,会触发缺页中断,此时有系统进行创建映射(包括找到另一个页框,备份,写入新数据,创建映射关系),然后重新执行刚刚被中断的指令.

当一个进程等待它的一部分读入内存中时,可以把CPU交给别的进程使用.

注意,程序向操作系统申请内存,操作系统派给内存(虚拟内存),一般情况下都是操作系统为当前进程选定一大块区域给它用,进程不知道这个事情,他只管要,系统封装了细节.一般来说,申请来的内存都是那一大块内存里的(虽然这一大块也是虚拟的,不过通过映射,对程序来说那就是真实的内存)

分页技术

由程序产生的这些地址称为虚拟地址,他们构成了一个虚拟地址空间,在使用虚拟内存的计算机上,虚拟地址被送到内存管理单元(MMU)中,进行解析得到物理地址或进行重新映射.虚拟地址空间按照固定大小划分成一个个的页面,而在物理内存中,与页面大小一致且与页面对应的,是叫页框的东西.那便是页面映射的目标了.

087cda0ff0a08ee8e239ffe737acf683.png

页表

页表就像一个函数一样,可以把虚拟地址映射到具体的(精确到字节的)物理内存地址.它定义了页面与页框的一一对应的映射关系.

MMU内部大概如下所示,里面有一个页表,指出了页面如何映射到页框:

14ec554261c6e79a195b3eeff6bacff5.png

对于输入,假设是一个16位数,那么前4位作为页号(页表的下标),后12位作为偏移量,得出的地址为定位到的页表项的值(新的4位)+偏移量.

一个页表包含很多项,每项的大概结构如下所示:

014903b7675527f69eff885f09eb6c72.png

来看一下具体各项参数的意义:

页框号用来定位实际的地址. 在/不在位用来标识此项是否可用. 保护位用来指出可进行的操作,是读还是写还是执行. 修改位记录调入内存后页面是否被修改过. 访问位记录被访问的次数. 高速缓存禁止位指出是否开启高速缓存.

关于高速缓存位,如果是即时IO操作,最好关掉,不然可能会造成读取失败.关于访问位,那是给操作系统看的,用来决定置换哪个页面.

加速分页过程

  1. 转换检测缓冲区 TLB,又称快表,用来记录常使用的映射关系,这样在需要的时候直接查表就可以了,而不需要通过MMU进行转换.TLB中含有页表项的大多数,除了虚拟页号外(它在页表中起到索引的作用,而在TLB中,整个虚拟地址就是索引(数组下标),故不再需要虚拟页号的存在),几乎都有.

如果某次执行匹配,发现TLB没有此项,会在MMU查询之后从TLB中淘汰一个,并进行替换.

  1. 软件TLB管理 在内存中申请一块地址,保存TLB表,刚刚的那种方式是通过硬件实现的;这种软件的方式是交付给OS来实现.此时的OS不仅进行TLB维护,还进行TLB更新,页面替换,缺页中断处理等.不过要记得在TLB中固定这个软件TLB的地址,防止被替换,那就是"我杀我自己"了,就很尴尬.

来看看软失效和硬失效. 软失效查询的页面不在TLB但在内存中,这种不需要磁盘IO操作. 硬失效需要硬盘IO操作,并置换页面,更新映射关系,进行页表遍历(在页表结构中查找相应的映射)这个操作是软失效的百万倍耗时.

次要缺页错误指的是需要的数据已经被其他某个进程调入内存,此时重新整理映射关系就好. 严重缺页错误指的是需要从磁盘调入某个数据到内存中并建立映射关系.

针对大内存的页表

把整个页表保存在内存里不一定可以,尤其是在内存很大的情况下,那就一点都不现实了.来看两个解决方法.

  1. 多级页表

使用两个或多个页表进行映射,为什么这么设计呢?因为对一个程序来说,给它分配的内存,尤其是中间的空闲区(用来增长的)根本没必要加进去,而这些空闲区往往还占了很大的空间,所以采用分级的方法,就能减少表的大小.

比方说在顶级页表的第0级,保存程序正文,倒数第一级,保存堆栈,倒数第二级保存数据,中间都是不用的.然后刚刚的三级分别指向三个二级页表,每个二级页表再映射到页框上,这样,只需要四个页表就能保存了.

915c10c06a8af7707d021de91af0bed8.png

对于顶级页表中间的,把他们的在/不在位设为0,禁止程序访问,这样可以监控程序的行为.

  1. 倒排页表 刚刚那种都是页面映射页框,注意,在实际上可能是多个页面映射一个页框(当前是A进程映射页框A,过会可能是B进程映射页框A,但是页表都记下来了),但是实际运行只有一个页面和一个页框在映射.所以可以设计某种方式,让一个页框对应多个页面,这样就节省很多空间了.

但是这种方式有一个弊端,就是想找到页面对应的页框时,得全部遍历,即使有TLB,这也不是个好办法,于是乎,可以使用哈希表,获取虚拟地址的哈希值,再使用链接法,把他们链接起来,注意,此时链接的是一个个的小结构体,它们由页面和页框组成,所以还是可以找到页面对应的页框的.

95eb246c3cc3b7aa954c42382f706edc.png

此方法在64位机里很常见.

页面置换算法

在执行页面置换之前,记得进行备份页框的数据.如果准备换出的页面在内存上被修改过,记得备份到硬盘,否则直接覆盖.

最优页面置换算法

通过某种神奇的方式获取最不常用的页面并置换,可是不可能实现.

最近未使用页面置换算法

对于页面,定期的把它的R位(访问位)清零,以区别最近没有被访问的页面和被访问的页面.

当发生缺页中断时,操作系统检查R位和M位(修改位)的值,并把页面分为四类:

第0类:没有被访问,没有被修改; 第1类:没有被访问,已被修改; 第2类:已被访问,没有被修改; 第3类:已被访问,已被修改;

当准备置换时,从编号最小且非空类里选一个页面出来.

先进先出页面置换算法

排个队,每次把新加入的页面排到队尾,替换时优先选择队首的.

第二次机会页面置换算法

在先进先出算法的基础上,检查R位,如果是0,就置换,否则置0并加到队尾.

时钟页面置换算法

类似第二次机会算法,但是是环形链表形式的.

当发生缺页中断时,首先检查当前指针指向的页面R位,是0就替换,并前移指针,否则置0前移指针.

最近最少使用页面置换算法

此方法要求有一个硬件计数器,在每条指令执行完后自增,然后访问页面时自动加到页面上去,这个值代表页面的上次使用时间,越小说明页面越旧,最后替换就行,可是难点在于,不好实现.

用软件模拟LRU

在这里,为每个页面添加程序计数器,然后使用一种老化算法,首先,在R位被加到计数器之前,右移一位计数器,再把R加到左边,这样就完成了更新.这样就能保证计数器越大,页面被访问越频繁.

e2a881e74efb90072db57543e12c1c54.png

该算法和LRU的区别是老化计数器只有有限位数,所以没法更细致的区分页面.

工作集页面置换算法

首先明确几个概念.一个线程当前正在使用的页面的集合被称为工作集.若每执行几条指令就发生一次缺页中断,那么成这个程序发生了颠簸.

在多道程序设计系统中,经常把进程转移到磁盘上(移走全部页面),但是再转换回来就很容易发生缺页中断,遂引入工作集模型,确保进程在运行前,它的工作集就已经在内存中了.在程序运行前预先装入其工作集页面也称为预先调页.

工作集是随时间变化的,设在任意时刻t都存在一个集合,它包含最近k次内存访问所访问过的全部页面,这个集合w(k, t)就是工作集,因为最近k=1次访问的页面必定属于k>1次所访问的页面.所以w(k, t)是k的单调非递减函数.当然,w(k, t)并不能无限增大,所以得到这样的图像:

dad903232df449230441dcf0eba52a90.png

k的值有一个很大的范围,它处在这个范围中时,工作集不会变,因为工作集随时间变化很慢.预先调页就是在程序运行之前预先推测出工作集(根据上次运行结果推测)并装入内存.OS还可以根据推断出的工作集,决定淘汰哪些个页面.

实际实现比较困难,于是选用近似方法,考虑执行时间,什么意思呢?进程的工作集可以定义为在过去的t秒的实际运行时间所访问过的页面的集合.然后在页表上添加一个时间属性.

工作过程:在时钟中断时,会清空R位.每次发生缺页中断时,就扫描页表来找寻合适的页面,此时检查每个页表的R位;

如果是1,就把当前时间写入到页表项的"上次使用时间"域,说明这个页面在当前时钟滴答(上一次时钟中断到现在时钟中断这个时间内)中已经被访问过;

如果R是0,则可以作为候选者,然后计算它的生存时间,与t比较,如果大于t就替换它,然后继续扫描以更新剩余的项.

如果R=0同时生存时间小于等于t,那么临时留下来,但是要记录生存时间最长的页面,然后继续,最后如果找到了多个R=0的页面,那么淘汰生存时间最长的页面.

最最最后,如果R都为1,那随机淘汰一个,最好淘汰一个干净的页面.

4314f756eb4bada614698939a4f337b4.png

工作集时钟页面置换算法

此算法类似时钟算法,简述其工作流程:

首先检查指向的页面,如果R=1,置0移动到下一个页面;

如果R=0且生存时间大于t且页面干净,申请此页框;如果被修改过,为了避免提前的IO操作,指针继续向前走;

如果回到起点,那么肯定发生二者之一的情况:至少调度了一次写操作;没有调度过写操作.

第一种情况,是我们需要的,没话说,置换遇到的第一个干净的页面;第二种,随便选一个干净的页面,所以需要遍历途中记录干净的页面,如果这也满足不了,那就替换当前页面

小结

实际使用中,软件模拟LRU页面置换算法和工作集时钟页面置换算法比较可行.

分页系统中的设计问题

局部分配策略与全局分配策略

来看一个实际的问题,如果进程进行新的映射,那么问题来了,是替换整个虚拟空间的最合适的页面呢?还是仅仅替换当前进程的虚拟空间的合适的页面呢?前者叫全局策略,后者叫局部策略.全局策略动态的分配进程的空间,局部策略则是固定了进程的内存空间大小.

使用局部策略需要在一开始指定合适的大小.如果使用全局策略,那就需要PFF(缺页中断率)算法来动态的更改分配给某个进程的页面数.此算法会计算每秒的缺页中断数,或者使用过去某些秒内的数据为参考,然后得出一个结果,如果缺页中断率过低,甚至可能剥夺某些进程的页面(进程口吐芬芳).如下图所示:中断率应保持在A和B之间.横坐标是时间.

313dbc0c5606597d7e2739b791d4d37b.png

对于某些算法,只有局部策略才有意义,比如两个工作集算法.

负载控制

如果某个进程发生了颠簸,且一直不好解决,那么可以尝试把一些进程移除内存,交换到硬盘,完全让出空间.如果还是不行,继续交换进程.但是并不收回他们的页面,不过在换出时,要记得考虑进程类型(IO密集型还是计算密集型),以及进程的特性.

页面大小

页面大小的选择也很重要,选择大了,会造成浪费,选择小了,会增加页表大小,以及管理的复杂度.小的页面还会占用更多的TLB,要知道,TLB对性能而言,异常重要.传输一个大的页面到硬盘和传输一个小的页面差不多.最后,设进程平均大小是s字节,页面大小是p字节,每个页表项需要p字节,我们来看看内存开销:

开销=se/p+p/2

求导得当p=(2se)^(1/2)时,开销最小.

现在常见的页面大小是4KB或8KB.

分离的指令空间和数据空间

在这种模式下,两个空间分别有自己的页表,自己的映射,自己的物理空间,分离的意义是为了更好地利用空间.程序放程序空间,数据放数据空间.

a3b239ee30dce6152dd8f93455a1add2.png

共享页面

为了减少内存空间地重复,更好地增加进程间传输能力.

有一点,当A进程结束时,应该可以注意到A进程的某些页面是否被共享了,是否被B进程所使用.所以需要一个专门的数据结构记录共享界面.

当然,还要记得加上写时复制,如果某个进程试图更新共享页面,则立马生成此页面地副本,这样每个进程都有自己的副本了,记得,只有实际修改的数据页面需要复制,这样就能保证每个进程的独立性了.

共享库

如果某个程序已经装载了某个库在内存中的话,新的进程就不需要再次导入;共享库是以页面为单位装载的,需要多少便加载多少.除了实现了更好地节约内存,更快的性能,另一点就是更新方便,不会牵一发而动全身,仅仅更新部分的库就可以,而不需要整个重加载.

为了解决共享库的地址重定位问题,在代码中使用相对指令的地址,这样就可以做到与具体的位置无关了.

内存映射文件

通过把文件当成内存的形式映射到页面,实现进程间的文件共享,是文件操作向内存操作一样简单易用.

清楚策略

为了保证有足够的页框可用,分页守护线程会定期运行,使用页面置换算法获取更多的页框,并进行把原页框的数据写会到磁盘(如果有必要的话).

分页守护线程还可以确保页框都是"干净"的,这样可以在下次直接写入.

虚拟内存接口

实现进程间的内存共享.

有关实现的问题

与分页有关的工作

与分页有关的工作,共有四个时期:进程创建时,进程执行时,缺页中断时,进程终止时.

进程创建: OS确定进程需要的空间大小,然后为它们创建一个页表并初始化并固定到内存中.若进程被换出,页表就可以清除了.还有,OS还要在硬盘中分配出交换空间,用来放置换出的程序.OS还要使用程序正文和数据对交换空间进程初始化,以便进行置换页面时方便使用.最后,OS把页表和交换空间的信息存储在进程表里.

进程执行: 当一个进程执行时,必须重置MMU,刷新TLB(所以看得出MMU TLB仅为当前进程服务,不会记录前一个进程的信息).新进程的页表必须称为当前页表.

缺页中断: OS读取硬件寄存器,找到是哪个虚拟地址导致了缺页中断,并计算出需要的页面,然后进行页面置换,把磁盘的数据复制到相应的页框中;最后,回退程序计数器,重新执行导致中断的指令.

进程退出: 当一个进程退出时,OS释放与它有关的页表,页面,和页面所占用的磁盘空间;当然,如果他们中的某些被共享了,就得等最后一个使用它们的进程退出才能释放.

缺页中断处理的细节

一个具体的缺页中断处理过程如下:

  1. 硬件陷入内核,保存程序计数器.大多数机器将当前指令的各种信息保存在特殊的CPU寄存器中.
  2. 启动一个汇编程序,保存通用寄存器和 其他易失信息.此程序将OS作为一个函数来调用.
  3. OS被启动,读取某个硬件寄存器,获得导致失败的虚拟地址,如果没有的话,检索程序计数器,分析计算出需要的地址.
  4. 此时OS获得了虚拟地址,会检查地址是否有效,存取与保护是否一致,如不一致则发出一个信号或杀死进程.然后检查是否有页框可用,如果没有就置换;
  5. 如果需要置换,那就看是否需要写回磁盘,如果需要就挂起此进程(因为进行IO操作比较耗时)运行另一个进程,同时把此页框标记为忙碌,禁止其他进程使用.
  6. 当从磁盘写入页框时,还要把这个进程挂起,运行其他进程(还是因为磁盘IO操作比较耗时).
  7. 更新页表,更新页框状态为正常
  8. 恢复到发生缺页中断指令以前的状态,程序计数器重新指向这条指令.
  9. 调度引发缺页中断的进程,OS返回汇编程序的调用.
  10. 汇编指令恢复寄存器等状态信息,返回到用户空间继续执行,仿佛什么都没发生过.

指令备份

用来处理虚拟地址计算问题,有些CPU不好推算出虚拟地址,于是使用第二个隐藏的寄存器保存指令.

锁定内存中的页面

确保正在进行IO操作的进程不会被移出内存.

后备存储

Unix系统从文件系统上划分出一块分区用于做交换分区,当系统启动时,该交换分区为空,并在内存中单独的记录着它的大小.与每个进程对应的是交换分区的磁盘地址,保存在进程表里.计算页面在磁盘中的地址很简单,虚拟地址的偏移量加上交换空间的起始地址.

对于进程的增长,可以采用正文与数据分离的形式,分别建立交换区.

对于分配方法,有两种,一种预先分配好,一种用时再分配.前者页面对应的磁盘位置直接可以获得,后者需要新的表记录每个页面对应的磁盘位置.

策略和机制的分离

旨在降低复杂度,让存储管理器作为用户及进程运行.

3f9e6d03ab2db69c99586621b67ea06c.png

分段

概念

用于解决程序增长问题,把空间划分成一段一段的,用来实现更精细化的增长空间分配和控制

实例研究

文件系统

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值