# 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["访问页表
(页表基址寄存器PTBR + VPN × PTE大小)"] E --> F{"页表项有效位?"} F -->|"V=1"| G["取出PPN"] F -->|"V=0"| H["**缺页中断**
操作系统处理"] 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["终止进程
(非法访问)"] D -->|"有空闲"| F["从外存读入该页"] D -->|"无空闲"| G["执行页面置换算法
选择牺牲页"] G --> H{"牺牲页Dirty=1?"} H -->|"是"| I["将牺牲页写回外存"] H -->|"否"| J["直接覆盖"] I --> F J --> F F --> K["修改页表
设置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
(快表)"} TLB -->|"命中
取出PPN"| PA["物理地址"] TLB -->|"未命中"| PT["查页表
(内存)"] 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
(页目录基址)"] --> PD["页目录
(1024项)"] PD -->|"页目录项"| PT1["页表1
(1024项)"] PD -->|"页目录项"| PT2["页表2
(1024项)"] PD -->|"页目录项"| PT3["页表3
(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,要求有效访问时间不超过 100ns,TLB 命中率至少为多少? 4. **多级页表**: 对于 64 位系统,为什么至少需要 3 级页表? --- ## 关联笔记 - [[13_存储管理基础]] — 存储器层次结构与地址空间基础 - [[15_段式存储管理]] — 分段式地址转换,段页式结合 - [[16_虚拟存储器]] — 基于分页的虚拟存储器实现 - [[11_处理机调度]] — 进程调度与缺页处理的关系 --- **上一讲**: [[13_存储管理基础]] **下一讲**: [[15_段式存储管理]]