操作系统的重要功能内存管理
操作系统的重要功能——内存管理
文章目录
一. 概述
内存是计算机中一种需要认真管理的重要资源
正如帕金森定律所述:“不管存储器有多大,程序都可以把它填满”
因此,我们需要针对内存进行单独管理,比避免这种现象的发生
本文将主要介绍在操作系统中,如何对内存进行管理
二. 无存储器抽象
最简单的存储器抽象就是没有存储器抽象,每一个程序都直接访问物理内存
举个例子,当一个程序执行以下指令:
MOV REGISTER1 10000
此时计算机会将位置为10000的物理内存中的内容移动到REGISTER1中
1. 三种组织形式
采用了无存储器抽象后,内存管理的可能组织形式主要有:
用户程序 + 位于RAM中的操作系统
位于ROM中的操作系统 + 用户程序
位于ROM中的设备驱动程序 + 用户程序 + 位于RAM中的操作系统
可以看到,对于第一种和第三种,由于用户程序和操作系统都操作同一块内存,当用户程序发生错误时,可能会破坏操作系统
2. 存在的问题
- 因为程序可以寻址内存的每个字节,所以很容易破坏操作系统
- 使用这个模型,多个程序并行是很困难的。即难以在内存中同时运行两个或多个程序
3. 改进:试图运行多个程序
前面我们提到,多个程序并行是困难的,但不意味着无法并行,可以采用以下方法实现
方法:操作系统可以把当前内存中所有内容保存到磁盘文件中,然后把下一个程序读到内存中再运行即可。只要在某一个时刻内存中只有一个程序,那么就不会发生冲突
下面介绍IBM早期用于运行多个程序的一个解决方案
IBM360早期模型的解决方案
将内存划分为2kb的块,每个块分配一个4位的保护键(存储在CPU的特殊寄存器)。PSW(程序状态字)中存在一个4位码,一个运行中的进程如果访问保护键与其PSW码不同的内存,360硬件会捕获这一事件,从而防止进程修改保护键(只有操作系统才能修改),从而防止用户进程之间,用户进程与操作系统之间的互相干扰
不过,该模型也存在一些问题,例如:如果程序引用了绝对物理地址,可能导致错误
对此,解决方案是,每个程序都应该使用一套私有的本地地址来进行内存寻址。IBM 360的实现方案是静态重定位。当一个程序装载到某个地址时(比如16384),这个常数加到每一个程序地址上(16384)。这个机制要求装载器需要一定的方法来辨别地址和常数
三. 地址空间
1. 为什么引入地址空间
我们看到,在无存储器抽象下,在内存中多程序并行的困难
虽然可以实现,但是遇到的主要问题在于,进程之间容易互相影响,如何解决进程保护和地址重定向的问题,就能实现多进程并行
因此,我们需要一个新的抽象——地址空间,隔离不同进程
2. 定义
- 地址空间是一个进程可用于寻址内存的一套地址集合
- 每个进程都有自己的地址空间,这个地址空间独立于其他进程的地址空间
- 广义的地址空间其实非常通用,比如本地电话号码(0000000-9999999),以"com"结尾网络域名的集合,I/O端口的地址空间(0-16383),IPv4的地址(0-2的32次方-1)
3. 简单实现:基址寄存器和界限寄存器
下面讨论一种简单实现进程拥有自己地址空间的方法
<1> 定义
- 通过使用基址寄存器和界限寄存器,实现动态重定位。最终把每个进程的地址空间映射到物理内存的不同部分
- 每个CPU都配置两个特殊的硬件寄存器:基址寄存器和界限寄存器
- 基址寄存器:存放程序的起始物理地址
- 界限寄存器:存放程序的长度
<2> 具体实现
当进程访问内存时,CPU硬件会在把地址发送到内存总线前,自动把基址值加到进程发出的地址值上。同时也会检查程序提供的地址是否等于或大于界限寄存器的值,如果超过了界限则会产生错误并中止访问
如上图所示,在访问指令JMP 24时,CPU硬件会将基址寄存器的值4加到地址24上得到地址28,并判断28是否等于或大于界限寄存器的值40,发现小于40,则跳到地址28,此时成功读取到CMP指令
<3> 缺点
每次访问内存都需要进行加法和比较运算。虽然比较运算可以做得很快,但加法运算由于进位传递时间的问题,在没有使用特殊电路的情况下会显得很慢
4. 内存超载
<1> 问题描述
如果计算机物理内存足够大,就可以保存所有进程。但实际上进程所需的RAM数量总和通常远远超出存储器能够支持的范围,如果内存不够,就做不到这一点,也就会发生内存超载
<2> 如何解决
解决方案有两种,交换和虚拟内存
- 交换:内存中完整调入一个进程,运行后再调出
- 虚拟内存:使得进程只有一部分被调入内存的情况下运行
关于这两种方案,将在下面详细介绍
5. 空闲内存管理
在动态分配内存时,操作系统必须进行管理
可以通过以下两种方法实现动态分配内存:
- 位图法
- 空闲区链表法
<1> 位图法
定义
- 内存被分为多个分配单元,每个分配单元小到几个字,大到几千字节
- 每个分配单元对应于位图中的一位,0表示空闲,1表示占用
下面是内存图与位图的对应关系
可以看到,上图是有3个进程和2段空闲区的内存,刻度表示内存分配单元,通过位图法记录了内存的分配状态
位图的分配单元
- 分配单元的大小是一个重要的设计因素,分配单元越小,位图越大
- 如果进程大小不是分配单元的整数倍,则最后一个分配单元中会有一定数量的内存被浪费
位图法的缺点
当把一个占k个分配单元的进程调入内存时,存储管理器必须搜索位图,在位图中查找有k个连续0的串,查找位图中指定长度的连续0串是耗时的操作
<2> 空闲区链表法
定义
维护一个记录已分配内存和空闲内存段的链表。其中链表中的一个结点或者包含一个进程,或者是两个进程中的一块空闲区。链表中的每一个结点都包含以下域:空闲区(H)和进程(P)的指示标志,起始地址,长度和指向下一结点的指针,下图是内存图与链表的对应关系
改进1:使用双向链表
可以使用双向链表。使用双向链表比单向链表更方便,这样的结构更容易找到上一个结点,并检查是否可以合并
内存分配算法
首次适配算法
存储管理器沿着段链表进行搜索,直到找到一个足够大的空闲区。如果空闲区大小和待分配的空间大小一样,则直接分配;否则将该空闲区分为两部分,一部分供进程使用,另一部分形成新的空闲区
速度很快的算法,尽可能少地搜索链表结点
下次适配算法
工作方式和首次适配算法相同,不同点是每次找到合适的空闲区都记录当时的位置,以便下次寻找时从上次结束的地方开始搜索
下次适配算法的性能略低于首次适配算法
最佳适配算法
搜索整个链表,找到能容纳进程的最小的空闲区
比首次适配算法慢(每次都搜索整个链表),浪费更多内存(产生大量无用的小空闲区)
最差适配算法
总是分配最大的可用空闲区,使新的空闲区比较大而可以继续使用
避免分裂出非常小的空闲区
改进2:独立链表维护
为进程和空闲区维护各自独立的链表
使用后,前面四个算法的速度都能提高,但增加了复杂度,内存释放速度也会变慢
可优化点
- 可以按照大小对空闲区链表进行排序,以提高最佳适配算法的速度
- 数据结构可以优化,只需要两个字,第一个字是空闲区大小,第二个字指向下一个空闲区
- 快速适配算法:为那些常用大小的空闲区维护单独的链表。例如有一个n项的表,该表的第一项指向大小为4KB的空闲区链表表头的指针,第二项指向大小为8KB的空闲区链表表头的指针,以此类推。使用该算法寻找一个指定大小的空闲区是十分快速的,但缺点是当一个进程被终止或换出时,寻找它的相邻快是否可以合并的过程非常费时。如果不合并则会产生大量无用的小空闲区
四. 交换
1. 定义
交换,通过把一个进程完整调入内存,使进程运行一段时间,然后把它存回磁盘。空闲进程主要存储在磁盘上,当它不运行时就不会占用内存
2. 实现
如下图所示,最开始内存有进程A,B,C,然后进程B被交换到磁盘,然后进程D被调入,然后C被调出,最后B再被调入。由于B的位置发生变化,因此它在换入时需要对其进行重定向。上述所讲的基址寄存器和界限寄存器就适用这种情况
3. 内存紧缩
从上图可以看到,交换在内存中产生了多个空闲区(也称为空洞),如果不处理会造成内存浪费
可以通过把所有的进程尽可能往下移动,从而把这些小的空闲区合成一大块,以提高内存利用率,这种方式称为内存紧缩
不过,这个操作会耗费大量的CPU时间,通常不进行这个操作
4. 交换遇到的问题及解决
- 进程创建时应分配的内存
- 如果进程创建时,其大小是固定不变的,则根据需要的大小进行分配
- 如果进程的数据段可以增长,则应该多为进程分配一些额外的内存
- 进程用完内存怎么办
如果用完了,可以把进程移动到足够大的空闲区中,或者结束该进程
五. 虚拟内存
1. 为什么会有虚拟内存
尽管前面提到的交换技术可以解决内存中运行多进程的问题,但是还有问题没有得到解决:即进程空间的膨胀
随着软件大小的增长,需要运行的程序往往大到内存无法容纳,这意味着即使内存只允许一个进程,它也会装不下那么大的内容。此时再提交换技术,也没有什么意义了
因此,需要有一种技术来解决这个问题。于是覆盖技术诞生了。它通过把程序分割成许多片段来解决问题。程序开始执行时,把覆盖管理程序装入内存,该管理模块立即装入并运行覆盖0。执行完成后,覆盖0通知管理模块装入覆盖1。另外,有些覆盖系统允许多个覆盖块同时运行在内存中
不过,这种方式虽然可以分段加载程序,但是却要求程序员将程序分成多个片段,因此需要有一种新的方法来让这种工作交给计算机去做,也就是下文介绍的虚拟内存
2. 定义
每个程序拥有自己的地址空间,这个空间被分割成很多块,每一块都称为页或者页面。每一页都有连续的地址范围。这些页被映射到物理内存,但并不是所有的页都必须在内存中才能运行程序。当程序引用到一部分在物理内存中的地址空间时,由硬件立即执行必要的映射。当程序引用到一部分不在物理内存中的地址空间时,由操作系统负责将缺失的部分装入物理内存并重新执行失败的指令
3. 特点
- 虚拟内存是对基址寄存器和界限寄存器的一种综合
- 使得整个地址空间可以用相对较小的单元映射到物理内存,而不是为正文段和数据段分别进行重定位
- 适合多道程序设计系统。很多程序的片段同时保存在内存中,当一个程序等待它的一部分读入内存时,可以把CPU交给另一个进程使用
4. 分页
<1> 定义
由程序产生的地址称为虚拟地址,它们构成了一个虚拟地址空间。在没有虚拟内存的计算机上,系统直接将虚拟地址送到内存总线上。读写操作使用具有同样地址的物理内存字。在使用虚拟地址的情况下,虚拟地址不是送到内存总线上,而是被送到内存管理单元MMU,MMU把虚拟内存地址映射为物理内存地址
虚拟地址空间按照固定大小被划分成被称为“页面”的若干单元,在物理内存中对应的单元称为“页框”。页面和页框通常是一样大的
RAM和磁盘之间的交换总是以整个页面为单位进行的,处理器支持对不同大小页面的混合使用和匹配
如果该页面没有被映射,则会使CPU陷入操作系统,这个陷阱称为缺页中断或缺页错误。此时操作系统找到一个很少使用的页框且把它的内容写入磁盘(如果它不在磁盘上),随后把需要访问的页面读到刚才回收的页框中,修改映射关系,然后重新启动引起陷阱的指令
<2> MMU的内部结构
下面是虚拟地址8196被映射到物理地址24580的过程:
从上图可以看出:
- 输入的虚拟地址被分为4位的页号和12位的偏移量
- 页表的索引是页号,用于得出该虚拟页面对应的页框号
- 如果在/不在的位是0,则引起一个操作系统陷阱,如果是1,则将查到的页框号复制到输出寄存器的高3位,加上输入虚拟地址的低12位偏移量,构成15位的物理地址
- 输出寄存器的内存地址被作为物理地址送到内存总线
5. 页表
<1> 定义
页表的目的是把虚拟页面映射到页框。页表是一个函数,它的参数是虚拟页号,结果是物理页框。通过这个函数可以把虚拟地址中的虚拟页面替换成页框域,从而形成物理地址
<2> 页表项的结构
一个页表包含多个页表项,一个页表项的结构如下图所示:
其中主要包括页框号,在/不在位,保护位,修改位,访问位,高速缓存禁止位等
- 页框号:最重要的域。页映射的目的是找到这个值
- 在/不在位:该位是1时表示该表项是有效的,可以使用;如果是0,表示该表项对应的虚拟页面现在不在内存中,访问该页面会引入一个缺页中断
- 保护位:指出一个页允许什么类型的访问。最简单的形式只有1位,0表示读/写,1表示只读。另一种是使用三位,分别对应是否启用读,写,执行该页面
- 修改位(脏位):在写入一页时由硬件自动设置。在操作系统重新分配页框时,如果一个页面已经被修改过,则必须把它写回磁盘;如果没有修改过,则简单地丢弃它就可以了
- 访问位:无论是读或者写,系统都会在该页面被访问时设置访问位。它的值用来帮助操作系统在发生缺页中断时选择被淘汰的页面。不再使用的页面比正在使用的页面更适合淘汰
- 高速缓存禁止位:对那些映射到设备寄存器而不是常规内存的页面非常重要。通过禁用高速缓存,可以保证硬件不断从设备中读取数据而不是访问一个旧的被高速缓存的副本
<3> 加速分页过程
分页系统需要考虑的两个主要问题
- 虚拟地址到物理地址的映射必须非常快
由于每次访问内存都需要进行虚拟地址到物理地址的映射,因此每条指令进行一两次或更多次页表访问是必要的,执行一条指令需要1ns,页表查询必须在0.2ns之内,以避免映射成为主要瓶颈
- 如果虚拟地址空间大,页表也会很大
现代计算机使用至少32位的虚拟地址,且64位非常普遍。假设页面大小为4kb,32位的地址空间将有100万页,此时页表也会有100万页,而64位地址空间则远远不止。另外每个进程都需要自己的页表(因为进程有自己的虚拟地址空间)
解决方案1:转换检测缓冲区
该方案用于解决问题1:虚拟地址到物理地址的映射慢
- 理论来源:大多数程序总是对少量的页面进行多次的访问
- 做法:设置一个小型的硬件设备,将虚拟地址直接映射成物理地址,而不需要再访问页表,这种设备叫做转换检测缓存区,又称相联存储器或者快表
- 它通常在MMU中,包含少量的表项。每个表项记录了一个页面的相关信息,这些域与页表中的域是一一对应的。另外还有一位记录这个表项是否有效,表的内容如下所示:
有效位 | 虚拟页号位 | 修改位 | 保护位 | 页框位 |
---|---|---|---|---|
1 | 23 | 1 | R X | 49 |
1 | 53 | 0 | RW | 30 |
1 | 213 | 1 | RW | 15 |
1 | 123 | 0 | R X | 98 |
1 | 13 | 1 | RW | 64 |
- 如何工作
- 硬件首先通过将该虚拟页号与TLB中所有表项同时进行匹配,判断虚拟页面是否在其中,如果匹配存在且该操作不违反保护位,则将页框号直接从TLB中取出而不再访问页表
- 如果该虚拟页号确实存在TLB中,但是试图在只读页面上做写操作,则会产生保护错误
- 如果该虚拟页号不存在TLB中,就会进行正常的页表查询。接着从TLB中淘汰一个表项,然后用新找到的页表项代替它
- 当一个表项被清除出TLB时,将修改位复制到内存中的页表项,而除了访问位,其他的值不变
- 可以采用软件TLB管理改进。对TLB的管理和失效处理都由软件实现,称为软件TLB管理
解决方案2:针对大内存的页表
该方案用于解决问题2:页表空间过大
多级页表
页表分成多级结构,避免把全部页表一直保存在内存中,不需要的页表不保留
页表的级数越多,灵活性越大
倒排页表
针对页表调度层级不断增长的另一种解决方案是倒排页表
在这种设计中,实际内存中的每个页框对应一个表项,而不是每个虚拟页面对应一个表项
采用了倒排页表后,节省了大量的空间。但是从虚拟地址到物理地址的转换会变得困难
解决方案是,使用TLB,TLB记录所有频繁使用的页面。当发生失效时,避免用软件搜索整个倒排页表,通过建立一张散列表解决。用虚拟地址来散列时,当前所有内存中具有相同散列值的虚拟页面被链接在一起
扩展概念
下面为一些常用概念,用于补充知识
软失效:当一个页面访问在内存而不是在TLB中。此时要做的就是更新一下TLB,需要10-20个机器指令,需要几纳秒
硬失效:当页面本身不在内存中(当然也不是TLB中),此时需要一次磁盘存取以装入页面,需要几毫秒
页表遍历:在页表结构中查找相应的映射称为页表遍历
未命中引发的错误类型
次要缺页错误:所需要的页面在内存中,但未在该进程的页表中
严重缺页错误:需要从磁盘重新调入页面
段错误(程序错误):程序访问了一个非法地址,根本不需要向TLB中新增映射
六. 页面置换算法
当发生缺页中断时,操作系统必须在内存中选择一个页面将其换出内存,以便为即将调入的页面腾出空间,也即页面置换算法
1. 最优页面置换算法
<1> 算法内容
在缺页中断发生时,有些页面在内存中,其中有一个页面将很快被访问,其他页面则可能要到10,100或1000条指令后才会被访问,每个页面都可以用在该页面首次被访问要所要执行的指令数作为标记。该算法规定应该置换标记最大的页面
<2> 存在的问题
它是无法实现的。当缺页中断发生时,操作系统无法知道各个页面下一次将在什么时候被访问
2. 最近未使用(NRU)页面置换算法
<1> 算法内容
为使操作系统能够收集有用的统计信息,系统为每个页面设置了两个状态位。当页面被访问时设置R位,被写入时设置M位,这些位包含在每个页表项中
当启动一个进程时,它所有页面的两个位都由操作系统设置成0,R位被定期清零,以区别最近没有被访问的页面和被访问的页面
当发生缺页中断时,操作系统检查所有的页面并根据它们当前R位和M位的值,分为4类:
- 第0类:没有被访问,没有被修改
- 第1类:没有访问,已被修改
- 第2类:已被访问,没有被修改
- 第3类:已被访问,已被修改
3. NRU算法
<1> 算法内容
NRU算法随机从类编号最小的非空类中挑选一个页面淘汰。在最近一个时钟滴答中淘汰一个没有被访问的已修改页面比淘汰一个被频繁使用的干净页面好
<2> 优点
易于理解,能够有效地被实现
<3> 缺点
性能不是最好的
4. 先进先出(FIFO)页面置换算法
<1> 算法内容
操作系统维护一个所有当前在内存中的页面的链表,最新进入的页面放在表尾,最早进入的页面放在表头。当发生缺页中断时,淘汰表头的页面并把新调入的页面加入到表尾
<2> 缺点
有可能会置换被经常使用的页面
5. 第二次机会页面置换算法
<1> 算法内容
改进了FIFO算法的缺点
检查最老页面的R位,如果R位是0,那么这个页面既老又没有被使用,可以立即置换掉;如果是1,就将R位清0,并把该页面放到链表的尾端,修改它的装入时间就像它刚装入的一样,然后继续搜索
第二次机会算法就是寻找一个在最近的时钟间隔内没有被访问过的页面,如果所有页面都被访问过了,该算法就简化为纯粹的FIFO算法
<2> 缺点
需要在链表中移动页面,效率低下
6. 时钟页面置换算法
<1> 算法内容
优化了第二次机会算法,把所有的页面都保存在一个类似钟面的环形链表中,一个表针指向最老的页面
当发生缺页中断时,首先检查表针指向的页面,如果它的R位是0就淘汰该页面,并把新的页面插入这个位置,然后把表针向前移动一个位置,如果R位是1就清除R位并把表针向前移动一个位置,重复这个过程直到找到了一个R位为0的页面为止
7. 最近最少使用(LRU)页面置换算法
<1> 算法内容
LRU算法:在缺页中断发生时,置换未使用时间最长的页面
LRU在理论上可以实现,但代价高
实现LRU方式:需要在内存中维护一个所有页面的链表,最近最多使用的页面在表头,最近最少使用的页面在表尾。每次访问内存时,更新整个链表
硬件方式实现LRU:硬件有一个64位的计数器C,它在每条指令执行完成后自动加1,每个页表项有一个足够容纳这个计数器值的域。每次访问内存后将当前的C值保存到被访问页面的页表项中,当发生缺页中断时,找到值最小的一个页面,这个页面就是最近最少使用的页面
软件模拟LRU(称为NFU算法,最不常用算法)
算法的主要内容:该算法将每个页面与一个软件计数器相关联。计数器的初始值为0。每次时钟中断时,操作系统扫描内存中所有的页面,将每个页面的R位加到它的计数器上。这个计数器大体跟踪了各个页面被访问的频繁程度。发生缺页中断时,置换计数器值最小的页面
<2> 主要问题
从不忘记任何事情。第一次扫描中被频繁使用的页面进入第二次扫描时,其计数器的值仍然很高。可能导致操作系统置换有用的页面
8. 老化算法
<1> 算法内容
在R位被加进之前将计数器右移一位,其次将R位加到计数器最左端的位
如下图所示,最开始计数器都是000000,时钟滴答0后,页面0的R位为1,此时页面0的值为100000;时钟滴答1后,页面0的R位为1,此时页面0的值为110000,其他页面以此类推
<2> 该算法与LRU的区别
- LRU置换页面按照最近最少使用,而老化算法有时候无法区分
- 老化算法的计数器只有有限位数,限制了其对以往页面的记录
在实践中,如果滴答是20ms,8位一般是够用的。一个页面160ms没被访问,那么它并不重要
9. 工作集页面置换算法
<1> 定义
请求调页:页面在需要时被调入,而不是预先装入
预先调页:在进程运行前预先装入其工作集页面
局部性访问行为:在进程运行的任何阶段,都只访问较少的一部分页面
工作集:一个进程当前正在使用的页面的集合称为它的工作集
工作集模型:不少分页系统会跟踪进程的工作集。确保进程在运行以前,工作集就存在内存中。这种方法称为工作集模型。目的在于减少缺页中断率
颠簸:执行几条指令程序就发生一次缺页中断
<2> 算法内容
假定使用硬件来置R位和M位,假定在每个时钟滴答中,在一个定期的时钟中断会用软件方法来清除R位,每当缺页中断发生时,扫描页表以找到一个合适的页面淘汰
处理页表时,检查R位,如果它是1,就把当前实际时间写进表项的“上次使用时间”域,以表示缺页中断时发生时该页面正在被使用,此时不应该被删除
如果R是0,则表示在当前时钟滴答中该页面没有被使用。此时作为候选者,计算它的生存时间,然后与r作比较。如果生存时间大于r(r是当前实际时间),则这个页面就会替换
如果R是0,同时生存时间小于或等于r,则该页面仍然在工作集中,此时需要临时保留页面,但是记录生存时间最长的页面。如果扫描完整个页表则没有找到被淘汰的页面,则意味着所有的页面都有工作集中,就淘汰生存时间最长的页面
最后,最坏情况下所有页面都被访问了(所有的页面R=1),则随机选一个页面淘汰,最好选择一个干净页面
<3> 缺点
缺页中断时需要扫描整个页表才能淘汰页面,费时
10. 工作集时钟页面置换算法
<1> 特点
改进了工作集算法,基于时钟算法,实现简单,性能好
<2> 算法内容
采用以页框为元素的循环表,页面加入并形成环。每个表项包含来自基本工作集算法的上次使用时间,R位以及M位
每次缺页中断时,首先检查指针指向的页面,如果R位被置为1,则说明该页面被使用过,则该页面不适合淘汰,此时把该页面R位置为0,指针指向下一个位置
指针指向R=0时,如果页面的生存时间大于t并且该页面是干净的,则它不在工作集中,且在磁盘上有一个有效的副本,申请此页框,并把新页面放在其中。
如果该页面被修改过,则不能申请页框(此时需要写操作,会引起进程切换),指针继续向前走
指针经过一圈后,返回它的起始点,这里有两种情况:
至少调度了一次写操作
此时已经调度了一个或多个写操作,最终会有某个写操作完成,页面会被标记为干净,置换遇到的第一个干净页面
没有调度过写操作
此时所有的页面都工作集中,可以随机置换一个干净的页面来使用。扫描中需要记录干净页面的位置,如果不存在干净页面,就选定当前页面并写回磁盘
11. 算法小结
算法 | 结论 |
---|---|
最优算法 | 不可实现,但可用作基准 |
NRU(最近未使用)算法 | LRU的很粗糙的近似 |
FIFO(先进先出)算法 | 可能抛弃重要页面 |
第二次机会算法 | 比FIFO有较大的改善 |
时钟算法 | 现实的 |
LRU(最近最少使用)算法 | 很优秀,但很难实现 |
NFU(最不经常使用)算法 | LRU的相对粗糙的近似 |
老化算法 | 非常近似LRU的有效算法 |
工作集算法 | 实现起来开销很大 |
工作集时钟算法 | 好的有效算法 |
七. 设计分页系统
本节主要讲解如何设计一个合理的分页系统,解决设计过程中遇到的一些问题
1. 局部分配策略和全局分配策略
<1> 定义
- 局部分配策略:有效地为每个进程分配固定的内存片段
- 全局分配策略:在可运行进程之间动态分配页框
下面以一个例子说明,三个进程A,B,C,现在需要置换一个页面
按照局部分配策略,则置换页面A2
按照全局分配策略,则置换页面B1
<2> 两种策略的对比
全局算法在通常情况下工作比局部算法好。使用局部算法,随着工作集的增长会导致颠簸,而工作集缩小又会浪费内存
<3> 页面置换算法的页面分配策略
适合全局和局部分配策略的算法:FIFO,LRU
只适合局部分配策略的算法:工作集,工作集时钟算法
2. 负载控制
<1> 问题
即使是使用最优页面置换算法并对进程采用理想的全局页框分配,系统也可能发生颠簸。一旦所有进程的组合工作集超出了内存容量,就可能发生颠簸
<2> 解决方案
从内存中临时去掉一些进程。可以将一部分进程交换到磁盘,并释放他们所占有的页面,他们所释放的页框就可以被处于巅峰的进程共享
将进程交换出去以减轻内存需求的压力借用了两级调度的思想,将恰好足够的进程交换出去以获取可以接受的缺页中断率。一些进程被周期性地从磁盘调入,一些进程被周期性地交换到磁盘
最后,在决定哪个进程交换出去时,还要考虑它的特性(CPU密集还是IO密集)。当内存中进程数过低时,CPU可能在很长时间处于空闲状态
3. 页面大小
<1> 问题
页面大小是操作系统可以选择的一个参数。如何确定最佳的页面大小
<2> 思考
- 选择大页面
- 浪费内存空间,造成内存碎片
- 程序页表小,减少装入程序的时间
- 选择小页面
- 产生较小的内部碎片,提高内存的利用率
- 意味程序需要更大的页表,装入同样大小的程序需要更多的时间(增大寻址查找速度和额外开销)
<3> 解决方案
假设进程平均大小是s个字节,页面大小是p个字节,每个页表项需要e个字节。则每个进程需要的页数大约是s/p,占用了se/p字节的页表空间,内存碎片在最后一页浪费的内存是p/2,因此页表和内存碎片的开销为:开销=se/p+p/2
可以看出,当页面小,第一项大(页表大小),当页面大,第二项大(内存碎片)。因此,最优值为页面大小的某个中间值,对式子求导,得:
p=根号2se,当s=1mb,e=8b,最优页面大小为4kb
4. 分离的指令空间和数据空间
<1> 问题
大多数计算机只有一个地址空间,既存放程序也存放数据,当地址空间过小时,使得对地址空间的使用出现困难
<2> 解决方案
为指令(程序正文)和数据设置分离的地址空间,分别称为I空间和D空间
两种地址空间都可以进行分页,相互独立,拥有自己的页表
拥有分离的I空间和D空间不会引入任何复杂的设计,还会使地址空间加倍
5. 共享页面
<1> 问题
在大型多道程序设计系统中,不同用户同时运行同一个程序是很常见的
如果能实现共享页面,可以避免内存中有一个页面的两个副本,效率更高
<2> 解决方案
如果系统支持分离的I空间和D空间,那么可以让进程使用相同的I空间和不同的D空间页表。在这种方式中,每个进程在它的进程表中都有两个指针:一个指向I空间页表,一个指向D空间页表
<3> 上述方案存在的问题及改进
问题:如果两个进程共享页面,当调度程序从内存中移走A,会调走共享页面,从而引起B大量的缺页中断
改进:能够发现这些页面仍然在使用是非常必要的。可以通过一个专门的数据结构记录共享页面
6. 共享数据与写时复制
fork系统调用会导致父进程和子进程共享程序文本和数据。在分页系统中,通过是让进程分别拥有它们自己的页表,但都指向同一个页面集合。所有映射到两个进程的数据页面都是只读的。当进程更新数据时,会触发只读保护,引起操作系统陷阱,然后生成该页的副本,这样子每个进程都有自己的专用副本
<1> 写法复制
从来不会执行写操作的页面不需要复制,只有实际修改的数据页面才需要复制,这种方法称为写法复制。它通过减少复制而提高了性能
7. 共享库
<1> 问题
在现代操作系统中,有很多大型库被众多进程使用。如果处理浏览文件以便打开文件的对话框的库和多个图形库。把所有库静态地与磁盘上的每个可执行程序绑定在一起,会变得很庞大
<2> 解决方案
- 使用共享库。当一个程序和共享库链接时,链接器没有加载被调用的函数,而是加载了一小段能够在运行时绑定被调用函数的存根例程
- 依赖于系统和配置信息,共享库或者和程序一起被装载,或者在其所包含函数第一次被调用时被装载
- 如果其他程序已经装载了某个共享库,就不会再次装载
- 最后,当一个共享库被装载和使用时,不是整个库一次性读入,而是根据需要,以页面为单位装载,没有被调用的函数不会被装载到内存中
<3> 优点
- 节省内存空间
- 不需要重新编译调用了这个函数的程序
<4> 共享库遇到的问题及解决
问题:两个程序使用同一个共享库时,在程序重定位时可能会有错误
解决:使用相对地址,而不是绝对地址。尽量编写位置无关代码
8. 内存映射文件
- 内存映射文件的思想:进程可以通过发起一个系统调用,将一个文件映射到其虚拟地址空间的一部分。在多数实现中,在映射共享的页面时不会实际读入页面的内容,而是在访问页面时才会被每次一页地读入,磁盘文件则被当作后备存储。当进程退出或显式地解除文件映射时,所有被改动的页面会被写回到磁盘文件
- 内存映射文件提供了一种I/O的可选模型,可以把一个文件当作一个内存中的大字符数组来访问,而不用通过读写操作来访问这个文件
- 如果两个或两个以上的进程同时映射了同一个文件,它们可以通过共享内存来通信。一个进程在共享内存上完成了写操作,此时当一个进程在映射到这个文件的虚拟地址空间上执行读操作时,它可以立即看到上一个进程写操作的结果
9. 清除策略
<1> 问题
如何保证在发送缺页中断时,系统中有大量的空闲页框,因为此时分页系统可以工作在最佳状态
<2> 思考
分页系统可以新增一个分页守护进程的后台进程,它在大多数时候睡眠,但定期被唤醒以检查内存的状态。如果空闲页框过少,分页守护进程通过预定的页面置换算法选择页面换出内存。如果这些页面装入内存后被修改过,则将它们写回磁盘
<3> 解决方案
使用一个双指针时钟。前指针由分页守护进程控制。当它指向一个脏页面时,就把该页面写回磁盘,前指针向前移动。当它指向一个干净页面时,仅仅指针向前移动。后指针用于页面置换,就像在标准时钟算法中一样
10. 虚拟内存接口
<1> 问题
对于一些高级系统而言,希望程序员可以对内存映射进行控制,并可以通过非常规的方法来增强程序的行为
<2> 解决方案
通过提供虚拟内存接口,允许程序员对内存映射进行控制
<3> 好处
- 允许两个或多个进程共享一部分内存
- 用来实现高性能的消息传递系统
八. 实现分页系统
本节主要介绍实现分页系统的相关细节内容
1. 与分页有关的工作
操作要在以下的四段时间里做与分页相关的工作:
<1> 进程创建时
- 当在分页系统中创建一个新进程时,操作系统要确定程序和数据在初始时有多大,并为它们创建一个页表,并在内存中为页表分配空间和初始化
- 当进程被换出时,页表不需要驻留在内存中。当进程在运行时,它必须在内存中
- 操作系统在磁盘交换区中分配空间,以便在一个进程换出时在磁盘上有放置此进程的空间
- 初始化程序正文和数据交换区,当新进程发生缺页中断时,可以调入需要的页面
- 系统直接从磁盘上的可执行文件对程序正文进行分页,以节省磁盘空间和初始化空间
- 存储页表和磁盘交换区的信息在进程表中
<2> 进程执行时
- 当调度一个进程执行时,必须为新进程重置MMU,刷新TLB,以清除以前进程遗留的痕迹
- 在进程初始化时可以把进程的部分或者全部页面装入内存中以减少缺页中断的发生,例如PC(程序计数器)所指的页面
<3> 缺页中断时
当缺页中断发生时,必须通过读硬件寄存器来确定哪个虚拟地址造成了缺页中断。通过定位该页面,然后找到合适的页框存放新页面。必要的时候使用置换算法置换旧页面。最后,还需要回退程序计数器,使程序计数器指向引起缺页中断的指令,并重新执行该指令
<4> 进程终止时
当进程退出的时候,释放进程的页表,页面和页面在磁盘上所占的空间。如果某些页面与其他进程共享,当最后一个使用它们的进程终止时,才释放内存和磁盘上的页面
2. 缺页中断
- 硬件陷入内核,在堆栈中保存程序计数器,大多数机器将当前指令的各种状态信息保存在特殊的CPU寄存器中
- 启动汇编代码例程保存通用寄存器和其他易失的信息,以免被操作系统破坏
- 当操作系统发现缺页中断时,尝试发现需要哪个虚拟页面,通常一个硬件寄存器包含这一信息,如果没有的话,操作系统则检索程序计数器,取出指令,分析它在缺页中断时正在做什么
- 获取到发生缺页中断的地址,检查地址是否有效,检查存取和保护是否一致。如果不一致,则发出信号或杀掉进程,如果地址有效且没有保护错误发生,检查系统是否有空闲页框,没有,则执行置换算法淘汰一个页面
- 如果选择的页框脏了,则安排该页写回磁盘,并发生一次上下文切换。挂起产生缺页中断的进程,让其他进程运行直到磁盘传输结束。该页框被标记为忙以免其他原因被其他进程占用
- 页框干净后,操作系统查找所需页面在磁盘上的地址,然后通过磁盘操作将其装入。该页面在装入时,产生缺页中断的进程仍然被挂起,并且如果有其他可运行的用户进程,则选择另一个进程运行
- 当磁盘中断发生时,表明该页已经装入,页表已经更新可以反映它的位置,页框也标记为正常状态
- 恢复发生缺页中断以前的状态,程序计数器重新指向这条指令
- 调度引起缺页中断的进程,操作系统返回调用它的汇编语言例程
- 该例程恢复寄存器和其他状态信息,返回到用户空间继续执行,就好像缺页中断没有发生过一样
3. 指令备份
<1> 问题
当程序访问不在内存中的页面时,引起缺页中断的指令会半途停止并引发操作系统的陷阱。在操作系统取出所需的页面后,它需要重新启动引起陷阱的指令。但有点困难
<2> 解决方案
通过使用一个隐藏的内部寄存器。在每条指令执行之前,把程序计数器的内容复制到该寄存器。通过这些信息,操作系统可以消除引起缺页中断的指令所造成的所有影响,并使指令可以重新开始执行
4. 锁住内存中的页面
<1> 问题
一个进程刚刚通过系统调用从文件或者其他设备读取数据到其地址空间的缓冲区,在等待IO完成时,线程被挂起,另一个进程被允许允许且产生一个缺页中断。此时包含IO缓冲区的页面会有很小的机会被选中换出内存
<2> 解决方案
锁住某一些内存中的页面(IO缓冲区的页面)以保证它不会被移除内存。锁住一个页面通常称为在内存中钉住页面。另一种方式是在内核缓冲区中完成所有的IO操作,然后再把数据复制到用户页面
5. 后备存储
<1> 背景
后备存储主要解决当页面被换出时会存放在磁盘上的哪个位置
<2> 做法
- 静态交换区分页(a)
当系统启动时,交换分区为空。每当一个进程启动时,留出与这个进程一样大小的交换区块,剩余的为总空间减去这个交换分区。进程结束后,其交换区被释放。它的特点是:
- 每个页面有固定的位置
- 存在进程启动后会增大导致交换区大小不够的问题
- 动态备份页面(b)
最开始不分配,在页面换出时为其分配磁盘空间,并在换入时收回磁盘空间。内存中的进程不会固定于任何交换空间。缺点是内存中每个页面都要记录相应的磁盘地址。它的特点是:
- 每个页面没有固定的位置
- 每个进程都必须有一张表,记录每个页面在磁盘上的位置
6. 策略和机制的分离
控制系统复杂度的一种重要方法就是把策略从机制中分离出来。通过使大多数存储管理器作为用户级进程运行,就可以把该原则应用到存储管理中,存储管理系统被分为三个部分:
- 一个底层MMU处理程序
- 一个作为内核一部分的缺页中断处理程序
- 一个运行在用户空间中的外部页面调度程序
<1> 页面置换算法的位置
有以下两种方式:
- 放在外部页面调度程序。这种方式比较简单。但是存在一些问题,比如外部页面调度程序无权访问所有页面的R位和M位
- 放到内核中。这种情况下,缺页中断处理程序会告诉外部页面调度程序它所选择要淘汰的页面并提供数据
最后,这两种方式的共同点是,外部页面调度程序都需要把数据写到磁盘上
<2> 好处
模块化代码,更好的适应性,更高的可靠性
<3> 坏处
多次交叉“用户-内核”引起的额外开销,牺牲一些性能
九. 分段
1. 分页系统存在的问题及解决
问题:在上面提到的分页系统中,所有内容都存在同一个内存中,它是一个一维地址空间。当内存有多个动态增加的内容时,不同部分的内容可能会发生碰撞
解决:可以使用分段管理。使用分段管理后,每一个段都可以独立地增大或缩小,而不会影响其他的段
2. 分段的实现方式
<1> 纯分段
- 分段和分页的实现本质上不同。页面是定长的,而段不是(a)
- 在系统运行一段时间后内存被划分为很多块,一些块包含着段,一些成为空闲区,这种现象成为棋盘形碎片或外部碎片(d)
- 空闲区的存在浪费了内存,可以通过内存紧缩解决(e)
<2> 分段和分页结合:MULTICS
- MULTICS是一个实现了这种支持的系统,始于麻省理工学院的一个研究项目。几乎没有其他的操作系统能像MULTICS一样几乎没有修改地持续运行。MULTICS系统最具创新性的一面:虚拟存储架构
- 它为每个程序提供了最多2的18次方个段,每个段的虚拟地址空间最长为65535个字长,它具有分页的优点(统一的页面大小和在只使用一部分时不用把它全部调入内存),也具有分段的优点(易于编程,模块化,保护和恭喜)
- 实现:每个MULTICS程序都有一个段表,每个段对应一个描述符。描述符包含一个段是否在内存中的标志,还有一个18位的指向它的页表的指针。还有段大小,保护位以及其他的一些条目
- MULCS中一个地址由两部分构成:段和段内地址。段内地址又分为页号和页内的字
- 在进行内存访问时,执行下列算法:
<1> 根据段号找到段描述符
<2> 检查该段的页表是否在内存中。是就找到,不是就产生段错误。如果访问违反了段的保护,就发出一个越界错误
<3> 检查所请求虚拟页面的页表项。如果该页面不在内存中,则产生一个缺页中断。如果在内存中就从内存中取出在内存中的起始地址
<4> 把偏移量加到页面的起始地址上,得到要访问的字在内存中的地址
<5> 最后进行读或写操作
问题与优化
如果每条指令都运行上面的算法,那么程序运行会很慢
优化方案是,提供一个包含16个字的高速TLB,对给定的关键字它能并行搜索所有的表项。当地址送到计算机时,先检查是不是在TLB中,是就直接取
<3> 分段和分页结合:Intel x86
- x86系统实现了纯分页,纯分段和段页式管理,同时还要与286兼容,最终设计的系统非常简洁
- 它与MULTICS系统类似,既有分段机制也有分页机制。比MULTICS有更少的段,但是更大的段大小。因为几乎没有程序需要1000个以上的段,但是有很多程序需要大段
- 该处理器中虚拟内存的核心是两张表,即LDT(全局描述符表)和GDT(全局描述符表)。每个程序都有自己的LDT,而同一个计算机上的所有程序共享一个GDT。LDT描述局部于每个程序的段,GDT描述系统段,即操作系统本身
<4> 分段和分页的区别
区别 | 分页 | 分段 |
---|---|---|
需要程序员了解正在使用这种技术吗 | 否 | 是 |
存在多少线性地址空间 | 1 | 许多 |
整个地址空间可以超出物理存储器的大小吗 | 是 | 是 |
过程和数据可以被区分并分别被保护吗 | 否 | 是 |
其大小浮动的表可以很容易提供吗 | 否 | 是 |
用户间过程的共享方便吗 | 否 | 是 |
为什么发明这种技术 | 为了得到大的线性地址空间而不必要购买更大的物理存储器 | 为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护 |
十. 总结
本文主要讲解内存管理的相关知识,主要有:
- 最简单的存储器抽象:就是没有存储器抽象,每一个程序都直接访问物理内存
- 地址空间:一种存储器抽象,它解决了操作系统易被破坏以及多进程并行困难的两大问题
- 交换:通过交换进程到内存或磁盘,实现多进程并行。它使用位图法或空闲链表法记录内存或磁盘的空闲空间
- 虚拟内存:通过划分每个进程的地址空间成一个个页面,在必要的时候通过页面置换算法放入内存中任何可用的页框内
- 页面置换算法:有多种页面置换算法,其中两个比较好的算法是老化算法和工作集时钟算法
- 分页系统的设计:主要解决如何设计一个合理的分页系统,包括页面分配策略,负载控制,页面大小,分离的指令空间和数据空间,共享页面/数据/库,内存映射策略,清除策略,虚拟内存接口等
- 分页系统的实现:主要介绍分页系统实现的相关细节,包括与分页有关的工作,指令备份,锁住页面,后备存储,策略机制的分离
- 分段:通过分段,每个段都可以方便地增大缩少。这便于处理在执行过程中大小有变化的数据结构,同时简化链接和共享,并为不同的段提供不同的保护