# 纲要
> 主干纲要、Hint/线索/路标
- **虚拟内存**
- **地址翻译**——**虚拟地址到物理地址**的转换
- **虚拟地址与物理地址**间的映射机制
- 分段机制
- 分页机制
- 单级页表
- 多级页表
- **延迟映射**
- **缺页异常**
%%
# Q&A
#### 已明确
#### 待明确
> 当下仍存有的疑惑
**❓<font color="#c0504d"> 有什么问题?</font>**
# Buffer
## 闪念
> sudden idea
## 候选资料
> Read it later
%%
<br>
# 内存抽象——虚拟内存
> ❓ <font color="#c00000">为什么需要 "虚拟内存"?</font>
## 物理内存
物理内存相当于一个 "**字节数组**",每个字节对应一个 "**物理内存地址**",**CPU 只会根据 "==物理内存地址==" 进行寻址和访问**。
如果让进程直接使用 "**绝对的物理内存地址**",则缺点是[^1]:
- **==难以同时运行多个程序==**
- 1)要么只能每次将一个程序装载到内存,切换至另一程序运行时,将前者的内存内容拷贝到磁盘并加载后者到内存;
- 2)要么内存中同时保留多个进程信息,但**由于进程在编译时**确定了 "**绝对内存地址**"(例如从 0 起),因此将其加载到内存时需要 "重定位"
- **==缺少隔离==**——不同进程**可访问其他进程占用的物理内存空间**,可能意外地**重叠写入导致错误**。
为此,操作系统对 "**物理内存**" 进行了抽象——"**==虚拟地址空间==**"。
<br>
## 虚拟内存(虚拟地址空间)
操作系统为**每个进程抽象了一个 ==独立==的 "虚拟地址空间",各个进程在自己的虚拟地址空间中进行寻址,不同进程间的地址空间彼此==隔离==**。
在虚拟内存机制下:
- **虚拟内存与物理内存解耦**
- 每个进程所见的 "虚拟内存" 均是一个**巨大、==连续的==字节数组**(从虚拟内存地址 0 开始),其与 "**物理内存**" 相分离,可大于实际的"物理内存"。
- "**连续的虚拟地址空间**" 可以映射到 "**多块不连续的物理内存区域**"。
- **物理内存对进程 "透明"**
- 进程中只根据 "**虚拟地址**" 进行寻址访问,**由操作系统和 CPU 将虚拟地址转换为实际的 "物理地址"**,而后 **CPU 再根据物理地址来访问物理内存**。
- **各进程的虚拟地址空间==彼此隔离==**
- 每个进程的内存空间互不影响,操作系统确保一个进程不能访问另一个进程的内存,防止安全漏洞和崩溃。
> [!NOTE] 虚拟内存空间大小
>
> - 32 位系统下,地址为 32 位,理论虚拟地址空间为 $[0,2^{32})$,理论虚拟内存空间大小为 4GB
> - 64 位系统下,地址为 64 位,理论虚拟地址空间是 $[0,2^{64})$,理论虚拟内存空间大小为 16EB。
> - 实际受 CPU 架构限制,现代主流采用的是 **48 位虚拟地址空间**(高位取符号扩展),故虚拟内存空间大小为 **256 TB**。
>
<br>
#### 使用虚拟内存的好处
- **进程可以使用连续的地址空间**
- **允许应用程序使用的内存大小==超过物理内存的容量限制==**,OS 可将不常使用的虚拟内存区域**置换到硬盘上的 swap 区域**
- **权限控制**——OS 可为不同的虚拟页设置不同的访问权限
- **不同进程的内存空间实现相互隔离**,防止重叠、意外或错误写入;
- 支持**内存共享**
- **共享内存**:多个进程间可共享同一块内存数据,即多个虚拟内存地址实际上**映射到相同的物理内存地址**,由此实现进程间高效通信。
- **内存映射(Memory Mapping)**:操作系统允许进程将文件映射到它们的地址空间,从而直接通过内存操作文件数据,提升文件 I/O 效率。
<br><br><br>
# 地址翻译——虚拟地址到物理地址的转换
> "**地址翻译**" 也称 "**地址转换**" (Address Translation),指**虚拟地址**到**物理地址**的转换过程。
**地址翻译**由 **==CPU==** 和 **==操作系统==** 协同完成[^2]:
- 操作系统负责 **配置 ==虚拟地址与物理地址之间的具体映射机制==**——**分段 or 分页**
- CPU 硬件提供的支持:
- **==内存管理单元 MMU==** 负责 **完成虚拟地址到物理地址的转换**。
- **页表基址寄存器**:保存进程页表的物理起始地址
- **界限寄存器**:检查地址合法性
> [!NOTE] "**虚拟地址**" 到 "**物理地址**" 的转换
>
> CPU 访问内存单元时,必须向内存提供 **内存单元的物理地址**。因此 CPU 通过地址总线送入存储器的是一个内存单元的**物理地址**。
>
> 在 CPU 内部需要执行从 "**虚拟地址**" 到 "**物理地址**" 的转换,
> 这一过程由 **CPU 中的==内存管理单元 MMU==** 以及 **==转址缓存部件 TLB==** 完成。 ^4rm5ce
>
> ![[_attachment/02-开发笔记/05-操作系统/内存管理/虚拟内存管理.assets/IMG-虚拟内存管理-D70D31FB67A8C95AC5F48FD6DDB86D27.png|510]]
>
<br>
## 内存管理单元 MMU
> - **内存管理单元**(Memory Management Unit,**MMU**)
> - **地址转换旁路缓冲** / **转址旁路缓存**(Translation-lookaside buffer, **TLB**)
MMU 负责完成**虚拟内存地址**到**物理内存地址**的转换,以及内存保护和缓存控制。
MMU 内部包含有硬件单元 "**转址旁路缓存 TLB**" ,用以**加速地址翻译**——TLB 缓存了 **"==虚拟页号=>物理帧号==" 键值对**。
对于每次内存访问,MMU 首先检查 TLB,**若命中则直接获取到物理帧号完成转换**,而 **不用访问页表**(减少一次额外的内存访问),
若 TLB 未命中,则意味着 **至少一次==额外的的内存访问==**——访问页表获取 "页号->帧号" 映射,而后更新到 TLB 中进行缓存。
如下图所示[^3]:
![[_attachment/02-开发笔记/05-操作系统/内存管理/虚拟内存管理.assets/IMG-虚拟内存管理-7BB4427BABC00B3E78E41089AA4E758E.png|440]]
> [!info] TLB 缓存的内容
>
> TLB 中每条记录缓存的是 "**页表项**",包括字段:
>
> **虚拟页号**、**物理页帧号**、控制位(Valid、Dirty、Access、权限位)。
<br><br>
# 虚拟地址与物理地址间的映射
操作系统**实现 "虚拟地址" 与 "物理地址" 间映射**的机制有两种:
- **==分段==机制 Segmentation**(早期使用)
- 基于**程序中的 "不同段(代码段、数据段)" 进行内存分配与管理**,**每个段具有不同的长度**。
- 分段可能导致 "**外部内存碎片**。
- **==分页==机制 Paging**(现代采用)
- 基于**固定大小的页**进行内存管理,**物理页和虚拟页都是同样的大小**。
- 分页可能导致 "**内部内存碎片**"。
**分段机制由于其固有的问题,如外部碎片,未能像分页那样广泛地应用于现代系统**。
x86 架构同时支持分页和分段,但其中**分段通常仅仅是为了向后兼容**,而实际的内存管理**主要依赖于分页机制**。
<br><br><br>
# 分段机制
分段机制下,OS 以 "**==段==**" (一块连续物理内存)为单位管理/分配内存。
**OS 根据进程虚拟内存空间中的段划分(代码段、数据段、堆、栈),将每个段独立地映射到一个 "==等长的物理内存段=="**。
## 段表
OS 在内存中保存着一张 "**==段表==**"(也称 "**段描述符表**",GDT/LDT),每个**段表项**记录一个 **"虚拟内存段" 所对应的 "物理内存段"** 的相关信息:
- **段基址**:该段在 "**物理内存**" 中的基准地址;
- **段界限**:该**段的长度**,用以确定段所占的内存范围。
- 段权限:可读、可写等;
- 段的特权级(DPL)
> [!NOTE] 虚拟地址段 => 物理地址段的映射
> ![[_attachment/02-开发笔记/05-操作系统/内存管理/虚拟内存管理.assets/IMG-虚拟内存管理-D33935468DA361E4D569D7A697EFA85F.png|491]]
> [!info] 关于 "段错误" [^4]
>
> "段错误" 是指在分段机制下发生了 "**==非法内存访问==**"——即地址翻译时,检查到 **"段偏移" 超出段界限**,从而触发 "异常"(trap)。
>
> 现代系统中普遍采用分页机制,但仍然保留了这一术语,标识非法内存访问。
>
> ^nsxzgq
<br>
## 分段机制下的虚拟地址表示
分段机制下,每个 "**虚拟地址**" 解释为两部分:虚拟地址 = **<==段选择子==>** **<==段内偏移==>**。
其中,**段选择子**(Segment Selector)取自 **虚拟地址高位部分**,标识 **该虚拟地址所在的段**,而剩余部分则作为 "**段内偏移**"。
> [!example] 一个简化的 "虚拟地址" 表示示意[^4]
> ![[_attachment/02-开发笔记/05-操作系统/内存管理/虚拟内存管理.assets/IMG-虚拟内存管理-E197532D232B15645971606691044916.png|449]]
<br>
## 分段机制下的地址翻译过程
一个虚拟地址中的 "**段选择子**" 部分给出了 "**==段号==**",即为**段表中一个段描述符的索引**。
MMU 根据 "段号" 查找段表,获取该段的 "**段基址**" 与 "**段界限**",从而完成地址翻译——物理地址 = **==段基址==** + **==段内偏移==**。
下图参见 [^5]
![[_attachment/02-开发笔记/05-操作系统/内存管理/虚拟内存管理.assets/IMG-虚拟内存管理-BB5358FE78BBD5C3C64F99EC9BCA577B.png|686]]
## 分段机制的缺点
1. **外部内存碎片问题**
2. **内存交换的效率低**(因为每次需要交换**一整个连续段**)
由于**每个段的长度不同**,故多次进行内存分配和回收后,可能存在显著的**外部内存碎片**问题——多个离散的不连续小内存块。
为解决外部内存碎片问题,需要进行 "**==内存交换==**"(swap) ——将进程占用的内存写到硬盘上,再重新从硬盘写回内存并调整装载位置,实现合并 "**空闲内存块**"。在此过程中,由于需要交换的是 "**一整个连续段**",因此交换效率很低(访问硬盘的速度远慢于内存)。
<br><br><br>
# 分页机制
分页机制将虚拟内存和物理内存均划分成 **==连续的==**、**==等长的页==**,**虚拟页和物理页的页长默认固定且相等**。
OS 在内存中为 "**==每个进程==**" 分别保存了一张 "**==页表==**"(per-process),记录着**进程虚拟地址空间中每个虚拟页到物理页的映射**。
进程的页表的 **==物理起始地址==** 存放于 CPU 的 "**页表基址寄存器**"中,当发生 "**进程上下文切换**" 时,通过切换页表基址寄存器中存放的 **页表起始地址** 即可完成**不同进程的虚拟地址空间切换**。
> [!info] 物理页也称 "页帧"(frame),虚拟页也称 "页面"(page)[^6]
> [!info] Linux 下,每个页的大小默认为 `4KB` 。
> ![[_attachment/02-开发笔记/05-操作系统/内存管理/虚拟内存管理.assets/IMG-虚拟内存管理-314810A3968E17C81995D0C95E2C658A.png|341]]
> [!info] 在 x86-64 下,每个页表项的大小是 "8 字节";x86 下,页表项是 4 字节。
<br>
## 分页机制下的虚拟地址表示
分页机制下,每个 "**虚拟地址**" 解释为两部分:虚拟地址 = **<==虚拟页面号==>** **<==页内偏移==>**。
- **虚拟页面号**(Virtual Page Number,VPN):地址的**高位部分**
- **页内偏移**:地址的**低位部分**,位数取决于**页大小**,例如 Linux 中一个页默认为 4KB,则**页内偏移量占低 12 位**,$2^{12}=4K$。
在地址翻译时,页内偏移不变,仅需**查询页表 or TLB**,替换地址高位的 "**虚拟页号 VPN**" =>"**物理帧号 PFN**",即得到物理地址。
> [!NOTE] 虚拟地址 => 物理地址
>
> 下图为一个简化的示意:
>
> ![[_attachment/02-开发笔记/05-操作系统/内存管理/虚拟内存管理.assets/IMG-虚拟内存管理-82FC60EDE7E0BFD88C1238F33C46B6BB.png|268]]
>
<br>
## 单级页表
单级页表机制[^5] [^7]:
- 页表为 "**==线性页表==**",存储 **进程整个虚拟地址空间中所有 "虚拟页" => "物理页帧" 的映射**。
- 虚拟地址的高位部分作为 "**虚拟页面号**",用作页表中 **"页表项的索引下标"**。
![[_attachment/02-开发笔记/05-操作系统/内存管理/虚拟内存管理.assets/IMG-虚拟内存管理-629776D096BEF9674B20CE657F6524BC.png|688]]
### 单级页表下的页表项
> 页表项(Page Table Entry,**PTE**)
单级页表下,每个页表项存储着该 "虚拟页" 对应的 "**==物理页帧号 PFN==**" 以及一些虚拟页的**标志位**。
> [!NOTE] 页表项中的标志位
>
> - **存在位**(Present,P):该页是否在**物理内存 RAW**中
> - **读写位**(R/W):该页是否可读 or 可写
> - **保护位**(SUP):是否需要**内核模式**访问
> - **脏位**(Dirty):标记该页是否**被修改过**
> - **访问位**(Accessed):标识该页是否被访问过,用以追踪 "热页"
> - **巨页位**(PageSize, PS):为 1 时,表示该页表项指向的是一个巨页(Huge Page)
> - ...
> ^udccry
<br>
### 单级页表下的地址翻译过程
进程页表的 **物理起始地址** 存储于 CPU 的 "**==页表基地址寄存器==**" 中,虚拟地址中的 "**虚拟页号**" 作为页表项的索引,
故给定**虚拟地址**:
1. 计算 **==页表项物理地址==**:由 "**页表基址**" 加上虚拟页号偏移得到
- `页表项物理地址 = 页表基址 + 虚拟页号VPN * sizeof(页表项)`
2. 访问页表项,获取得到 "**虚拟页**" 对应的 "**==物理帧号==**"
3. 用 "**物理帧号**" 替换虚拟地址中的 "**虚拟页号**" 部分,转换得到**物理地址**。
![[_attachment/02-开发笔记/05-操作系统/内存管理/虚拟内存管理.assets/IMG-虚拟内存管理-1E62647060095F5C4FF346559961FC6E.png|564]]
### 单级页表的缺点
1. **==页表占用内存过大==**
- 例如,32 位系统共 4GB 的虚拟内存空间,页大小为 4KB,故共需 $2^{32}/2^{10}=2^{20}$ 个页表项,而每个页表项为 4 字节,故**单个页表需占 4MB 内存**。若系统上同时运行 100 个进程,则**所有页表将占用 400MB 内存**。
- 同时,进程虚拟内存空间里的**大部分内存地址都未使用**,即 **==页表项为 invalid==**,**存储大量 invalid 页表项浪费了空间**。
2. **整个页表在物理内存中必须==占据连续内存空间==**
- 线性页表在物理内存中占据一整块连续空间,地址转换是基于 "**页表基地址**"(物理地址)+ "**虚拟页号索引**" 得到的。
3. **TLB 未命中概率较大**
- 单个页表仅 4KB,覆盖的地址范围小,故在 **TLB 缓存相同数量条目** 的情况下,覆盖的内存范围小,**TLB 未命中概率会较大**。
为此,有两方面改进:
1. **采用==多级页表==**
2. **引入==大页==**
> [!NOTE] "单级页表" 与 "多级页表" 的对比示意
>
> PFN 为 "**物理页帧号**"(Page Frame Number),PTBR 为 "**页表基址寄存器**",PDBR 为 "**页目录基址寄存器**"。
>
> ![[_attachment/02-开发笔记/05-操作系统/内存管理/虚拟内存管理.assets/IMG-虚拟内存管理-F1E40F7E6DF551651FEF054403A90B97.png|726]]
>
<br><br><br>
## 多级页表 ⭐
> 多级页表(Multi-Level Page Table)
多级页表[^8] 引入了 "**页目录**" 和 "**多级索引**" 的思想,采用类似树形的分层方式**组织整个页表结构**:
1. 将 **整张页表** 拆分成多个 "**==页大小==**" 的页表单元,**每个单元存储于一个==离散的物理页帧==,无需连续**。
2. 引入 "**==页目录==**",每个页目录同为 "**页大小**" 的单元,其中每个"**页目录项**" 存放 **"==下一级页目录==" 或 "==页表小单元==" 的 ==物理起始地址==**。
3. 对于 **==k 级页表==**,将虚拟地址中的 "**虚拟页号**" 拆分为 **==k 个部分==**——依次作为 **"==各级页目录==" & "==页表小单元==" 中的索引号**。
4. CPU 的 "**页表基地址寄存器**" 中存储 "**==最顶级页目录==**" 的物理起始地址。
> [!info] 部分资料未用 "页目录" 称呼,而是统称为 "x 级页表"——0 级、1 级、2 级、3 级页表....
>
> 总之,多级页表机制下:
>
> 1. 前面各级页表中, "页表项" 存的都是 "**下一级页表的物理地址**"。
> 2. "**最后一级页表**"中,"页表项" 存的是 "**虚拟页**" 对应的 "**物理页帧号**"。
>
<br>
### 多级页表下的页表项
以 x86-64 为例,4 级页表下,各页表层级中的表项如下:
| 缩写 | 全称 | 所在层级 | 作用 |
| ----- | ---------------------------------- | ---------- | --------------------------- |
| PTE | Page Table Entry | 第 4 级(最底层) | 映射 4KB 的页 |
| PDE | Page Directory Entry | 第 3 级(页目录) | 映射 2MB 巨页 or 指向**底层页表** |
| PDPTE | Page Directory Pointer Table Entry | 第 2 级 | 映射 1GB 巨页 or 指向**下一层页目录** |
| PML4E | Page Map Level 4 Entry | 第 1 级(顶层) | 映射 512GB 巨页 or 指向**下一层页目录** |
从 "功能" 上划分,页表项可分为三种:
| 页表项的名称 | 说明 |
| ------------ | --------------------------------------------------------- |
| **页描述符**<br> | **最后一级页表**中的页表项,存储虚拟页对应的 **==物理页帧号==**,以及虚拟页相关的 "**标志位**" |
| **表描述符** | **页目录** 中的页表项,存储着 **==下一级页表的物理起始地址==** |
| **块描述符** | **页目录** 中直接指向 "**==物理页==**"的页表项,所指物理页称之为 "**大页/超页/巨页**" |
![[02-开发笔记/05-操作系统/内存管理/虚拟内存管理#^udccry]]
> [!NOTE] 在 64 位架构中,上一级**页表项**所能覆盖的物理页大小,为下一级页表项向能够覆盖的**物理页大小**的**512 倍**(不考虑 "大页")
>
> 以 x86-64 下 Linux 系统为例,一个页大小默认为 4KB,一个页表项为 8 字节,故一个页表中共有 $4KB/8=512$ 个页表项。
>
> 在不考虑 "大页" 的情况下:
> - 最后一级页表中每一项映射到一个 4KB 的物理页;
> - 在倒数第二级页表中每一项则间接映射到 `4KB * 512 = 2MB` 的物理内存;
> - 在倒数第三级页表中每一项则间接映射到 `2MB * 512 = 1GB` 的物理内存。
> - 在倒数第四级页表中每一项则间接映射到 `1GB * 512 = 512GB` 的物理内存。
#### 大页
> 也称 "巨页"

传统物理页帧的大小通常为 4KB,大页则可以为 2MB、1GB、512GB 等,具体大小取决于硬件和 OS。
使用大页时,单个页表项**对应的==物理内存区域==更大**,可减少页表项数量,具有优点:
1. **减少 TLB 未命中概率**;
2. 减少地址翻译时所需的**页表遍历次数**。
由此,大页通常用于需要**大量连续内存访问**以及**高性能内存访问**的场景。
> [!NOTE] OS 通常提供了方法来启用或配置大页的使用
>
> 在 Linux 中,可以用 "Transparent Huge Pages" 或配置 `/proc/sys/vm/nr_hugepages` 来管理巨页的数量。
>
> [!NOTE] 巨页的标识
>
> MMU 地址翻译过程中,硬件通过"**页表项的 ==PS 标志位==**"(Page Size) 识别当前页是否为巨页,并据此决定**页内偏移位数**和**页表解析深度**。
>
> 例如,若在某一级页目录的页表项的 `PS=1`,则意味着页表项中存储的是 "**巨页的物理页帧号**",而虚拟地址中后续位均为 "**页内偏移**" 。
>
> 64 位系统下:
>
> - 对于 4KB 页,页内偏移为 12 位,MMU 查找到 **PTE 层**(4 级页表页)获取物理页帧号。
> - 对于 2MB 巨页,**页内偏移为 21 位**,MMU 查找到 **PDE 层**(3 级页目录)终止;
> - 对于 1GB 巨页,**页内偏移为 30 位**,MMU 查找到 **PDPTE 层**(2 级页目录)终止。
>
> [!example] 示例:巨页可减少 TLB 未命中率
>
> TLB 条目数量固定时,缓存 "巨页" 的页表项,**可覆盖的内存区域更大**。
>
> 假设 TLB 只有 128 个 Entry,则:
>
> - 缓存 4KB 页的映射,可覆盖 128 × 4KB = **512KB** 的内存;
> - 缓存 2MB 巨页,可覆盖 128 x 2MB = 256MB 的内存。
>
<br>
### 多级页表下的虚拟地址结构
k 级页表下,虚拟地址中的 "**==虚拟页号==**"(VPN)分成 **k 个部分**,依次为**各级页目录 & 页表小单元中的索引** [^5]。
![[_attachment/02-开发笔记/05-操作系统/内存管理/虚拟内存管理.assets/IMG-虚拟内存管理-FD92557E42ED7861048686BD669F789E.png|704]]
> [!NOTE] 下图以 “**三级页表**” 为例,虚拟地址总长 30 位[^9]:
>
> - **虚拟页号** 占 21 位,分成 **三个部分**——0 级 & 1 级页目录索引号、最后一级页表的索引号,分别各占 7 位
> - **页内偏移** 占 9 位。
>
> ![[_attachment/02-开发笔记/05-操作系统/内存管理/虚拟内存管理.assets/IMG-虚拟内存管理-2F4BDD309039D6719982EC9B1C528B07.png|610]]
> 由此:
>
> - 根据页内偏移的位数可推得,该示例中假设 "单个页大小是 512 字节",
> - 根据各级页表的索引位数可推得,单张页目录/页表中共 128 个页表项,故每个页表项大小为 512B / 128 = 4 字节。
<br>
### 多级页表下的地址翻译过程
在 **k 级页表**机制下,地址转换过程 **至少涉及 ==k 次内存访问==**(无 TLB 缓存时):
1. 从 CPU 页表基址寄存器中,获取 **0 级页目录的物理地址**。
2. 从虚拟地址高位中的 "**虚拟页号**" 提取得到 **0 级页目录中的索引值**,访问 **==0 级页目录的物理页帧==**(计 1 次),获取 **1 级页目录的物理地址**。
3. 类似的,从虚拟页号中提取得到 **1 级页目录中的索引值**,访问 **==1 级页目录的物理页帧==**(计 1 次),获取 **2 级页目录的物理地址**。
4. ...依次类推,最后访问**第 k 级**页表时,获取虚拟地址对应的 "**==物理页帧号==**"。
> [!NOTE]
>
> 示例一:48 位地址,"四级页表" 机制下的地址翻译过程[^10]:
>
> 
>
> 示例二:32 位地址,"三级页表" 机制下的地址翻译过程[^5]:
>
> ![[_attachment/02-开发笔记/05-操作系统/内存管理/虚拟内存管理.assets/IMG-虚拟内存管理-268E9C209D5857F4F64A11ECBE84FEDD.png|564]]
>
<br>
<br>
### 多级页表的优缺点
- 优点:**节省内存空间** & **无需整张表占用连续物理内存**
- 缺点:地址翻译过程中需要**更多的内存访问次数**;
##### 优点一——节省内存空间
相较于单级页表,多级页表将**整张 "线性页表"** 拆分成多个 "**页大小**" 的页表小单元,且额外引入了 "**页目录**",初看反而增加了内存空间占用。
其能够节省空间的原因在于:**==多级页表中允许存在 "空洞"== & ==延迟映射==**
- 单级页表中,OS 需要为进程创建 **整张线性页表** 并分配物理内存。
- 多级页表中:
- OS 初始时只需为进程创建 "**==0 级页表==**"(页目录),**仅占一个页的大小**;
- 后续仅**当进程需要实际访问某一页**时,才**为该页分配物理页帧**,并**在页表中添加相应的映射**。
> [!NOTE] 内存空洞示例[^11]
>
> 下图以二级页表为例,图中蓝色页表项为**空项**——**未指向物理页帧**。
> 表现到整个 "**虚拟内存空间**" 上,就形成了绿色部分的 "**虚拟内存空洞**"。
>
> ![[_attachment/02-开发笔记/05-操作系统/内存管理/虚拟内存管理.assets/IMG-虚拟内存管理-55E17247243844C560D210DD90958A6D.png|582]]
>
> [!faq] "多级页表" 相较于 "单级页表" 能够减少页表占用空间的前提
>
> 这一前提是 "**进程实际使用的虚拟地址远小于总的虚拟地址空间**",使得多级页表可存在空洞,从而减少页表的物理内存占用。
> 如果虚拟地址空间用满,则 **多级页表中最末级的页表** 和 **单级页表机制** 占用内存大小是一样的,此时**前面几级页目录反而占用了额外空间**。
>
##### 优点二——内存占用不连续
- 单级页表为线性页表,要求**整张页表占用连续的物理页**。
- 多级页表中,每个页目录或页表小单元占用的 "**物理页帧**" 不要求连续,可离散分布。
<br><br>
# 延迟映射
> 延迟映射(Lazy Mapping of Page Tables)[^12]
延迟映射将 "**虚拟内存的分配**" 与 "**物理内存的分配**" 解耦。
进程启动时,内核仅为其 **创建 "==0 级页表页=="(页目录)并分配物理页帧**,但不会为其中各个页目录项指向的 "**次级页表页**" 分配 "**物理页帧**"——即仅存在页表项,而**未填充"页表映射"**。仅**当进程需要实际访问某一页**时,才**为该页分配物理页帧**,并**在页表中添加相应的映射**。
### 实现方式
当进程**首次实际访问某个虚拟页**时,由于页表中没有映射(标志位为**无效**),CPU 触发 **==缺页异常==**。
缺页异常处理程序检查 "**目标地址**" 是否属于某个 "**==已分配的==虚拟内存区域**",并且**检查读、写、执行权限是否匹配**。
若检查通过(**合法的缺页异常**),则**为该虚拟页分配物理页**,并**在页表中添加映射**,然后重新运行触发异常的指令。
(系统调用结束后的返回行为是**重新执行触发缺页异常的指令**)
### OS 为进程分配虚拟内存区域的时机
- OS 在创建进程时为其**创建若干虚拟内存区域**,分别对应代码段、数据段、栈等,并且进程初始页表中仅包含**0 级页表页**。进程运行过程中,通过延迟映射机制来实际分配物理内存页(包括分配物理页作为页表页)并在页表中添加对应映射。
- 当进程申请**动态添加虚拟内存区域**时(例如调用 `brk` 或 `mmap`),同样采样延迟映射,只**分配一段虚拟内存区域**(在进程的 `vm_area_struct` 结构体链表中插入一个新项代表新分配的内存区域),等到**实际访问而触发缺页异常时才分配物理页并添加页表映射**。
# 缺页异常
> 缺页异常(Page Fault)
指 **进程访问某个虚拟地址**,而该地址对应的 "**页" 并没有在物理内存中**,导致 CPU 触发 "**缺页异常**" 而陷入内核态,交由**缺页异常处理程序**响应[^13]。
缺页异常包括以下几种触发情况:
| 缺页异常的触发情况 | 缺页处理方式 |
| ------------------------------ | ------------------------------------- |
| **物理页帧 ==尚未分配==**(延迟映射机制) | 内核分配一个物理页,更新页表映射,返回重新执行指令 |
| **物理页被 ==swap out==** | swap in,从磁盘上将该页重新读入内存,更新页表映射,返回重新执行指令 |
| **只读页被 "写入"**(触发 **==写时拷贝==**) | 内核分配一个新物理页,拷贝原内容 & 更新页表标志位为可读写 |
| **==非法地址访问==**(段错误) | 终止进程(发送 **SIGSEGV 信号**),报告段错误 |
> [!example] 物理页被置换到磁盘 swap 区而导致的缺页异常
> ![[_attachment/02-开发笔记/05-操作系统/内存管理/虚拟内存管理.assets/IMG-虚拟内存管理-771FCA620DCEC0619D516C4EE9797093.png|513]]
![[02-开发笔记/03-计算机基础/机器视角下的进程#段错误 (Segmentation Fault)|机器视角下的进程]]
![[02-开发笔记/05-操作系统/内存管理/虚拟内存管理#^nsxzgq]]
<br><br><br>
# Swap 交换空间 & 页面置换
Swap(交换空间)是磁盘上的一块区域,用作 "**物理内存**" 的扩展,共同构成 "**虚拟内存空间**"。
任一时刻,每个进程仅有**部分页**需要驻留在物理内存中,这些页构成了 "**驻留集**",而未使用的页则保留在磁盘上的 "**==交换区==**" 中。
虚拟内存机制下允许 **"进程们实际占用的虚拟内存总和"** 大于 "**物理内存上限**",通过 "**==页面置换==**"(Page Swapping)机制实现:
- **Page out**:当物理内存不够用时,**OS 会将物理内存中 "部分不活跃的页" 拷贝写入到磁盘上的 swap 空间中**,标记,从而腾出物理页帧。
- **Page in**:当**某个已被换出的页**需被访问时,**OS 将其从 swap 区中重新读入内存**。
在磁盘与内存之间 "**传递页**" 的活动即称之为 "**交换/置换**(swap)" 或 "**页面调度**"。
> [!NOTE] 页面置换的过程 [^14]
>
> ![[_attachment/02-开发笔记/05-操作系统/内存管理/虚拟内存管理.assets/IMG-虚拟内存管理-77C36B30291268AB397F9BB6D53EFC68.png|541]]
>
> 1. 进程访问某虚拟地址,MMU 查页表项发现**无对应的物理页帧**,触发 "**缺页异常**";
> 2. **OS 定位到所需页面的磁盘位置**;
> 3. OS 尝试找出一个 "**空闲物理帧**":
> - 若有空闲帧,则直接使用;
> - 若无空闲帧,则通过 **==页面置换算法==**,选出一个 "**牺牲帧**";
> - 将 "**牺牲帧**" 的内容写入到**磁盘 swap 空间**中,标记其对应**页表项**为 "**无效**";
> 3. 将**所需页面**从磁盘读入 "**物理帧**",修改页表标记为 "有效"。
> 4. 重新执行触发 "**缺页错误**" 的指令.
>
> 选择哪个物理页帧作为 "**牺牲帧**" 而被置换,取决于 OS 的 "**==页面置换算法==**"。例如 LRU[^15],交换出最近最久未被访问的页。
<br><br><br>
# 内存碎片
内存碎片分为两种:
- **内部内存碎片**:**==分配的内存块大于进程请求的内存==**,导致未使用的内存浪费。
- **外部内存碎片**:多个空闲内存块**离散地分布在不同位置而不连续**,即使空闲内存总量够大,也**无法满足==大块连续内存==** 的分配需求。
> [!NOTE] "分段" 与 "分页" 机制内存碎片问题
>
> - 分段机制下,各个**段大小不同**,故会产生 "**外部内存碎片**";
> - 分页机制下,每个**页大小固定**,无外部碎片问题,但会产生 "**内部内存碎片**"
> - 原因:内存分配按 “**页**” 为单位进行,但进程所需内存可能并非页的整数倍,故最后一个页用不完。
> - 例如,系统按 4KB 的页分配内存,但进程只需要 3KB,剩下的 1KB 就是内部碎片。
>
> [!NOTE] 外部内存碎片示意[^5]
> ![[_attachment/02-开发笔记/05-操作系统/内存管理/虚拟内存管理.assets/IMG-虚拟内存管理-DF00F9C5E0E64C028F010E17FB96A155.png|567]]
<br><br><br>
# 进程结构体中的虚拟地址空间
Linux 内核视角下,每个进程由 **结构体 `task_struct`** 表示,其中 `mm` 字段指向一个 **==`mm_struct` 结构体==**,
该结构体描述了 "**进程的虚拟地址空间**",其中:
- `pgd` 字段存储着 "**该进程 ==0 级页目录的基址==**"。当该进程正运行于 CPU 上时,该地址会被存入寄存器中。
- `mmap` 字段指向一个 "**`vm_area_struct` 结构体构成的链表**",记录了 "**==已分配==给进程的虚拟内存区域**"。
如下图所示:
![[_attachment/Excalidraw/Excalidraw-Solution/os-进程基础概念.excalidraw.assets/IMG-os-进程基础概念.excalidraw-8CBB1FC91FD5B29453248DE825630574.png|612]]
> [!info] 每个 `vm_area_struct` (**VMA**)描述了 "**进程虚拟地址空间中的一个==已分配的内存区域==**" [^16]
>
> - `vm_start` 与 `vm_end` 指向区域起始/终止地址;
> - `vm_prot` 描述该区域包含的 "**所有页的读写权限**";
> - `vm_flags` 描述该区域内页面的标志,例如 "**进程间共享**" or "**独占**"。
> - `vm_next`:指向链表中下一区域结构。
>
> [!NOTE] `mmap()` 系统调用背后的实现即是 "为进程添加一个`vm_area_struct` 结构体,表明添加一段虚拟内存区域[^12]。
> ![[_attachment/02-开发笔记/05-操作系统/内存管理/虚拟内存管理.assets/IMG-虚拟内存管理-669F56952A25076447E5175C0C8FA9CA.png|351]]
> [!example] 进程结构体中的虚拟内存空间表示(非 Linux)[^17]
>
> 
>
<br><br>
# 进程的虚拟内存空间布局
略。参见[[02-开发笔记/03-计算机基础/机器视角下的进程#进程的虚拟内存空间 ⭐|机器视角下的进程]]
%%
## 内核地址空间的地址映射
操作系统内核自身使用的地址空间也称为**内核地址空间**。
操作系统内核与用户进程一样采用 **多级页表结构** 来保存虚拟和物理地址的映射,但地址翻译的方式不同。
操作系统采用的地址映射方式为 **直接映射** :虚拟地址 = 物理地址 + 固定偏移。
在 Linux 内核中,内核使用固定的虚拟地址范围来直接映射几乎所有的物理内存,
地址映射为 **直接映射** / **线性映射** :虚拟地址 = **物理地址 + 固定偏移**。
例如,在 x86-64 架构上,虚拟地址从 `PAGE_OFFSET` 开始的范围直接映射到物理内存。
在该映射方式下,OS 需要访问一个物理地址时,只需访问该物理地址+固定偏移值得到的虚拟地址即可。

%%
<br><br><br>
# 参考资料
- 《操作系统导论 OSTEP》:13~21 章
- 《深入理解计算机系统》:第 9 章
- 《操作系统:原理与实现》:第 4 章虚拟内存管理
# Footnotes
[^1]: 《现代操作系统》(P104)
[^2]: 《操作系统导论 OSTEP》(P105)
[^3]: 《操作系统概念》(P249)
[^4]: 《操作系统导论 OSTEP》(P113)
[^5]: [小林Coding](https://xiaolincoding.com/os/3_memory/vmem.html#%E5%86%85%E5%AD%98%E5%88%86%E6%AE%B5)
[^6]: 《操作系统概念》(P245)
[^7]: 《操作系统:原理与实现》(P86)
[^8]: 《操作系统导论 OSTEP》(P157)
[^9]: 《操作系统导论 OSTEP》(P161)
[^10]: 《操作系统:原理与实现》(P88)
[^11]: 《深入理解计算机系统》(P573)
[^12]: 《操作系统:原理与实现》(P105, P107)
[^13]: 《深入理解计算机系统》(P564)
[^14]: 《操作系统概念》(P273)
[^15]: 《操作系统概念》(P277)
[^16]: 《深入理解计算机系统》(P580)
[^17]: 《操作系统:原理与实现》(P104)