页面置换算法

98 阅读6分钟

题目:页面置换

背景介绍:分页内存管理

在现代操作系统中,为了高效管理内存,系统会将进程的虚拟内存划分为大小相等的页 (Page) 。物理内存同样被划分为大小相同的页框 (Frame) 。当进程需要访问某个页的数据时,操作系统必须将该页从磁盘调入物理内存的某个页框中,这个过程我们称之为缓存

你的任务是模拟这个缓存和置换过程,计算在给定的页面访问序列下,总共发生了多少次页置换(即因缓存已满而需要替换掉一个旧页面的情况)。

核心概念

  1. 页框 (Frames): 物理内存的容量,由 frameNum 决定,表示最多可以同时缓存多少个页。

  2. 缓存中的页: 每个被缓存的页都需要记录两个关键属性:

    • 访问次数 (Access Count): 该页被访问的总次数。
    • 访问时间 (Access Time): 该页最近一次被访问的时间点。我们可以用一个从 0 开始递增的计时器来表示。
  3. 页面访问: 按顺序处理 pages 数组中的每一个页面访问请求。

功能要求:页面访问规则

对于每一个页面访问请求,遵循以下规则:

情况一:页面命中 (Page Hit)
  • 当要访问的页已存在于缓存中:

    • 不发生置换。
    • 该页的访问次数加 1。
    • 更新该页的访问时间为当前最新时间。
情况二:页面缺失 (Page Fault / Miss)
  • 当要访问的页不存在于缓存中:

    • a. 如果有空闲页框:

      • 不发生置换。
      • 将新页放入一个空闲页框中。
      • 初始化该页的访问次数为 1,访问时间为当前最新时间。
    • b. 如果没有空闲页框:

      • 发生一次页置换,这是我们需要计数的事件。
      • 必须先按照下面的置换策略从缓存中选择一个“牺牲页”并将其移除。
      • 然后将新页放入腾出的页框中,并初始化其访问次数为 1,访问时间为当前最新时间。

置换策略详解

当缓存已满,需要置换一个页面时,遵循一个两步决策过程:

第一步:确定候选范围
  • 将缓存中所有已缓存的页,按照访问时间从早到晚排序。
  • 选择其中最老windowSize 个页,作为“过期候选页”。
第二步:选择牺牲页
  • 在“过期候选页”的范围内,应用以下规则来确定最终要被置换出去的牺牲页:

    1. 优先规则: 优先选择访问次数最少的那个页。
    2. 平局规则: 如果有多个页的访问次数都同为最少,则在它们当中选择访问时间最老(即最久未被访问)的那个页。

输入格式

  1. 第一个参数 frameNum:

    • 物理内存的页框数量。
    • 1 <= frameNum <= 64
  2. 第二个参数 windowSize:

    • 置换策略中候选范围的大小。
    • 1 <= windowSize <= frameNum
  3. 第三个参数 pages:

    • 一个整数数组,代表按顺序发生的页面访问请求。
    • 0 <= pages[i] <= 100 (页编号)
    • pages.length < 2000

输出格式

  • 一个整数,表示在处理完所有页面访问请求后,总共发生了多少次页置换。

样例

输入样例 1

3 2 [51, 52, 53, 54, 54, 51, 52, 53, 54]

输出样例 1

4

样例 1 执行流程详解

  • 系统状态: frameNum=3, windowSize=2
  • 时间 t 从 0 开始,每次访问递增。
访问页操作类型缓存状态 [页号(访问次数, 访问时间)]置换次数解释
51Miss (有空闲)[51(1, t=0)]0缓存未满,直接放入。
52Miss (有空闲)[51(1,0), 52(1,1)]0缓存未满,直接放入。
53Miss (有空闲)[51(1,0), 52(1,1), 53(1,2)]0缓存填满。
54Miss (需置换)[52(1,1), 53(1,2), 54(1,3)]1置换开始: 1. 按访问时间排序: 51(t=0), 52(t=1), 53(t=2)
2. 候选范围 (windowSize=2): {51, 52}
3. 候选者访问次数都是1,平局。选择时间最老的 51 进行置换。
54Hit[52(1,1), 53(1,2), 54(2,4)]154 已在缓存中。访问次数+1,更新访问时间。
51Miss (需置换)[53(1,2), 54(2,4), 51(1,5)]2置换开始: 1. 按访问时间排序: 52(t=1), 53(t=2), 54(t=4)
2. 候选范围: {52, 53}
3. 候选者访问次数都是1,平局。选择时间最老的 52 进行置换。
52Miss (需置换)[54(2,4), 51(1,5), 52(1,6)]3置换开始: 1. 按访问时间排序: 53(t=2), 54(t=4), 51(t=5)
2. 候选范围: {53, 54}
3. 候选者 53 (访问1次) vs 54 (访问2次)。选择访问次数少的 53 进行置换。
53Miss (需置换)[54(2,4), 52(1,6), 53(1,7)]4置换开始: 1. 按访问时间排序: 54(t=4), 51(t=5), 52(t=6)
2. 候选范围: {54, 51}
3. 候选者 54 (访问2次) vs 51 (访问1次)。选择访问次数少的 51 进行置换。
54Hit[52(1,6), 53(1,7), 54(3,8)]454 已在缓存中。访问次数+1,更新访问时间。

最终,总置换次数为 4。

import java.util.*;

public class PageReplacementSimulator {
    /**
     * 内部类,用于封装缓存在页框中的页信息
     */
    static class PageEntry {
        int pageNumber;
        int accessCount;
        int accessTime;

        public PageEntry(int pageNumber, int accessCount, int accessTime) {
            this.pageNumber = pageNumber;
            this.accessCount = accessCount;
            this.accessTime = accessTime;
        }
    }

    /**
     * 计算页置换次数
     *
     * @param frameNum   物理内存的页框数量
     * @param windowSize 置换时过期候选窗口的大小
     * @param pages      依次访问的页编号数组
     * @return 发生页置换的总次数
     */
    public int countPageReplacements(int frameNum, int windowSize, int[] pages) {
        // 使用 Map 模拟物理内存,Key: pageNumber, Value: PageEntry
        Map<Integer, PageEntry> frames = new HashMap<>();
        int replacementCount = 0;
        int currentTime = 0;

        for (int pageToAccess : pages) {
            currentTime++; // 每次访问,时间戳递增

            // --- 1. 检查页是否命中缓存 ---
            if (frames.containsKey(pageToAccess)) {
                PageEntry entry = frames.get(pageToAccess);
                entry.accessCount++;
                entry.accessTime = currentTime;
                continue; // 命中,继续下一次访问
            }

            // --- 2. 缺页处理 ---
            // 2A. 如果有空闲页框
            if (frames.size() < frameNum) {
                frames.put(pageToAccess, new PageEntry(pageToAccess, 1, currentTime));
            }
            // 2B. 如果没有空闲页框,需要置换
            else {
                replacementCount++; // 发生了一次置换

                // --- 执行置换策略 ---
                // 步骤 1: 将所有缓存页按访问时间排序,选出候选者
                List<PageEntry> allCachedPages = new ArrayList<>(frames.values());
                allCachedPages.sort(Comparator.comparingInt(p -> p.accessTime));

                // 获取窗口内的候选页
                List<PageEntry> candidates = new ArrayList<>();
                for (int i = 0; i < windowSize && i < allCachedPages.size(); i++) {
                    candidates.add(allCachedPages.get(i));
                }

                // 步骤 2: 在候选页中选择被置换页
                // 优先按访问次数升序,其次按访问时间升序
                PageEntry victim = Collections.min(candidates, 
                    Comparator.comparingInt((PageEntry p) -> p.accessCount)
                              .thenComparingInt(p -> p.accessTime)
                );
                
                // 执行置换
                frames.remove(victim.pageNumber);
                frames.put(pageToAccess, new PageEntry(pageToAccess, 1, currentTime));
            }
        }

        return replacementCount;
    }

    public static void main(String[] args) {
        PageReplacementSimulator simulator = new PageReplacementSimulator();

        // 样例 1
        int frameNum1 = 3;
        int windowSize1 = 2;
        int[] pages1 = {51, 52, 53, 54, 54, 51, 52, 53, 54};
        int result1 = simulator.countPageReplacements(frameNum1, windowSize1, pages1);
        System.out.println("样例 1 的置换次数: " + result1); // 预期输出: 4

        // 样例 2
        int frameNum2 = 4;
        int windowSize2 = 3;
        int[] pages2 = {1, 1, 30, 20, 5, 6, 5, 20, 5, 20, 1, 6, 7, 6};
        int result2 = simulator.countPageReplacements(frameNum2, windowSize2, pages2);
        System.out.println("样例 2 的置换次数: " + result2); // 预期输出: 2
    }
}