3.4 页面替换算法 Page Replacement Algorithms

当 cpu 访问到一个已分配但未缓存的页面时,触发 page fault,操作系统需要分配一个物理页帧。如果此时物理内存不足,就需要将部分页面交换到磁盘,然后将新的页面加载到内存。

为什么说是要分配一个物理页帧?

如果是 malloc 等动态分配内存的操作,只需要在内存中分配一个页帧,而如果还需要从磁盘加载数据的话,还需要将页面从磁盘加载到这个页帧中。

什么情况下会需要将页面换出?

当内存中的页帧不足,但是又需要分配新页帧时。

如果没有开启 swap,是否不会产生页交换?

理论上不会将页面交换到 swap 分区,也就不存在“页交换”。但是 linux 系统仍然有一些回收内存的机制,直接将一些页面丢弃掉,为新页面腾出空间,如:文件页回收,OOM Killer。

既然有页交换机制,为什么还会触发 OOM ?

  • swap 空间不足
  • 部分内存也不允许被换出,如:内核内存、mlock 锁定内存、共享内存等

当一个进程需要页交换时,应当换出哪个进程的页面?

都可以,由页面替换算法决定。

页交换涉及到磁盘 IO,性能损耗大,应当尽力避免不必要的页交换,这就需要合理选择被换出的页面

如果频繁访问的页面被换出,CPU 很快又会访问它们,又需要将他们从 swap 加载回内存,导致不必要的性能损耗。

3.4.1 The Optimal Page Replacement Algorithm

“计算”每个页面到下一次被引用时需要经过的指令数,需要经过指令数最多的页面就是最晚被引用的,移除这个页面。

缺点:难以实现。无法确定每个页面的下一次访问时间。

3.4.2 The Not Recently Used Page Replacement Algorithm

么个虚拟页都有两个标志位:

  • R:页面是否被访问(读或者写)
  • W:页面是否被写入

每次内存引用时都必须更新这两个标志位,这就有必要通过硬件来实现。如果硬件不支持,可以通过软件模拟(page fault & clock interrupt)。操作系统定期复位 R 标记位,用来标识近期哪些页面被访问。

进程启动时,所有的页面都不在内存中。发生 page fault 后,将页面加载到内存中,将 R 标志位置位,此时页面处于 Read Only 模式。如果之后页面被修改,将 M 标志位置位,页面可以被读写。

根据 R/M 标记位,将页面分为四类,需要替换页面时,从换出优先级最高的一类页面中随机选择一个页面换出。

R M 换出优先级
Class 0 0 0 最高
Class 1 0 1
Class 2 1 0
Class 3 1 1 最低

3.4.3 The First-In, First-Out (FIFO) Page Replcaement Algorithm

操作系统维护一个队列来保存页面,新页面从队列尾部入队,当需要替换页面时,移除队列首部的页面。

FIFO 算法没有考虑到页面访问的频率。当内存引用访问到一个已经在队列中的页面时,页面在队列中的顺序不变,所以队首的页面可能是一个被频繁访问的页面,不应当被替换出去。

3.4.4 The Second-Chance Page Replacement Algorithm

对 FIFO 算法改进:使用 R 标志位记录当前页面是否被使用。

  1. 当页面被引用时,R 标志位置位
  2. 当一个页面将要被替换出去时,如果 R 标志位置位,就清除 R 标志位,并将页面放在队列尾部,修改页面的加载时间为当前时间,否则换出页面。

通过上述算法,始终可以找到内存中最老且最近没有被访问的页面,然后将这个页面替换出去。

当队列中的所有页面的 R 标志位都置位时,该算法会退化为 FIFO 算法。

3.3.5 The Clock Page Replacement Algorithm

Second-Chance 算法的性能有一些小瑕疵,完全没有必要移动链表的节点。

Clock 算法将 Second-Chance 算法的链表替换为循环链表,使用一个指针指向最老的一个页面。当发生 page fault 时:

  • 如果指针指向的页面 R 标志位为 0,就替换该页面
  • 如果指针指向的页面 R 标志位为 1,标志位复位,指针指向下一个页面,直到找到 R 为 0 的页面

3.4.6 The Least Recently Used (LRU) Page Replacement Algorithm

原理:时间局部性原理,最近经常使用的指令在接下来的一段时间内大概率也会被使用,反之亦然。

基于这一原理的 LRU 算法,在需要替换页面时,换出最久未使用的页面。

LRU 的页面替换效率接近 Optimal 算法,但是实现复杂并且代价昂贵。

  • 需要在内存中维护一个记录了所有页面使用频率的有序链表
  • 每次发生内存引用时,都需要更新链表
  • 在链表中删除节点、移动节点等操作都是昂贵的

可以凭借特殊的硬件实现 LRU。用一个 64 bit 的计数器,在每次执行指令后自增一。每个页表项保留一个字段,在每次内存引用后保存计数器的值,作为这个页面最后一次被访问的时间戳。

当发生 page fault 时,找到时间戳最小的页面,替换出去。

3.4.7 Simulating LRU in Software

使用 NFU (Not Frequently Used) 算法软件化模拟 LRU。

每个页面都有一个独立的计数器(counter),发生时钟中断时,扫描内存中的所有页面,如果 R 标志位为 1 则 counter++。当发生 page fault 时,淘汰 counter 最小的页面。

NFU 算法存在一个致命缺陷,如果或一个页面早期使用频繁,但是近期不怎么使用,它的 counter 依然会很高,在 page fault 时,可能会将近期频繁使用的页面替换出去。

在此基础上,aging 算法做了改进,在统计页面使用频率时,先将 counter 右移一位,然后将 R 标志位放到 counter 的最高位。这样 counter 从最高位到最低位,依次表示最近的某个时钟周期内,该页面是否被使用过,由此反映出这个页面最近的使用情况。

\(counter = (counter >> 1) | (R << (n-1))\)(n 为 counter 的位数)

当发生 page fault 时,依然选择 counter 最小的页面淘汰掉。距离现在越近的页面,它在 counter 中的位置越高,所占的权重越大。

3.4.8 The Working Set Page Replacement Algorithm

请求分页(demand paging)

  • 仅加载必需页:进程启动时,只加载少数的必需页
  • 按需加载:当访问到未加载的页触发 page fault 时,由操作系统动态加载页面
  • 惰性加载:避免一次性加载所有页

当进程被操作系统启动时,CPU 访问进程的第一条指令,然后触发 page fault,将进程入口页面加载到内存中,随后全局变量、堆栈等页面也被 page fault 加载到内存中。当且进程,需要访问到一个页面时,才加载整个页面。

引用局部性(locality of reference):在进程的任意执行阶段,总是只引用有关联的一小部分页面。

  • 时间局部性
  • 空间局部性

工作集(working set):当前进程正在使用的页面的集合。

当一个进程的工作集页面都在内存中时,进程可以正常运行且不会触发page fault,直到进程进入下一个阶段(工作集发生变动)。

系统抖动(thrashing):进程每隔几条指令就触发 page fault,导致大量时间花费在页面置换上。

如果系统内存太小,不足以容纳进程的完整工作集,那么进程会频发触发 page fault 并且执行得很慢。

工作集模型(working set model):追踪进程的工作集,确保进程运行时,工作集始终在内存中。

在多任务系统中,进程总是会被操作系统交换到磁盘上,让新的进程执行。当磁盘上的进程重新回到内存时(一开始只是一个页面重新进入内存),它会触发大量的 page fault,直到工作集的所有页面都被加载到内存中。这种方法会浪费大量的 cpu 资源在 page fault 的中断处理上,理想的方法应当是将工作集页面一次性全部加载到内存中。

预分页(prepaging):在进程运行前(在进程从磁盘回到内存时),先加载页面,然后再启动进程。

定义工作集为:最近 k 个内存引用对用页面的集合。

工作集页面替换算法:发生 page fault 时,将不在工作集的页面替换出去。

如何选取 k 的值?

k 的取值越大,工作集的包含的页面越多,随着 k 取值的增加,工作集的大小增加地越来越少,逐渐趋于平稳,直到 \(k_{max}\) 对应的工作集涵盖了这个进程从启动开始的所有页面。那么 k 取一个合适的值即可。

如何计算工作集?

维护一个长度为 k 的队列,每次发生内存引用时,将对应的页面入队,如果队列长度超过 k,将最老的页面出队。最后对整个队列去重,就得到了工作集。但是这个算法的开销太大,实际上不会被使用。

近似地,将工作集定义为:最近一段时间 τ 内,进程引用过的页面的集合。

需要注意的是,这里的时间是进程在 cpu 上实际运行的时间,称为 当前虚拟时间(current virtual time)

每个页表项维护两个字段,最近访问时间和 R 标志位。

当页面被访问时,R 标志位置位。时钟中断定期清除页面的 R 标志位。R 标志位表示这个页面在当前周期内是否被访问过。

当发生 page fault 时,扫描所有的页表项:

  1. R 置位,更新最近访问时间为当前虚拟时间
  2. R 复位,且最近访问时间早于 τ,移除这个页面(可替换页面)
  3. R 复位,但最近访问时间晚于 τ,记录这些页面中最老的一个。
  4. 如果全表扫描结束后,没有可替换的页面,就将第 3 步中记录的页面替换出去。
  5. 如果所有的页面 R 都置位,那就随机选择一个替换出去。

3.4.9 The WSClock Page Replacement Algorithm

working set 算法需要遍历所有的页表项,所以开销很大。

WSClock 算法基于 clock 算法,同时也使用了 working set,有效简化了算法复杂度、提高了算法性能。

使用循环链表组织页帧,当首次加载一个时,将其放入到链表的尾部。
链表的每一项都包含页面最近访问时间(working set 算法中的最近访问时间),R 标志位和 M 标志位。

发生 page fault 时,考虑以下情况:

  • 指针指向的页面 R = 1,将 R 复位,指针向前移动跳过这个页面

    R = 1 说明这个页面近期访问过,处于 working set 中

  • 指针指向的页面 R = 0,
    • 页面的存活时间超过 τ,并且 M = 0,不涉及脏页写会,可以直接替换出去
    • 页面的存活时间超过 τ,并且 M = 1,涉及到脏页写回,所以不能替换该页,指针继续前移
    • 页面存活时间不超过 τ,说明该页在 working set 中,跳过
R age M 是否在 working set 是否可以替换
1 - -
0 小于 τ -
0 大于 τ 0
0 大于 τ 1

上述,遇到不在 working set 中的脏页时,如果要换出个脏页,就必须调用磁盘 IO ,等待脏页写回完成,极大降低 WSClock 算法的性能。可以异步调度磁盘 IO 来完成脏页写回

如果指针转完一圈后没有找到可以换出的页,那么考虑以下情况:

  • 至少调度了一次写操作。那么只需要等待任意写操作完成,就可以得到一个干净的页,然后将该页换出。
  • 没有调度过写操作。说明所有页都在 working set 中,选择任一干净的页换出,否则选择任意脏页同步写回并换出。

3.4.10 Summary of Page Replacement Algorithms

算法 说明 优势 劣势
Optimal 换出未来最晚访问的页 全局最优解 无法实现
NRU 根据 R M 将页面分为四类,换出优先级最低一类的任一页面 易于实现 性能较差
FIFO 先进先出,换出最老的页面 易于实现 可能会换出经常使用的页面,性能差
Second-Chance 改进 FIFO,如果被被换出的页面还在使用,就保留该页面 实现简单,相比 FIFO 性能更优 相比LRU等,性能较差
Clock 改进 Second-Chance,无需移动链表节点 无需移动链表节点,开销比 Second-Chance 略好 相比LRU等,性能较差
LRU 通过硬件设备记录最近最少使用的页面 性能好,接近 Optimal 需要借助硬件设备,难以实现
NRU 软件模拟的 LRU 算法 相比 LRU 无需特定的硬件设备 只是 LRU 的粗略近似,性能较差
age 通过位运算记录近期页面的使用情况 性能上接近 LRU 算法 -
Working Set 计算进程的 working set,换出不在其中的页面 性能好 实现开销大
WSClock 改进 Working Set,无需遍历全部页表 性能好、开销低

综上,age 算法和 WSClock 算法具有良好的页面调度性能,可以有效地实现,被广泛应用。

posted @ 2025-09-10 23:57  DantalianLib  阅读(23)  评论(0)    收藏  举报