Files
obsidian/操作系统/14_分页存储管理/14_分页存储管理.md

598 lines
18 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.
# 14. 分页存储管理
> **课程**: 操作系统 - 存储器管理
> **核心内容**: 碎片问题、分页思想、地址结构、页表、地址变换、TLB快表、多级页表、倒转页表
---
## 前置知识
- [[13_存储管理基础]] — 存储器层次结构、逻辑地址与物理地址、MMU
- [[01_系统运行机制]] — CPU工作模式与硬件基础
---
## 一、碎片问题
在连续分配方式中,内存中会出现无法被利用的空闲区域,称为**碎片**。
| 碎片类型 | 出现位置 | 产生原因 | 对应分配方式 |
|---------|---------|---------|------------|
| **内碎片** (Internal Fragmentation) | 分配区域**内部** | 固定分区大小 > 进程实际需要 | 固定分区分配 |
| **外碎片** (External Fragmentation) | 分配区域**外部** | 空闲分区太小,无法满足任何请求 | 动态分区分配 |
```mermaid
flowchart LR
subgraph 内碎片示意
direction TB
A1["┌──────────────┐"]
A2["│ 进程实际数据 │"]
A3["│──────────────│"]
A4["│ 未使用空间 │ ← 内碎片"]
A5["└──────────────┘"]
end
subgraph 外碎片示意
direction TB
B1["┌────┐ ┌────┐ ┌────┐ ┌────┐"]
B2["│进程A│ │空闲│ │进程B│ │空闲│"]
B3["└────┘ └────┘ └────┘ └────┘"]
B4[" ↑外碎片 ↑外碎片"]
end
```
> **根本原因**: 无论是固定分区还是动态分区,都要求进程在内存中**连续存放**。要彻底解决碎片问题,必须放弃"连续"要求——这就是**分页**思想的核心出发点。
---
## 二、分页的基本思想
分页存储管理将**虚拟地址空间**和**物理内存**都划分为大小相等的小块:
- 虚拟地址空间的每一块称为**虚拟页 (Virtual Page, VP)**
- 物理内存的每一块称为**物理页框 (Page Frame, PF)**
常见的页面大小为 $2^k$ 字节512B、1KB、2KB、**4KB**(最常用)。
```mermaid
flowchart TB
subgraph VA["虚拟地址空间 (进程视角)"]
direction TB
VP0["虚拟页 VP0"]
VP1["虚拟页 VP1"]
VP2["虚拟页 VP2"]
VP3["虚拟页 VP3"]
VP4["..."]
end
subgraph PM["物理内存"]
direction TB
PF0["物理页框 PF0"]
PF1["物理页框 PF1"]
PF2["物理页框 PF2"]
PF3["物理页框 PF3"]
PF4["..."]
end
VP0 -->|"页表映射"| PF2
VP1 -->|"页表映射"| PF0
VP2 -->|"不在内存"| DISK["外存(磁盘)"]
VP3 -->|"页表映射"| PF3
style VA fill:#e3f2fd,stroke:#1976d2
style PM fill:#fff8e1,stroke:#f9a825
style DISK fill:#fce4ec,stroke:#c62828
```
**关键特性**:
- 虚拟页在物理内存中**不需要连续**存放
- 每个虚拟页可以映射到任意一个空闲物理页框
- 消除了外碎片(但每个页内可能有少量内碎片)
---
## 三、地址结构
在分页系统中,虚拟地址被划分为两部分:
```
虚拟地址 VA (v+k 位)
┌─────────────────┬─────────────┐
│ VPN (v位) │ VPO (k位) │
│ 虚拟页号 │ 页内偏移 │
└─────────────────┴─────────────┘
```
**计算公式**:
- 页大小 = $2^k$ 字节
- 虚拟页号 VPN = $\lfloor VA / 2^k \rfloor$(即高位部分)
- 页内偏移 VPO = $VA \mod 2^k$(即低 $k$ 位)
物理地址同理:
```
物理地址 PA (p+k 位)
┌─────────────────┬─────────────┐
│ PPN (p位) │ PPO (k位) │
│ 物理页号 │ 页内偏移 │
└─────────────────┴─────────────┘
```
- 物理页号 PPN 由页表查得
- **PPO = VPO**(页内偏移不变,直接复制)
### 地址计算示例
> **例题**: 某系统虚拟地址 16 位,页面大小 4KB ($2^{12}$),求虚拟地址 VA = 0x3A6F 对应的 VPN 和 VPO。
- 页大小 $2^k = 2^{12}$,所以 $k = 12$$v = 16 - 12 = 4$
- VPN = $0x3A6F / 0x1000 = 0x3$高4位0011
- VPO = $0x3A6F \mod 0x1000 = 0xA6F$低12位
```
VA = 0x3A6F = 0011 1010 0110 1111
──── ────────────────
VPN VPO
(0x3) (0xA6F)
```
---
## 四、页表
页表是实现虚拟页到物理页框映射的核心数据结构。
### 页表结构
每个进程拥有**独立的页表**。页表以 VPN 为索引,每个页表项 (PTE) 包含:
```
页表 (以VPN为索引)
┌──────┬───────┬──────────────────────────────┐
│ VPN │ PPN │ 控制位 │
├──────┼───────┼──────────────────────────────┤
│ 0 │ 5 │ V=1 D=0 R=1 W=1 U/S=1 │
│ 1 │ 2 │ V=1 D=1 R=1 W=1 U/S=1 │
│ 2 │ --- │ V=0 (不在内存) │
│ 3 │ 8 │ V=1 D=0 R=1 W=0 U/S=1 │
│ ... │ ... │ ... │
└──────┴───────┴──────────────────────────────┘
```
### 页表项 (PTE) 各字段
| 字段 | 含义 | 说明 |
|------|------|------|
| **有效位 (Valid/Present)** | 页面是否在内存中 | V=1在内存V=0不在内存缺页 |
| **修改位 (Dirty)** | 页面是否被写过 | D=1被修改过换出时需写回外存 |
| **引用位 (Reference)** | 页面是否被访问过 | 用于页面置换算法如Clock、LRU近似 |
| **读/写权限 (R/W)** | 读写保护 | 只读页被写入时触发保护异常 |
| **用户/内核 (U/S)** | 访问权限 | U=0仅内核可访问U=1用户可访问 |
| **物理页号 (PPN)** | 对应的物理页框号 | 与VPO拼接得到物理地址 |
---
## 五、地址变换过程
### 基本地址变换流程
```mermaid
flowchart TD
A["CPU发出虚拟地址 VA"] --> B["从VA中提取VPN和VPO"]
B --> C{"TLB查找VPN"}
C -->|"TLB命中"| D["直接取出PPN"]
C -->|"TLB未命中"| E["访问页表<br/>(页表基址寄存器PTBR + VPN × PTE大小)"]
E --> F{"页表项有效位?"}
F -->|"V=1"| G["取出PPN"]
F -->|"V=0"| H["**缺页中断**<br/>操作系统处理"]
H --> I["从外存调入页面"]
I --> J["更新页表"]
J --> G
D --> K["物理地址 PA = PPN × 2^k + PPO"]
G --> K
K --> L["访问物理内存"]
style H fill:#ffcdd2,stroke:#c62828
style C fill:#e8f5e9,stroke:#2e7d32
```
### 详细步骤
1. **提取地址字段**: 从虚拟地址 VA 中分离出 VPN 和 VPO
2. **查TLB**: 用 VPN 在 TLB 中查找(见第六节)
3. **查页表**: 若 TLB 未命中,用 **PTBR页表基址寄存器+ VPN** 定位页表项
4. **检查有效位**:
- V=1取出 PPN与 PPO 拼接得到物理地址
- V=0触发**缺页中断**OS 从外存调入页面
5. **拼接物理地址**: PA = PPN | PPO将 PPN 放高位PPO 放低位)
### 缺页中断处理流程
```mermaid
flowchart TD
A["发生缺页中断"] --> B["保存CPU现场"]
B --> C{"外存中找到该页?"}
C -->|"找到"| D{"内存有空闲页框?"}
C -->|"未找到"| E["终止进程<br/>(非法访问)"]
D -->|"有空闲"| F["从外存读入该页"]
D -->|"无空闲"| G["执行页面置换算法<br/>选择牺牲页"]
G --> H{"牺牲页Dirty=1?"}
H -->|"是"| I["将牺牲页写回外存"]
H -->|"否"| J["直接覆盖"]
I --> F
J --> F
F --> K["修改页表<br/>设置V=1, PPN"]
K --> L["重新执行被中断的指令"]
```
### 地址变换计算示例
> **例题**: 某系统页面大小 1KB ($2^{10}$),虚拟地址 14 位。页表如下,求虚拟地址 VA=0x1A8F 对应的物理地址。
| VPN | PPN | Valid |
|-----|-----|-------|
| 0 | 3 | 1 |
| 1 | 7 | 1 |
| 2 | --- | 0 |
| 3 | 5 | 1 |
| 4 | 2 | 1 |
| 5 | 8 | 1 |
| 6 | 1 | 1 |
**解题过程**:
1. 页面大小 $2^{10}$,所以 $k=10$$v=14-10=4$
2. VA = 0x1A8F = **01 1010 1000 1111** (二进制)
- VPN = 高4位 = 0110 = **6**
- VPO = 低10位 = 10 1000 1111 = 0x28F
3. 查页表VPN=6 对应 PPN=**1**Valid=1
4. PA = PPN | PPO = 1 × 2^{10} + 0x28F = 0x400 + 0x28F = **0x68F**
```
VA = 0x1A8F: 0110 1010001111
VPN=6 VPO=0x28F
查页表: VPN=6 → PPN=1
PA = 0001 1010001111 = 0x068F
PPN=1 PPO=0x28F
```
---
## 六、TLB 快表
### TLB 概述
**TLB (Translation Lookaside Buffer)** 是集成在 MMU 中的高速缓存,存储最近使用的页表项。
```mermaid
flowchart LR
CPU["CPU"] -->|"虚拟地址"| TLB{"TLB<br/>(快表)"}
TLB -->|"命中<br/>取出PPN"| PA["物理地址"]
TLB -->|"未命中"| PT["查页表<br/>(内存)"]
PT -->|"PPN"| PA
PT -->|"更新TLB"| TLB
style TLB fill:#e8f5e9,stroke:#2e7d32
style PT fill:#fff3e0,stroke:#e65100
```
### TLB 组织方式
| 方式 | 说明 | 特点 |
|------|------|------|
| **全相联** | VPN可以放在TLB的任意位置 | 灵活但查找慢适合小容量TLB |
| **组相联** | VPN映射到固定的组(set),组内任意放置 | 折中方案,最常用 |
| **直接映射** | VPN映射到固定的TLB位置 | 最快但冲突多 |
### TLB 性能分析
设 TLB 查找时间为 $\lambda$,内存访问时间为 $t$TLB 命中率为 $a$
**无 TLB 时**:每次地址变换需要访问一次页表(内存)+ 一次数据访问(内存)
$$EAT_{无TLB} = t + t = 2t$$
**有 TLB 时**
$$EAT = a(\lambda + t) + (1-a)(\lambda + t + t) = \lambda + t + (1-a) \cdot t$$
> **计算示例**: 设 $\lambda = 10$ns, $t = 100$ns, $a = 0.98$98%命中率)
>
> $EAT = 10 + 100 + (1 - 0.98) \times 100 = 10 + 100 + 2 = 112$ ns
>
> 相比无TLB的 $2t = 200$ ns性能提升了约 **44%**。
---
## 七、多级页表
### 为什么需要多级页表
对于 32 位系统,页面大小 4KB
- 虚拟地址空间 = $2^{32}$ = 4GB
- 页数 = $2^{32} / 2^{12} = 2^{20}$ = 1M 个页
- 每个页表项 4 字节 → 页表大小 = 1M × 4B = **4MB**
4MB 的页表对于每个进程都太大了!而且页表必须连续存放。
### 二级页表结构
**核心思想**: 将页表本身也分页,用"页目录"来索引这些页表页。
```
32位虚拟地址 (二级页表)
┌──────────────┬──────────────┬──────────────┐
│ 页目录索引(10位)│ 页表索引(10位) │ 页内偏移(12位)│
└──────────────┴──────────────┴──────────────┘
```
```mermaid
flowchart LR
CR3["CR3<br/>(页目录基址)"] --> PD["页目录<br/>(1024项)"]
PD -->|"页目录项"| PT1["页表1<br/>(1024项)"]
PD -->|"页目录项"| PT2["页表2<br/>(1024项)"]
PD -->|"页目录项"| PT3["页表3<br/>(1024项)"]
PT1 -->|"页表项+偏移"| PF1["物理页框"]
PT2 -->|"页表项+偏移"| PF2["物理页框"]
PT3 -->|"页表项+偏移"| PF3["物理页框"]
style PD fill:#e3f2fd,stroke:#1976d2
style PT1 fill:#fff3e0,stroke:#e65100
style PT2 fill:#fff3e0,stroke:#e65100
style PT3 fill:#fff3e0,stroke:#e65100
```
### 多级页表的优势
- 页目录只需常驻内存4KB页表页按需创建
- 未使用的虚拟地址区域**不需要分配页表页**,节省大量内存
- 64 位系统通常使用 **3~5 级页表**(如 Linux 的 4 级页表PGD→PUD→PMD→PTE→偏移
### 二级页表地址变换
1. 用 CR3 找到页目录基址
2. 用**页目录索引**在页目录中找到页表页的物理地址
3. 用**页表索引**在页表页中找到 PPN
4. PPN 与**页内偏移**拼接得到物理地址
> **注意**: 二级页表需要 **3 次内存访问**(页目录 + 页表 + 数据),比一级页表多一次。因此 TLB 的作用更加重要。
---
## 八、倒转页表
### 基本思想
传统页表以**虚拟页号**为索引,每个进程一张。倒转页表以**物理页框号**为索引,整个系统一张。
| 对比项 | 传统页表 | 倒转页表 |
|--------|---------|---------|
| 索引 | 虚拟页号 (VPN) | 物理页框号 (PFN) |
| 表项数 | 虚拟页数(可能很大) | 物理页框数(固定) |
| 进程数 | 每进程一张 | 全系统一张 |
| 查找方式 | 直接索引 | 需要搜索或用Hash |
```mermaid
flowchart LR
subgraph 传统页表
direction TB
T1["VPN 0 → PPN x"]
T2["VPN 1 → PPN y"]
T3["VPN 2 → PPN z"]
T4["..."]
end
subgraph 倒转页表
direction TB
I1["PFN 0 ← VPN a, 进程P1"]
I2["PFN 1 ← VPN b, 进程P2"]
I3["PFN 2 ← VPN c, 进程P1"]
I4["..."]
end
```
**优点**: 表大小与物理内存成正比,节省空间(尤其在 64 位系统)
**缺点**: 查找需要搜索整个表(通常用 Hash 加速)
---
## 九、内存保护
分页系统中通过以下机制实现内存保护:
### 1. 越界保护
- 页表项中的有效位 V=0 表示该页不在内存,访问时触发缺页中断
- 非法虚拟地址(超出进程地址空间范围)触发保护异常
### 2. 标志位保护
| 标志位 | 保护功能 |
|--------|---------|
| R/W | 读/写权限控制。只读页被写入时触发异常 |
| U/S | 用户/内核权限。用户态访问内核页时触发异常 |
| NX (No Execute) | 禁止执行位。数据页被当作代码执行时触发异常 |
### 3. 键保护
- 每个物理页框有一个保护键 (Protection Key)
- 每个进程有一个键寄存器
- 只有键匹配时才允许访问
- Intel MPX/MPK 技术支持此机制
---
## 十、空闲页面管理
操作系统需要跟踪物理内存中哪些页框是空闲的:
### 1. 位示图法 (Bitmap)
用一个 bit 表示一个物理页框的状态0=空闲1=已分配。
```
位示图示例 (假设16个物理页框):
位号: 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
状态: 1 1 0 1 0 0 1 1 0 1 0 0 1 1 0 1
↑ ↑ ↑
空闲 空闲 空闲
```
- **优点**: 简单,查找连续空闲块方便
- **缺点**: 位示图本身占用内存(物理内存 4GB、页大小 4KB → 位示图 128KB
### 2. 链表法
将所有空闲页框用链表串起来:
```
空闲链表: [PF3] → [PF5] → [PF8] → [PF12] → NULL
```
- **优点**: 实现简单,不额外占用大量空间
- **缺点**: 查找连续空闲块需要遍历链表
---
## 十一、Nachos 页式存储管理代码
Nachos 教学操作系统中实现了基本的分页存储管理。以下是关键代码片段:
### 核心数据结构
```cpp
// 页表项结构 (Machine/translate.h)
typedef struct {
int virtualPage; // 虚拟页号 (VPN)
int physicalPage; // 物理页号 (PPN)
bool valid; // 有效位
bool readOnly; // 只读标志
bool use; // 引用位 (用于LRU等算法)
bool dirty; // 修改位
} TranslationEntry;
```
### 地址转换核心代码
```cpp
// Machine/translate.cc - translate()
// 将虚拟地址转换为物理地址
int Machine::Translate(int virtAddr, int *physAddr, int size, bool writing) {
// 1. 计算VPN和偏移
int vpn = virtAddr / PageSize;
int offset = virtAddr % PageSize;
// 2. 查找页表
TranslationEntry *entry = &pageTable[vpn];
// 3. 检查有效位
if (!entry->valid) {
// 缺页处理
return PageFaultException;
}
// 4. 检查写权限
if (writing && entry->readOnly) {
return ReadOnlyException;
}
// 5. 计算物理地址
*physAddr = entry->physicalPage * PageSize + offset;
// 6. 更新引用位和修改位
entry->use = true;
if (writing) entry->dirty = true;
return NoException;
}
```
### 内存分配示例
```cpp
// AddrSpace/addrspace.cc - 进程地址空间初始化
// 为进程分配物理页框
void AddrSpace::InitRegisters() {
// 初始化页表
pageTable = new TranslationEntry[numPages];
for (int i = 0; i < numPages; i++) {
pageTable[i].virtualPage = i;
pageTable[i].physicalPage = bitMap->Find(); // 位示图分配
pageTable[i].valid = true;
pageTable[i].readOnly = false;
pageTable[i].use = false;
pageTable[i].dirty = false;
}
}
```
---
## 十二、小结
```mermaid
mindmap
root((分页存储管理))
碎片问题
内碎片(固定分区)
外碎片(动态分区)
根因:连续存放
分页思想
虚拟页VP
物理页框PF
等大划分
地址结构
VPN+VPO
PPN+PPO
页内偏移不变
页表
VPN→PPN映射
有效位/Dirty/Ref
每进程独立
地址变换
PTBR+VPN查页表
取PPN拼PPO
缺页中断处理
TLB快表
高速缓存页表项
命中率影响EAT
多级页表
页目录+页表
节省内存
倒转页表
按物理块号索引
全系统一张
```
---
## 思考题
1. **概念理解**: 为什么分页能消除外碎片但不能消除内碎片?内碎片平均浪费多少?
2. **计算题**: 某系统页面大小 4KB虚拟地址 20 位,物理地址 18 位。
- 虚拟地址空间有多少页?
- 物理内存最大多少?
- 页表至少需要多少项?
3. **TLB计算**: 设 TLB 查找时间 5ns内存访问时间 80ns要求有效访问时间不超过 100nsTLB 命中率至少为多少?
4. **多级页表**: 对于 64 位系统,为什么至少需要 3 级页表?
---
## 关联笔记
- [[13_存储管理基础]] — 存储器层次结构与地址空间基础
- [[15_段式存储管理]] — 分段式地址转换,段页式结合
- [[16_虚拟存储器]] — 基于分页的虚拟存储器实现
- [[11_处理机调度]] — 进程调度与缺页处理的关系
---
**上一讲**: [[13_存储管理基础]]
**下一讲**: [[15_段式存储管理]]