Files
Operating-System/Memory/change_page.md
2026-03-22 17:40:44 +08:00

11 KiB
Raw Permalink Blame History

行,我直接给你一版“可当笔记+可当实现模板+可当面试答案”的压缩版,全塞满但保证结构清晰。你可以一屏一屏滚着读。


一、问题模型(统一抽象,后面所有算法共用)

页面置换问题 = 给定引用串 + 固定物理页框数 → 最小化缺页次数

  • 输入:

    • pages[]:页面访问序列(虚拟页号)
    • frames:物理页框数(内存容量)
  • 输出:

    • page_faults:缺页次数
  • 核心状态:

    • memory:当前在内存中的页集合
    • 替换策略:当 memory 满时,决定踢谁

二、FIFOFirst-In First-Out最简单但很蠢

思想

谁最早进来谁先滚(队列)

特点

  • 实现极简单
  • 会出现 Belady异常页框增加反而缺页更多

数据结构

  • 队列 queue
  • 集合 setO(1)判断是否命中)

过程

访问 page:
  if page 在 memory:
    命中
  else:
    缺页
    if memory 未满:
        加入 queue + set
    else:
        victim = queue.pop_front()
        删除 victim
        插入新 page

C++实现

int FIFO(const vector<int>& pages, int capacity) {
    unordered_set<int> memory;
    queue<int> q;
    int faults = 0;

    for (int p : pages) {
        if (memory.count(p)) continue;

        faults++;
        if (memory.size() == capacity) {
            int victim = q.front(); q.pop();
            memory.erase(victim);
        }
        memory.insert(p);
        q.push(p);
    }
    return faults;
}

三、OPTOptimal理论最优用来对比

思想

淘汰 未来最久不会再用 的页

特点

  • 最优(理论下界)
  • 不可实现(需要预知未来)

数据结构

  • 当前内存 memory
  • 每次缺页 → 扫描未来找“最晚使用”的

过程核心

for 每个 page:
  if miss:
    if 未满: 加入
    else:
      对 memory 中每个页:
        找它下一次出现的位置
      选“最远”或“不再出现”的

C++实现

int OPT(const vector<int>& pages, int capacity) {
    vector<int> memory;
    int faults = 0;

    for (int i = 0; i < pages.size(); i++) {
        int p = pages[i];

        if (find(memory.begin(), memory.end(), p) != memory.end())
            continue;

        faults++;

        if (memory.size() < capacity) {
            memory.push_back(p);
        } else {
            int idx = -1, farthest = i;

            for (int j = 0; j < memory.size(); j++) {
                int k;
                for (k = i + 1; k < pages.size(); k++) {
                    if (pages[k] == memory[j]) break;
                }
                if (k > farthest) {
                    farthest = k;
                    idx = j;
                }
            }
            memory[idx] = p;
        }
    }
    return faults;
}

四、LRULeast Recently Used最重要

思想

淘汰 最近最久没用 的页(用“过去”预测“未来”)

特点

  • 实际系统常用
  • 不会出现 Belady 异常
  • 需要维护“访问顺序”

实现1链表 + 哈希(标准解)

数据结构

  • list:维护访问顺序(最新在前)
  • unordered_map:页 → 迭代器

操作复杂度

  • 全部 O(1)

C++实现

int LRU(int capacity, const vector<int>& pages) {
    list<int> lru;
    unordered_map<int, list<int>::iterator> pos;
    int faults = 0;

    for (int p : pages) {
        if (pos.count(p)) {
            lru.erase(pos[p]);
        } else {
            faults++;
            if (lru.size() == capacity) {
                int victim = lru.back();
                lru.pop_back();
                pos.erase(victim);
            }
        }
        lru.push_front(p);
        pos[p] = lru.begin();
    }
    return faults;
}

实现2时间戳简单但慢

int LRU_slow(const vector<int>& pages, int capacity) {
    vector<int> memory;
    unordered_map<int, int> last_used;
    int faults = 0;

    for (int i = 0; i < pages.size(); i++) {
        int p = pages[i];

        if (find(memory.begin(), memory.end(), p) != memory.end()) {
            last_used[p] = i;
            continue;
        }

        faults++;

        if (memory.size() < capacity) {
            memory.push_back(p);
        } else {
            int lru_page = memory[0];
            for (int m : memory) {
                if (last_used[m] < last_used[lru_page])
                    lru_page = m;
            }
            replace(memory.begin(), memory.end(), lru_page, p);
        }
        last_used[p] = i;
    }
    return faults;
}

五、LFULeast Frequently Used

思想

淘汰访问次数最少的页

问题

  • “历史包袱”:旧热点永远不走
  • 需要加 aging衰减

基本实现(简化版)

int LFU(const vector<int>& pages, int capacity) {
    unordered_map<int, int> freq;
    vector<int> memory;
    int faults = 0;

    for (int p : pages) {
        if (find(memory.begin(), memory.end(), p) != memory.end()) {
            freq[p]++;
            continue;
        }

        faults++;

        if (memory.size() < capacity) {
            memory.push_back(p);
        } else {
            int victim = memory[0];
            for (int m : memory) {
                if (freq[m] < freq[victim])
                    victim = m;
            }
            replace(memory.begin(), memory.end(), victim, p);
            freq.erase(victim);
        }
        freq[p] = 1;
    }
    return faults;
}

六、ClockSecond Chance操作系统最常用

思想LRU的近似

  • 每页有一个 引用位 R

  • 扫描环:

    • R=1 → 置0跳过给第二次机会
    • R=0 → 淘汰

数据结构

  • 环形数组
  • ref_bit[]
  • 指针 hand

过程

while true:
  if ref_bit[hand] == 0:
    替换
    break
  else:
    ref_bit[hand] = 0
    hand++

C++实现

int Clock(const vector<int>& pages, int capacity) {
    vector<int> memory(capacity, -1);
    vector<int> ref(capacity, 0);
    int hand = 0, faults = 0;

    for (int p : pages) {
        bool hit = false;
        for (int i = 0; i < capacity; i++) {
            if (memory[i] == p) {
                ref[i] = 1;
                hit = true;
                break;
            }
        }
        if (hit) continue;

        faults++;

        while (true) {
            if (ref[hand] == 0) {
                memory[hand] = p;
                ref[hand] = 1;
                hand = (hand + 1) % capacity;
                break;
            } else {
                ref[hand] = 0;
                hand = (hand + 1) % capacity;
            }
        }
    }
    return faults;
}

七、算法对比(你考试/面试直接用)

算法 是否最优 是否可实现 时间复杂度 是否Belady异常
FIFO O(1)
OPT O(n²) 不会
LRU 近似最优 O(1) 不会
LFU 不稳定 O(n) 不一定
Clock LRU近似 OS常用 O(1) 不会

八、核心理解(你真正该掌握的)

你别死记算法,要抓住这条主线:

页面置换的本质 = 用过去信息预测未来访问

  • FIFO完全不预测 → 垃圾
  • OPT完美预测 → 理论
  • LRU用“最近”预测 → 工程最佳
  • ClockLRU的低成本近似 → OS实际用
  • LFU用“频率”预测 → 容易翻车

九、你如果继续往下学(很关键)

你下一步应该看:

  • 工作集模型Working Set
  • 局部性原理Temporal / Spatial Locality
  • 页抖动Thrashing
  • Linux实际实现active/inactive list本质是LRU变种

如果你要,我下一步可以直接给你:

“Linux 内核到底怎么做页面置换(不是课本版,是源码级逻辑)” 那个才是真正有含金量的版本。


LRULeast Recently Used最近最少使用是一种非常经典的缓存淘汰算法

它的核心思想基于“时间局部性”原理:如果一个数据在最近一段时间内没有被访问过,那么在将来它被访问的可能性也比较小。 因此,当缓存空间存满时,我们会优先清理掉那个“最久未被访问”的数据。


1. LRU 的工作机制

想象一个有固定容量的队列,数据流的操作遵循以下规则:

  1. 访问数据
    • 如果数据已经在缓存中(命中),则将其移动到队列的最前端(表示它刚被用过)。
    • 如果数据不在缓存中(未命中),将其插入到队列的最前端
  2. 淘汰数据
    • 当新数据进入且缓存已满时,直接丢弃队列末尾的数据,因为它就是那个“最久没用过”的对象。

2. 工业级实现:哈希表 + 双向链表

在实际编程中(如 Redis 或操作系统内存管理),为了达到 O(1) 的操作效率,通常采用 Hash Map + Doubly Linked List 的组合结构:

  • 双向链表 (Double Linked List):负责维护数据的访问顺序
    • 靠近 Head 的是最近访问的。
    • 靠近 Tail 的是最久未访问的。
  • 哈希表 (Hash Map):负责快速定位
    • Key 是数据的标识Value 是指向链表节点的指针/引用。这样不需要遍历链表就能找到数据。

为什么不用普通数组或单链表?

  • 数组:虽然查询快,但移动元素(把中间的挪到开头)需要 $O(n)$,代价太高。
  • 单链表:删除末尾节点时,无法在 O(1) 时间内找到前驱节点。

3. LRU 的优缺点

优点 缺点
性能均衡:对具有时间局部性的数据流非常高效。 缓存污染:如果偶尔发生一次大规模的“全表扫描”,会导致热点数据全部被踢出。
实现简单:逻辑清晰,是面试和系统设计中的首选。 无法感知频率:一个数据即便访问了 1000 次,只要它比只访问 1 次的数据更早被访问,它也会被淘汰。

4. 代码实现参考 (Rust)

考虑到你对系统级语言的偏好,这里给出一个简单的逻辑结构示例:

use std::collections::HashMap;

struct LRUCache {
    capacity: usize,
    // 实际生产中通常使用双向链表配合 Raw Pointer 或 RefCell
    // 这里示意Map 存 Key -> (Value, Access_Timestamp)
}

impl LRUCache {
    fn get(&mut self, key: i32) -> Option<i32> {
        // 1. 在 Map 中找
        // 2. 如果找到,更新该节点的顺序(移到链表头)
        // 3. 返回值
        None
    }

    fn put(&mut self, key: i32, value: i32) {
        // 1. 如果 Key 存在,更新 Value 并移到头
        // 2. 如果不存在,插入新节点到头
        // 3. 如果超过 capacity删除链表尾部节点并同步从 Map 中移除
    }
}

下一步建议

LRU 虽然经典,但在面对高并发或特定访问模式时有其局限性。你想了解它的改进版 LRU-K(解决缓存污染问题)或者像 Linux 内核中使用的 Clock 算法(更节省内存的近似 LRU 实现)吗?