3、内存管理篇

厨子大约 32 分钟操作系统内存管理原创面试题技术解析程序员存储技术

内存管理篇

🌟什么是物理内存?

我们常说的物理内存大小就是指内存条的大小,一般买电脑时都会看下内存条是多大容量。

话说如果内存条大小是 100G,那这 100G 就都能够被使用吗?不一定,更多的还是要看 CPU 地址总线的位数,如果地址总线只有 20 位,那么它的寻址空间就是 1MB,即使可以安装 100G 的内存条也没有意义,也只能视物理内存大小为 1MB。

物理内存的缺点

这种方式下每个程序都可以直接访问物理内存,有两种情况:

  1. 系统中只有一个进程在运行:如果用户程序可以操作物理地址空间的任意地址,它们就很容易在不经意间破坏了操作系统,使系统出现各种奇奇怪怪的问题。

  2. 系统有多个进程同时在运行:如图,理想情况下可以使进程 A 和进程 B 各占物理内存的一边,两者互不干扰,但这也只是在理想情况下。

    1. 进程 B 在后台正常运行着,程序员在调试进程 A 时有可能就会误操作到进程 B 正在使用的物理内存,导致进程 B 运行出现异常,两个程序操作了同一地址空间,第一个程序在某一地址空间写入某个值,第二个程序在同一地址又写入了不同值,这就会导致程序运行出现问题,所以直接使用物理内存会使所有进程的安全性得不到保证

如何解决?

可以考虑为存储器创造新的抽象概念:地址空间

地址空间为程序创造了一种抽象的内存,是进程可用于寻址内存的一套地址集合,同时每个进程都有一套自己的地址空间,一个进程的地址空间独立于其它进程的地址空间。

如何为程序创造独立的地址空间?

最简单的办法就是把每个进程的地址空间分别映射到物理内存的不同部分。这样就可以保证不同进程使用的是独立的地址空间。

给每个进程提供一个基址 A 和界限 B,进程内使用的空间为 x,则对应的物理地址为 A + x,同时需要保证 A + x < B,如果访问的地址超过的界限,需要产生错误并中止访问。 为了达到目的,CPU 配置了两个特殊硬件寄存器:基址寄存器和界限寄存器,当一个进程运行时,程序的起始物理地址和长度会分别装入到基址寄存器和界限寄存器里,进程访问内存,在每个内存地址送到内存之前,都会先加上基址寄存器的内容。

**缺点:**每次访问内存都需要进行加法和比较运算,比较运算很快,但是加法运算由于进位传递事件的问题,在没有使用特殊电路的情况下会显得很慢。

此外,每个进程运行都会占据一定的物理内存,如果物理内存足够大到可以容纳许多个进程同时运行还好,但现实中物理内存的大小是有限的,可能会出现内存不够用的情况,怎么办?

方法 1:如果是因为程序太大,大到超过了内存的容量,可以采用 手动覆盖 技术,只把需要的指令和数据保存在内存中。

方法 2:如果是因为程序太多,导致超过了内存的容量,可以采用 自动交换 技术,把暂时不需要执行的程序移动到外存中。

覆盖技术

把程序按照自身逻辑结构,划分成多个功能相互独立的程序模块,那些不会同时执行的模块可以共享到同一块内存区域,按时间顺序来运行:

  • 将常用功能需要的代码和数据常驻在内存中;
  • 将不常用的功能划分成功能相互独立的程序模块,平时放到外存中,在需要的时候将对应的模块加载到内存中;
  • 那些没有调用关系的模块平时不需要装入到内存,它们可以共用一块内存区,需要时加载到内存,不需要时换出到外存中;

交换技术

多个程序同时运行,可以将暂时不能运行的程序送到外存,获得更多的空闲内存,操作系统将一个进程的整个地址空间内容换出到外存中,再将外存中某个进程的整个地址空间信息换入到内存中,换入换出内容的大小是整个程序的地址空间。

交换技术在实现上有很多困难:

  • 需要确定什么时候发生交换:简单的办法是可以在内存空间不够用时换出一些程序;
  • 交换区必须足够大:多个程序运行时,交换区(外存)必须足够大,大到可以存放所有程序所需要的地址空间信息;
  • 程序如何换入:一个程序被换出后又重新换入,换入的内存位置可能不会和上一次程序所在的内存位置相同,这就需要动态地址映射机制。

覆盖 vs 交换

  • 覆盖只能发生在那些相互之间没有调用关系的程序模块之间,因此程序员必须给出程序内的各个模块之间的逻辑覆盖结构。
  • 交换技术是以在内存中的程序大小为单位来进行的,它不需要程序员给出各个模块之间的逻辑覆盖结构。

通俗来说:覆盖发生在程序的内部,交换发生在程序与程序之间。

但是这两种技术都有缺点

  • 覆盖技术:需要程序员自己把整个程序划分为若干个小的功能模块,并确定各个模块之间的覆盖关系,增加了程序员的负担,很少有程序员擅长这种技术;
  • 交换技术:以进程作为交换的单位,需要把进程的整个地址空间都换进换出,增加了处理器的开销,还需要足够大的外存。

那有没有更好的解决上述问题的方法呢?答案是虚拟内存技术

对比维度
覆盖技术
交换技术
发生范围
程序内部模块之间
不同程序(进程)之间
管理单位
程序模块(函数/代码段)
整个进程的地址空间
实现方式
程序员手动划分模块和覆盖关系
操作系统自动管理
外存需求
仅需存放不常用模块
需存放整个进程的地址空间(交换区需足够大)
地址映射
模块加载位置固定(需程序员指定)
动态地址映射(换入位置可能变化)
优点
1. 减少常驻内存代码
2. 适合早期内存极小场景
1. 程序员无需干预
2. 支持多程序并发
缺点
1. 增加编程复杂度
2. 模块间不能有调用关系
3. 难以维护
1. 换入换出开销大
2. 外存占用多
3. 可能引发抖动(频繁交换)
适用场景
内存极端受限的单道程序系统(如DOS时代游戏)
早期多道程序系统(如Unix早期版本)
是否需要OS支持
基本不需要(由程序逻辑实现)
需要操作系统提供交换管理机制

🌟什么是虚拟内存?

虚拟内存,那就是虚拟出来的内存,它的基本思想就是确保每个程序拥有自己的地址空间,地址空间被分成多个块,每一块都有连续的地址空间,同时物理空间也分成多个块,块大小和虚拟地址空间的块大小一致,操作系统会自动将虚拟地址空间映射到物理地址空间,程序所关注的只是虚拟内存,请求的也是虚拟内存,其实真正使用的是物理内存。

虚拟内存技术有覆盖技术的功能,但它不是把程序的所有内容都放在内存中,因而能够运行比当前的空闲内存空间还要大的程序。它比覆盖技术做的更好,整个过程由操作系统自动来完成,无需程序员的干涉;

虚拟内存技术有交换技术的功能,能够实现进程在内存和外存之间的交换,因而获得更多的空闲内存空间。它比交换技术做的更好,它只对进程的部分内容在内存和外存之间进行交换。

虚拟内存技术的具体实现:

虚拟内存技术一般是在页式管理(下面介绍)的基础上实现:

  • 在装入程序时,不必将其全部装入到内存,而只需将当前需要执行的部分页面装入到内存,就可让程序开始执行;
  • 在程序执行过程中,如果需执行的指令或访问的数据尚未在内存(称为缺页)。则由处理器通知操作系统将相应的页面调入到内存,然后继续执行程序;
  • 另一方面,操作系统将内存中暂时不使用的页面调出保存在外存上,从而腾出更多空闲空间存放将要装入的程序以及将要调入的页面。

虚拟内存技术的特点:

  • 大的用户空间:通过把物理内存与外存相结合,提供给用户的虚拟内存空间通常大于实际的物理内存,即实现了两者的分离。如 32 位的虚拟地址理论上可以访问 4GB,而可能计算机上仅有 256M 的物理内存,但硬盘容量大于 4GB;
  • 部分交换:与交换技术相比较,虚拟存储的调入和调出是对部分虚拟地址空间进行的;
  • 连续性:程序可以使用一系列相邻连续的虚拟地址来映射物理内存中不连续的大内存缓冲区;
  • 安全性:不同进程使用的虚拟地址彼此隔离。一个进程中的代码无法更改正在由另一进程或操作系统使用的物理内存。

为什么要有虚拟地址空间呢?

  1. 先从没有虚拟地址空间的时候说起吧!没有虚拟地址空间的时候,程序都是直接访问和操作的都是物理内存 。但是这样有什么问题呢?
  2. 用户程序可以访问任意内存,寻址内存的每个字节,这样就很容易(有意或者无意)破坏操作系统,造成操作系统崩溃。

想要同时运行多个程序特别困难,比如你想同时运行一个微信和一个 QQ 音乐都不行。为什么呢?举个简单的例子:微信在运行的时候给内存地址 1xxx 赋值后,QQ 音乐也同样给内存地址 1xxx 赋值,那么 QQ 音乐对内存的赋值就会覆盖微信之前所赋的值,这就造成了微信这个程序就会崩溃。

总结来说:如果直接把物理地址暴露出来的话会带来严重问题,比如可能对操作系统造成伤害以及给同时运行多个程序造成困难。

通过虚拟地址访问内存有以下优势:

  • 程序可以使用一系列相邻的虚拟地址来访问物理内存中不相邻的大内存缓冲区。
  • 程序可以使用一系列虚拟地址来访问大于可用物理内存的内存缓冲区。当物理内存的供应量变小时,内存管理器会将物理内存页(通常大小为 4 KB)保存到磁盘文件。数据或代码页会根据需要在物理内存与磁盘之间移动。
  • 不同进程使用的虚拟地址彼此隔离。一个进程中的代码无法更改正在由另一进程或操作系统使用的物理内存。

虚拟内存如何映射到物理内存?

如图,CPU 里有一个内存管理单元(Memory Management Unit),简称 MMU,虚拟内存不是直接送到内存总线,而是先给到 MMU,由 MMU 来把虚拟地址映射到物理地址,程序只需要管理虚拟内存就好,映射的逻辑自然有其它模块自动处理。

什么是分页内存管理?

将虚拟地址空间分成若干个块,每个块都有固定的大小,物理地址空间也被划分成若干个块,每个块也都有固定的大小,物理地址空间的块和虚拟地址空间的块大小相等,虚拟地址空间这些块就被称为页面,物理地址空间这些块被称为

关于分页这里有个问题,页面的大小是多少合适呢?页面太大容易产生空间浪费,程序假如只使用了 1 个字节却被分配了 10M 的页面,这岂不是极大的浪费,页面太小会导致页表(下面介绍)占用空间过大,所以页面需要折中选择合适的大小,目前大多数系统都使用 4KB 作为页的大小。

上面关于虚拟内存如何映射到物理内存,只介绍了 MMU,但是 MMU 是如何工作的还没有介绍,MMU 通过页表这个工具将虚拟地址转换为物理地址。32 位的虚拟地址分成两部分(虚拟页号和偏移量),MMU 通过页表找到了虚拟页号对应的物理页号,物理页号 + 偏移量就是实际的物理地址。

上图只表示了页表的大体功能,页表的结构其实还很复杂,继续往下看。

页表的目的就是虚拟页面映射为物理内存的页框,页表可以理解为一个数学函数,函数的输入是虚拟页号,函数的输出是物理页号,通过这个函数可以把虚拟页面映射到物理页号,从而确定物理地址。不同机器的页表结构不同,通常页表的结构如下:

  • **页框号:**最主要的一项,页表最主要的目的就是找到物理页号;
  • **有效位:**1 表示有效,表示该表项是有效的,如果为 0,表示该表项对应的虚拟页面现在不在内存中,访问该页面会引起缺页中断,缺页中断后会去物理空间找到一个可用的页框填回到页表中;
  • **保护位:**表示一个页允许什么类型的访问,可读可写还是可执行;
  • **修改位:**该位反应了页面的状态,在操作系统重新分配页框时有用,在写入一页时由硬件自动设置该位,重新分配页框时,如果一个页面已经被修改过,则必须把它这个脏页写回磁盘,如果没有被修改过,表示该页是干净的,它在磁盘上的副本依然是有效的,直接丢弃该页面即可。
  • **访问位:**该位主要用于帮助操作系统在发生缺页中断时选择要被淘汰的页面,不再使用的页面显然比正在使用的页面更适合被淘汰,该位在页面置换算法中发挥重要作用。
  • **高速缓存禁止位:**该位用于禁止该页面被高速缓存。

如何加快地址映射速度?

每次访问内存都需要进行虚拟地址到物理地址的映射,每次映射都需要访问一次页表,所有的指令执行都必须通过内存,很多指令也需要访问内存中的操作数,因此每条指令执行基本都会进行多次页表查询,为了程序运行速度,指令必须要在很短的时间内执行完成,而页表查询映射不能成为指令执行的瓶颈,所以需要提高页表查询映射的速度。

**如何才能提高速度呢?**可以为页表提供一个缓存,通过缓存进行映射比通过页表映射速度更快,这个缓存是一个小型的硬件设备,叫 快表(TLB),MMU 每次进行虚拟地址转换时,首先去 TLB 中查找,找到了有效的物理页框则直接返回,如果没有找到则进行正常的页表访问,页表中找到后则更新 TLB,从 TLB 中淘汰一个表项,然后用新找到的表项替代它,这样下次相同的页面过来时可以直接命中 TLB 找到对应的物理地址,速度更快,不需要继续去访问页表。

这里之所以认为 TLB 能提高速度主要依靠程序局部性原理,程序局部性原理是指程序在执行过程中的一个较短时间,所执行的指令地址和要访问的数据通常都局限在一块区域内,这里可分为 时间局部性空间局部性

  • 时间局部性:一条指令的一次执行和下次执行,一个数据的一次访问和下次访问都集中在一个较短时间内;
  • 空间局部性:当前指令和邻近的几条指令,当前访问的数据和邻近的几个数据都集中在一个较小区域内。

多级页表

通过 TLB 可以加快虚拟地址到物理地址的转换速度,还有个问题,现在都是 64 位操作系统啦,有很大的虚拟地址空间,虚拟地址空间大那对应的页表也会非常大,又加上多个进程多个页表,那计算机的大部分空间就都被拿去存放页表,有没有更好的办法解决页表大的问题呢?答案是多级页表

页表为什么大?32 位环境下,虚拟地址空间有 4GB,一个页大小是 4KB,那么整个页表就需要 100 万页,而每个页表项需要 4 个字节,那整个页表就需要 4MB 的内存空间,又因为每个进程都有一个自己的页表,多个进程情况下,这简直就是灾难。

如图,以一个 32 位虚拟地址的二级页表为例,将 32 位虚拟地址划分为 10 位的 PT1 域,10 位的 PT2 域,以及 12 位的 offset 域,当一个虚拟地址被送入 MMU 时,MMU 首先提取 PT1 域并把其值作为访问第一级页表的索引,之后提取 PT2 域把把其值作为访问第二级页表的索引,之后再根据 offset 找到对应的页框号。

32 位的虚拟地址空间下:每个页面 4KB,且每条页表项占 4B:

  • 一级页表:进程需要 1M 个页表项(4GB / 4KB = 1M, 2^20 个页表项),即页表(每个进程都有一个页表)占用 4MB(1M * 4B = 4MB)的内存空间。
  • 二级页表:一级页表映射 4MB(2^22)、二级页表映射 4KB,则需要 1K 个一级页表项(4GB / 4MB = 1K, 2^10 个一级页表项)、每个一级页表项对应 1K 个二级页表项(4MB / 4KB = 1K),这样页表占用 4.004MB(1K * 4B + 1K * 1K * 4B = 4.004MB)的内存空间。

二级页表占用空间看着貌似变大了,为什么还说多级页表省内存呢

每个进程都有 4GB 的虚拟地址空间,而显然对于大多数程序来说,其使用到的空间远未达到 4GB,何必去映射不可能用到的空间呢?

也就是说,一级页表覆盖了整个 4GB 虚拟地址空间,但如果某个一级页表的页表项没有被用到,也就不需要创建这个页表项对应的二级页表了,即可以在需要时才创建二级页表。做个简单的计算,假设只有 20% 的一级页表项被用到了,那么页表占用的内存空间就只有 0.804MB(1K4B+0.21K1K4B=0.804MB),对比单级页表的 4M 是不是一个巨大的节约?

那么为什么不分级的页表就做不到这样节约内存呢?我们从页表的性质来看,保存在主存中的页表承担的职责是将虚拟地址翻译成物理地址。假如虚拟地址在页表中找不到对应的页表项,计算机系统就不能工作了。所以页表一定要覆盖全部虚拟地址空间,不分级的页表就需要有 1M 个页表项来映射,而二级页表则最少只需要 1K 个页表项(此时一级页表覆盖到了全部虚拟地址空间,二级页表在需要时创建)。

二级页表其实可以不在内存中:其实这就像是把页表当成了页面。当需要用到某个页面时,将此页面从磁盘调入到内存;当内存中页面满了时,将内存中的页面调出到磁盘,这是利用到了程序运行的局部性原理。我们可以很自然发现,虚拟内存地址存在着局部性,那么负责映射虚拟内存地址的页表项当然也存在着局部性了!这样我们再来看二级页表,根据局部性原理,1024 个第二级页表中,只会有很少的一部分在某一时刻正在使用,我们岂不是可以把二级页表都放在磁盘中,在需要时才调入到内存?

我们考虑极端情况,只有一级页表在内存中,二级页表仅有一个在内存中,其余全在磁盘中(虽然这样效率非常低),则此时页表占用了 8KB(1K4B+11K*4B=8KB),对比上一步的 0.804MB,占用空间又缩小了好多倍!

什么是缺页中断?

缺页中断就是要访问的页不在主存中,需要操作系统将页调入主存后再进行访问,此时会暂时停止指令的执行,产生一个页不存在的异常,对应的异常处理程序就会从选择一页调入到内存,调入内存后之前的异常指令就可以继续执行。

缺页中断的处理过程如下:

  1. 如果内存中有空闲的物理页面,则分配一物理页帧 r,然后转第 4 步,否则转第 2 步;
  2. 选择某种页面置换算法,选择一个将被替换的物理页帧 r,它所对应的逻辑页为 q,如果该页在内存期间被修改过,则需把它写回到外存;
  3. 将 q 所对应的页表项进行修改,把驻留位置 0;
  4. 将需要访问的页 p 装入到物理页面 r 中;
  5. 修改 p 所对应的页表项的内容,把驻留位置 1,把物理页帧号置为 x;
  6. 重新运行被中断的指令。

🌟都有哪些页面置换算法?

当缺页中断发生时,需要调入新的页面到内存中,而内存已满时,选择内存中哪个物理页面被置换是个学问,由此引入了多种页面置换算法,致力于尽可能减少页面的换入换出次数(缺页中断次数)。尽量把未来不再使用的或短期内较少使用的页面换出,通常在程序局部性原理指导下依据过去的统计数据来进行预测。

最优页面置换算法

当一个缺页中断发生时,对于保存在内存当中的每一个逻辑页面,计算在它的下一次访问之前,还需等待多长时间,从中选择等待时间最长的那个,作为被置换的页面。注意这只是一种理想情况,在实际系统中是无法实现的,因为操作系统不可能预测未来,不知道每一个页面要等待多长时间以后才会再次被访问。该算法可用作其它算法的性能评价的依据(在一个模拟器上运行某个程序,并记录每一次的页面访问情况,在第二遍运行时即可使用最优算法)。

先进先出算法

最先进入的页面最先被淘汰,这种算法很简单,不多介绍。

最近最久未使用算法

传说中的 LUR 算法,当发生缺页中断时,选择最近最久没有使用过的页面淘汰,该算法会给每个页面一个字段,用于记录自上次访问以来所经历的时间 T,当需要淘汰一个页面时,选择已有页面中 T 值最大的页面进行淘汰。

第二次机会页面置换算法

先进先出算法的升级版,只是在先进先出算法的基础上做了一点点改动,因为先进先出算法可能会把经常使用的页面置换出去,该方法会给这些页面多一次机会,给页面设置一个修改位 R,每次淘汰最老页面时,检查最老页面的 R 位,如果 R 位是 0,那么代表这个页面又老又没有被二次使用过,直接淘汰,如果这个页面的 R 位是 1,表示该页面被二次访问过,将 R 位置 0,并且把该页面放到链表的尾端,像该页面是最新进来的一样,然后继续按这种方法淘汰最老的页面。

时钟页面置换算法

第二次机会页面算法的升级版,尽管二次机会页面算法是比较合理的算法,但它需要在链表中经常移动页面,效率比较低,时钟页面置换算法如图,该算法把所有的页面都保存在一个类似时钟的环形链表中,一个表针指向最老的页面,当发生缺页中断时,算法首先检查表针指向的页面,如果它的 R 位是 0 就淘汰该页面,并且把新的页面插入这个位置,然后表针移动到下一个位置,如果 R 位是 1 就将 R 位置 0 并把表针移动到下一个位置,重复这个过程直到找到一个 R 位是 0 的页面然后淘汰。

最不常用算法

当发生缺页中断时,选择访问次数最少的那个页面去淘汰。该算法可以给每个页面设置一个计数器,被访问时,该页面的访问计数器 +1,在需要淘汰时,选择计数器值最小的那个页面。

这里有个问题:一个页面如果在开始的时候访问次数很多,但之后就再也不用了,那它可能永远都不会淘汰,但它又确实需要被淘汰,怎么办呢?可以定期把减少各个页面计数器的值,常见的方法是定期将页面计数器右移一位。

最不常用算法(LFU)和最近最久未使用算法(LRU)的区别:LRU 考察的是最久未访问,时间越短越好,而 LFU 考察的是访问的次数或频度,访问次数越多越好。

工作集页面置换算法

介绍该算法时首先介绍下什么是工作集。

工作集是指一个进程当前正在使用的页面的集合,可以用二元函数 W(t, s)表示:

  • t 表示当前的执行时刻)s 表示工作集窗口,表示一个固定的时间段
  • W(t, s)表示在当前时刻 t 之前的 s 时间段中所有访问页面所组成的集合

不同时间下的工作集会有所变化,如图:

  • 进程开始执行后随着访问新页面逐步建立较稳定的工作集
  • 当内存访问的局部性区域的位置大致稳定时(只访问那几个页面 没有大的改变时) 工作集大小也大致稳定
  • 局部性区域的位置改变时(进程前一项事情做完 去做下一项事情时) 工作集快速扩张和快速收缩过渡到下一个稳定值。

工作集置换算法主要就是换出不在工作集中的页面,示例如图:

  • 第 0 次访问 e:缺页,装入 e
  • 第 1 次访问 d:缺页,装入 d
  • 第 2 次访问 a:缺页,装入 a
  • 第 3 次访问 c:缺页,装入 c
  • 第 4 次访问 c:命中,时间窗口【1-4】,淘汰 e
  • 第 5 次访问 d:命中,时间窗口【2-5】
  • 第 6 次访问 b:缺页,时间窗口【3-6】,淘汰 a,装入 b
  • 第 7 次访问 c:命中,时间窗口【4-7】
  • 第 8 次访问 e:缺页,时间窗口【5-8】,装入 e
  • 第 9 次访问 c:命中,时间窗口【6-9】,淘汰 d,装入 c
  • 第 10 次访问 e:命中,时间窗口【7-10】,淘汰 b
  • 第 11 次访问 a:缺页,时间窗口【8-11】,装入 a
  • 第 12 次访问 d:缺页,时间窗口【9-12】,装入 d

工作集时钟页面置换算法

在工作集页面置换算法中,当缺页中断发生后,需要扫描整个页表才能直到页面的状态,进而才能确定被淘汰的是哪个页面,因此比较耗时,所以引入了工作集时钟页面算法。与时钟算法改进了先进先出算法类似,工作集页面置换算法 + 时钟算法=工作集时钟页面置换算法。避免了每次缺页中断都需要扫描整个页表的开销。

算法名称
核心思想
优点
缺点
适用场景
最优算法(OPT)
置换未来最长时间不被访问的页面
理论最优(缺页率最低)
无法实际实现(需预知未来)
作为其他算法的性能基准
先进先出(FIFO)
置换最早进入内存的页面
实现简单
可能置换高频使用页面(Belady异常)
早期简单系统
最近最久未用(LRU)
置换最长时间未被访问的页面
符合局部性原理,性能接近OPT
硬件实现代价高(需维护访问时间栈)
对性能要求高的系统
第二次机会(SC)
FIFO改进版,检查页面访问位(R),若为1则给第二次机会
减少高频页面被误置换
链表移动开销大
需平衡开销的中等复杂度系统
时钟算法(Clock)
环形链表维护页面,指针扫描时清除R位,置换R=0的页面
降低SC的移动开销
可能重复扫描
通用系统(Linux改进版常用)
最不常用(LFU)
置换访问频率最低的页面
适合长期热点页面明显的场景
计数器溢出问题,历史权重过高
长期运行的缓存系统
工作集(WS)
置换不在当前工作集(最近Δ时间内被访问的页面集合)中的页面
动态适应程序局部性变化
需维护时间窗口,计算开销大
多道程序分时系统
工作集时钟(WSClock)
结合工作集和时钟算法,环形扫描时检查是否在工作集窗口内
避免全表扫描,实时性较好
实现复杂
现代通用操作系统(如Windows)

什么是分段内存管理?

关于分段内存管理我们平时见的最多的应该就是 Linux 可执行程序的代码段数据段之类的啦,要了解分段最好的方式就是了解它的历史。分段起源于 8086CPU,那时候程序访问内存还是直接给出相应单元的物理地址,为了方便多道程序并发执行,需要支持对各个程序进行重定位,如果不支持重定位,涉及到内存访问的地方都需要将地址写死,进而把某个程序加载到物理内存的固定区间。通过分段机制,程序中只需要使用段的相对地址,然后更改段的基址,就方便对程序进行重定位。而且 8086CPU 的地址线宽度是 20 位,可寻址范围可以达到 1MB,但是它们的寄存器都是 16 位,直接使用 1 个 16 位寄存器不可能访存达到 1MB,因此引入了段,引入了段寄存器,段寄存器左移 4 位 + 偏移量就可以生成 20 位的地址,从而达到 1MB 的寻址范围。

以如今的科技水平,其实已经不再需要这种段移位加偏移的方式来访存,分段更多的是一种历史包袱,没有多大实际作用,而且我们经常见到的可执行程序中代码段数据段这些更多是为了在逻辑上能够更清晰有序的构造程序的组织结构。Linux 实际上没有使用分段而只使用了分页管理,这样会更加简单,现在的分段其实更多是为了使逻辑更加清晰。一个公司,为了方便管理都会划分为好多个部门,这其实和分段逻辑相似,没有什么物理意义但是逻辑更加清晰。

分页机制和分段机制的共同点和区别

共同点 :

  • 分页机制和分段机制都是为了提高内存利用率,减少内存碎片。
  • 页和段都是离散存储的,所以两者都是离散分配内存的方式。但是,每个页和段中的内存是连续的。

区别 :

  • 页的大小是固定的,由操作系统决定;而段的大小不固定,取决于我们当前运行的程序。
  • 分页仅仅是为了满足操作系统内存管理的需求,而段是逻辑信息的单位,在程序中可以体现为代码段,数据段,能够更好满足用户的需要。