操作系统之 虚拟存储页面置换算法

页面置换算法概念

  • 功能
    • 当出现缺页异常时 需调入新页面且内存已满 置换算法选择被置换的物理页面
  • 设计目标
    • 尽可能地减少页面调入调出次数
    • 把未来不再访问 或 短期内不访问的页面调出
  • 页面锁定 (Frame locking)
    • 描述必须常驻内存的逻辑页面
    • 操作系统的关键代码
    • 要求响应速度的代码和数据
    • 页表中的锁定标志位 (Lock bit)

页面置换算法评价方法

  • 记录进程访问内存的页面轨迹
    1
    2
    3
    4
    5
    虚拟地址访问用(页号, 位移)表示
    (3,0), (1,9), (4,1), (2,1), (5,3), (2,0), (1,9), (2,4), (3,1), (4,8)
    对应的页面轨迹
    3, 1, 4, 2, 5, 2, 1, 2, 3, 4
    替换如 c, a, d, b, e, b, a, b, c, d
  • 评价标准
    • 模拟页面置换算法的行为 记录各个置换算法产生缺页的次数 越少越好

局部页面置换算法

置换页面的选择范围仅限于当前进程占用的物理页面内 (在当前进程中的物理页面来决定换哪个页)

  • 最优算法 (预测未来来决定 太理想)
  • 先进先出算法 (先进先出并不一定反映了实际的访问特征 性能较差)
  • 最近最久未使用算法 (统计过去 假设过去的特征是对未来的预测)
    • 时钟算法
    • 最不常用算法

最优页面置换算法 (OPT optional)

置换在未来最长时间不访问页面

最优页面置换算法实现
  • 缺页时 计算内存中的每个逻辑页面的下一次访问时间
  • 选择 未来最长时间 不访问 的页面
最优页面置换算法特征
  • 缺页次数最少
  • 实际系统中无法实现
  • 无法预知每个页面在下次访问前的等待时间
  • 但是能作为置换算法性能评测依据
最优页面置换算法示例
1
2
3
4
5
第一次 第二次 第三次 第四次都是正常访问的
访问第五次的时候 要访问 e 不在物理内存 产生缺页
缺页异常中断服务例程 引用最优页面置换算法 算出未来最长时间不访问的页面 为 d 它 在 10 的时候访问
将 d页 替换为 e页
一共发生了 两次 缺页异常

OPT_page_replacement_algorithm

先进先出置换算法 (FIFO First-In First-Out)

选择在内存中驻留时间 最长的 页面进行置换

先进先出置换算法实现
  • 维护一个记录所有位于内存中的逻辑页面的链表
  • 链表元素按照驻留时间排序 链首最长 链尾最短
  • 出现缺页时 选择链首页面进行置换 新页面加到链尾
先进先出置换算法特征
  • 实现简单
  • 性能较差 调出的页面很可能是经常访问的
  • 进程分配物理页面数增加时 缺页不一定减少(Belady 现象)
  • 有缺陷 不单独使用 揉到其它算法里一起使用
先进先出置换算法示例
1
2
3
4
第一次 第二次 第三次 第四次 都是正常访问
第五次访问 e 出现缺页异常 进入缺页异常中断服务例程 引用先进先出置换算法
因为 物理页帧 a 是最早进来的 按照先进先出的规则 a 换为 e
整个过程 共发生了 五次 缺页异常

FIFO_page_replacement_algorithm

最近最久未使用页面置换算法 (LRU Least Recently Used)

属于 最优置换算法 和 先进先出置换算法 的折中

  • 选择最长时间没有被引用的页面进行置换
  • 某些页面长时间未被访问 则它们在将来还可能会长时间不会访问
最近最久未使用算法实现
  • 缺页时 计算内存中每个逻辑页面的上一次访问时间
  • 选择上一次使用到当前时间 最长的 页面置换出去
最近最久未使用算法特征

最优置换算法的一种近似

最近最久未使用算法示例
1
2
3
4
第一次 第二次 第三次 第四次 都不产生缺页异常
第五次访问 e 产生缺页异常
过去的时间 c 是最长时间没有被访问的 因此 将 c 换为 e
总共出现 三次 缺页异常

LRU_page_replacement_algorithm

最近最久未使用算法可能实现

每次访问开销都很大

  • 页面链表
    • 系统维护一个按最近一次访问时间排序的链表
      • 链表首节点是最近刚刚访问过的页面
      • 链表尾节点是最久未使用的页面
    • 访问内存时 找到相应页面 并将其移到链首
    • 缺页时 置换链表尾节点的页面
  • 活动页面栈
    • 访问时 将页号压入栈顶 并将栈内相同的页号删去
    • 缺页时 置换栈底页面
最近最久未使用算法栈实现
1
2
3
4
第一 ~ 第四次 相安无事 但要将依次访问过的页 压入栈顶
第五次 访问 e 缺页异常 将栈底 的 c 换为 e
第六次 访问 b 栈里已经有 b 了 将 栈中的 b 删去 再压入栈顶
共 发生 三次 缺页异常

LRU_stack_page_replacement_algorithm

时钟页面置换算法 (Clock)

仅对页面的访问情况进行大致统计 (统计过去一段时间 这个页面是否被访问过 若访问过则留下 没访问过则按照现有顺序做置换)

  • 数据结构
    • 在页表项加入 访问位 Access 位 描述页面在过去一段时间内访问情况
    • 各页面组织成环形链表
    • 指针指向最先调入的页面 后绕着环形链表走 看起来很像 时钟
  • 算法
    • 访问页面时 在页表项 记录页面访问情况
    • 缺页时 从指针处开始顺序查找未被访问过的页表进行置换
时钟置换算法特征

时钟算法是 LRU(最久未使用算法) 和 FIFO(先进先出算法) 做折中
因为它既不像 LRU 考虑的那么详细 又不像 FIFO 只在意一段时间内若不访问 就做置换 考虑的那么粗

时钟置换算法实现
  • 页面装入内存时 访问位 置 0
  • 访问页面 (读/写) 时 访问位 置 1
  • 缺页时 从指针当前位置顺序检查环形链表
    • 访问位为 0 则置换此页
    • 访问位为 1 则访问位置 0 指针继续移动到下一个页面 直到找到可置换页面
时钟置换算法示例
1
2
3
4
第一 ~ 第四次访问 的页面都已在环形链表中 故而 只将访问位 置 1
第五次访问 e 缺页异常 指针扫描环形链表 访问位 为 1 的 则置 0
不断地循环扫描 直至 扫出可置换页面 这里 会循环了一遍以后 将 页面 a 换为 e
一共发生了 四次 缺页异常

Clock_page_replacement_algorithm

改进时钟置换算法
  • 时钟置换算法缺点

之前的时钟置换算法如果要置换的页是被修改过的 那么就会先要将修改过的页 写到外存 然后
才将要换入的页读入内存 消耗时间过长

  • 改进

减少修改页的缺页处理开销 (如果要置换的页被修改了 则不置换此页 同时操作系统定期将修改过的页写到外存)

  • 算法
    • 在页面中加入修改位 并在访问时 进行相应修改
    • 缺页时 修改页面标志位 以跳过有修改的页面
改进时钟置换算法实例
1
2
3
4
5
6
7
第一次 只访问 c 访问位 置 1
第二次 写 a 访问位 修改位 都置 1
第三次 访问 d 访问位 置 1
第四次 写 b 访问位 修改位 都 置 1
第五次 访问 e 缺页异常 循环一遍 访问位 为 1 改成 0 访问位 为 0 若 修改位 为 1 改成 0
最后只会将 c 换 为 e 因为循环下来 a 和 b 页都是被修改过的 跳过 只有 c 访问位和修改位都为 0
共发生 三次 缺页异常

Improved_Clock_page_replacement_algorithm

最不常用算法 (LFU Least Frequently Used)

缺页时 置换访问次数最少的页

最不常用算法实现
  • 每个页面设置一个访问计数
  • 访问页数 访问计数 + 1
  • 缺页时 置换 计数最小的页面
最不常用算法特征
  • 算法开销大
  • 开始时频繁使用 以后不使用的页面难换出 (因为页面在里面待的越久 访问计数越大)
    • 解决方法 较大的计数定期右移
LRU 和 LFU 的区别
  • LRU 关注多久未使用 时间越短越好 (维护栈)
  • LFU 关注次数 待的越久越好 (稍微简单些)
最不常用算法示例
1
2
3
每次访问 修改 页面的计数 前 四次访问 都只是 累加
到第五次访问 e 缺页异常 寻找当前计数最小的页 为 a 并将其置换
共产生 五次 缺页异常

LFU 不能够比较快的访问 但是 它可以用在 读取硬盘这种存储访问 时间长一些也没关系

LFU_page_replacement_algorithm.png

Belady 现象

采用 FIFO 等算法时 可能出现分配的物理页面数增加 缺页次数反而升高的异常现象

产生 Belady 现象的原因
  • FIFO 算法的置换特征 与 进程访问内存的动态特征矛盾
  • 被它置换出去的页面 并不一定是 进程 近期不会访问的页面

总而言之就是预测(近期不访问的页面)不合理

FIFO 的 Belady 现象
1
2
当物理页面数 为 3 时 使用 FIFO 页面置换算法 共产生 9 次 缺页次数
当物理页面数 为 4 时 使用 FIFO 页面置换算法 反而产生了 10 次 缺页次数

FIFO_Belady_3

FIFO_Belady_4

LRU 没有 Belady 现象

LRU_No_Belady

LRU FIFO 和 Clock 比较
  • LRU 和 FIFO 本质都是 先进先出算法
    • LRU 根据页面最近访问时间排序
    • LRU 需要动态地调整顺序 (算法性能好 系统开销大)
    • FIFO 根据页面进入内存的实际排序
    • FIFO 的页面进入时间是固定不变的 (系统开销较小 但会发生 Belady 现象)

当页面进入内存后 没有被访问了 最近访问时间和进入内存的时间相同时 LRU 可退化成 FIFO

  • Clock 算法是 它们的折中
    • 页面访问时 不动态调整页面在链表中的顺序 只是做一个标记
    • 缺页时 再将它移动到链表末尾 (跳过访问位 为 1 的页面)

对于未被访问的页面 LRU FIFO Clock 性能一样 因为都退化到 FIFO 了
对于被访问过的页面 Clock 算法 不能记录准确的访问时间 而 LRU 可以

全局页面置换算法

置换页面的选择范围是所有(所有进程)可换出的物理页面 (考虑的角度是从各个进程所需的内存不同)

  • 工作集算法
  • 缺页率算法

给进程分配可变数目的物理页面

局部页面置换算法的不足

局部页面置换算法没有考虑进程访存的差异 (进程在不同阶段内存需求是不同的)

(使用局部页面置换算法 无论怎么置换 物理页面始终是那么多 缺页次数不能减少 如果给它加一些物理页面 性能会提高很多)

全局页面置换算法需要解决的问题

  • 进程在不同阶段的内存需求是变化的
  • 分配给进程的内存也需要在不同阶段有所变化
  • 全局置换算法需要确定分配给进程的物理页面数

CPU利用率和并发进程数的关系

CPU_process

  • CPU利用率 与 并发进程数存在相互促进和制约的关系
    • 进程数少时 提高并发进程数 可提高 CPU利用率
    • 并发进程导致内存访问增加
    • 并发进程的内存访问会降低访存局部性特征 (两个进程所做的事情不相干 局部性原理不适用)
    • 局部性特征下降 缺页率上升和 CPU利用率下降
工作集

一个进程当前正在使用的逻辑页面的集合 可表示为二元函数 W(t, Δ)

  • t 是当前的执行时刻
  • Δ 为工作集窗口(Working-set window) 一个定长的页面访问时间窗口
  • W(t, Δ) 为在当前时刻t前的 Δ 时间窗口中所有访问页面所组成的集合
  • | W(t, Δ) | 为工作集大小 页面数量
工作集的变化

Working_set

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

在当前时刻 进程实际驻留在内存当中的页面集合

  • 工作集和常驻集的关系
    • 工作集是进程运行过程中固有的性质 (进程在一段时间访问的页面集合)
    • 常驻集 取决于 系统分配给进程的物理页面数 和 页面置换算法 (实际在内存中的页)
  • 缺页率和常驻集的关系
    • 常驻集 包含 工作集时 缺页较少 (进程访问的页都在内存里)
    • 工作集发送剧烈过渡时 缺页较多
    • 进程常驻集大小达到一定数目后 缺页率不会明显下降 (内存够进程功能使用了 再去加内存 反而效率下降)

工作集置换算法

换出不在工作集中的页面 (并不一定是在缺页时才做 因此开销大)

工作集置换算法实现

窗口大小 τ 当前时刻前τ个内存访问的页引用是工作集

  • 访存链表 维护窗口内的访存页面链表
  • 访存时 换出不在工作集的页面 (开销大)
  • 缺页时 换入页面
工作集置换算法示例
1
2
3
4
第一次访问 缺页 装入 c 此时 窗口大小 τ 为 4 不需要换出页
第二次访问 不缺页 但是 此时在工作集的 页为 5 而窗口大小 τ 为 4 需要将最早的页 e 放到外存
以此类推
共 发送 五次缺页

Working_set_page_replacement_algorithm

缺页率算法 (PFF Page Fault Frequency)

缺页率 (Page fault rate) = 缺页次数 / 内存访问次数(不好用) 更多时候使用 缺页平均时间间隔的倒数

  • 影响缺页率因素
    • 页面置换算法 (只有这个能自己控制)
    • 分配给进程的物理页面数
    • 页面大小
    • 程序的编写方法 (局部性特征)
缺页率算法实现

通过调节常驻集的大小 使每个进程的缺页率保持在一个合理的范围

  • 若进程缺页率过高 则增加常驻集以分配更多物理页面数
  • 若进程缺页率过低 减少常驻集 将一些页面置换到外存

PFF_process

  • 访存时 设置引用位标记
  • 缺页时 计算从上次缺页时间 t_last 到现在 t_current 的时间间隔
    • 如果 t_current - t_last > T 缺页率低 则置换 所有在[t_last, t_current]中没有被引用的页 使其用在更有意义的地方(CPU并发率提高)
    • 如果 t_current - t_last <= T 缺页率高 则增加缺失页到常驻集中
缺页率算法示例
1
2
3
4
5
第一次访问 出现缺页 直接加(因为没有之前状态可供参考) 第二次 第三次 不缺页
第四次 访问 d 出现缺页 时间间隔 为 3 > 窗口大小 2 说明缺页率低 将这段时间没有访问过的页 置换到外存 只将 c 和 d 留下
第五次访问 不缺页 第六次访问 e 缺页异常 时间间隔 为 2 <= 窗口大小 2 直接将 e 加入内存
第七次 第八次 都不缺页 第九次访问 a 缺页异常 时间间隔 为 3 > 窗口大小 2 只留下 c e a 三个页
共 发送 五次 缺页异常

PFF_page_replacement_algorithm
跟 工作集算法对比 工作集算法每一次访问都在考虑置换哪一个页面到外存 缺页率算法只有在时间间隔够大的时候 才置换到外存 将置换这一过程放到了 缺页中断来完成
和 局部置换算法一样 开销降低

抖动和负载控制

抖动产生是因为 随着驻留内存的进程数目增加 分配给每个进程的物理页面数不断减小 缺页率不断上升
因此操作系统需在 并发水平 和 缺页率之间达到一个平衡 需要选择一个适当的进程数目 和 进程所需要的物理页面数

  • 抖动问题(Thrashing)
    • 进程物理页面太少 不能包含工作集
    • 造成大量缺页 频繁置换
    • 进程运行速度变慢

负载控制

通过调节并发进程数 (MPL Multiprogramming level) 来进行系统负载控制

  • 找到 平均缺页间隔时间 (MTBF Mean time between page faults) = 缺页异常处理时间 (PFST Page fault service time) 的范围

thrashing