# 纲要 > 主干纲要、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` 的物理内存。 #### 大页 > 也称 "巨页" ![image-20231001234622816|545](_attachment/02-开发笔记/05-操作系统/内存管理/虚拟内存管理.assets/IMG-虚拟内存管理-54F42357C739BB7A1040D119FF0E17D1.png) 传统物理页帧的大小通常为 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]: > > ![image-20231002104023616|557](_attachment/02-开发笔记/05-操作系统/内存管理/虚拟内存管理.assets/IMG-虚拟内存管理-683FB3FCA7046C096E64CB5A7C379DBA.png) > > 示例二: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] > > ![image-20231002110738891|500](_attachment/02-开发笔记/05-操作系统/内存管理/虚拟内存管理.assets/IMG-虚拟内存管理-9E6B586DF97F56A57104312B2B6DD077.png) > <br><br> # 进程的虚拟内存空间布局 略。参见[[02-开发笔记/03-计算机基础/机器视角下的进程#进程的虚拟内存空间 ⭐|机器视角下的进程]] %% ## 内核地址空间的地址映射 操作系统内核自身使用的地址空间也称为**内核地址空间**。 操作系统内核与用户进程一样采用 **多级页表结构** 来保存虚拟和物理地址的映射,但地址翻译的方式不同。 操作系统采用的地址映射方式为 **直接映射** :虚拟地址 = 物理地址 + 固定偏移。 在 Linux 内核中,内核使用固定的虚拟地址范围来直接映射几乎所有的物理内存, 地址映射为 **直接映射** / **线性映射** :虚拟地址 = **物理地址 + 固定偏移**。 例如,在 x86-64 架构上,虚拟地址从 `PAGE_OFFSET` 开始的范围直接映射到物理内存。 在该映射方式下,OS 需要访问一个物理地址时,只需访问该物理地址+固定偏移值得到的虚拟地址即可。 ![image-20231002100316368](_attachment/02-开发笔记/05-操作系统/内存管理/虚拟内存管理.assets/IMG-虚拟内存管理-9EF15AAFDE7A13FA44753712B85AAA34.png) %% <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)