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

474 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
行,我直接给你一版“可当笔记+可当实现模板+可当面试答案”的压缩版,全塞满但保证结构清晰。你可以一屏一屏滚着读。
---
# 一、问题模型(统一抽象,后面所有算法共用)
**页面置换问题 = 给定引用串 + 固定物理页框数 → 最小化缺页次数**
* 输入:
* `pages[]`:页面访问序列(虚拟页号)
* `frames`:物理页框数(内存容量)
* 输出:
* `page_faults`:缺页次数
* 核心状态:
* `memory`:当前在内存中的页集合
* 替换策略:当 `memory` 满时,决定踢谁
---
# 二、FIFOFirst-In First-Out最简单但很蠢
## 思想
谁最早进来谁先滚(队列)
## 特点
* 实现极简单
* 会出现 **Belady异常页框增加反而缺页更多**
## 数据结构
* 队列 `queue`
* 集合 `set`O(1)判断是否命中)
## 过程
```
访问 page:
if page 在 memory:
命中
else:
缺页
if memory 未满:
加入 queue + set
else:
victim = queue.pop_front()
删除 victim
插入新 page
```
## C++实现
```cpp
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++实现
```cpp
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++实现
```cpp
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时间戳简单但慢
```cpp
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衰减
---
## 基本实现(简化版)
```cpp
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++实现
```cpp
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 内核到底怎么做页面置换(不是课本版,是源码级逻辑)”**
那个才是真正有含金量的版本。
---
**LRU**Least 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)
考虑到你对系统级语言的偏好,这里给出一个简单的逻辑结构示例:
```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 实现)吗?