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 标志位记录当前页面是否被使用。
- 当页面被引用时,R 标志位置位
- 当一个页面将要被替换出去时,如果 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 时,扫描所有的页表项:
- R 置位,更新最近访问时间为当前虚拟时间
- R 复位,且最近访问时间早于 τ,移除这个页面(可替换页面)
- R 复位,但最近访问时间晚于 τ,记录这些页面中最老的一个。
- 如果全表扫描结束后,没有可替换的页面,就将第 3 步中记录的页面替换出去。
- 如果所有的页面 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 算法具有良好的页面调度性能,可以有效地实现,被广泛应用。

浙公网安备 33010602011771号