虚拟内存的运作

虚拟内存的运作 原文链接https://kidwjb.top/archives/196什么是虚拟内存在一个系统中的进程是与其他进程共享CPU和主存资源的如果太多的进程占用太多的内存那么部分进程就无法运行且内存容易遭到破坏。为了更加有效地管理内存并减少出错现代操作系统提出了虚拟内存的概念虚拟内存虚拟内存是硬件异常硬件地址翻译主存磁盘文件和内核软件的完美交互。它为每个进程提供了一个大的一致的且私用的地址空间虚拟内存提供三个重要的能力它将主存堪称是一个存储在硬盘磁盘上的地址空间的高速缓存在主存中只保存活动区域并格局需要在磁盘和主存之间来回传送数据这样以高效使用主存。虚拟内存 主存RAM 磁盘上的交换空间Swap/Pagefile。当 RAM 满了数据就去磁盘当 CPU 需要数据数据就从磁盘搬回 RAM它为进程提供了一致的地址空间从而简化了内存管理它保护了每个进程的地址空间不被其他进程破坏虚拟内存作为缓存的工具虚拟内存被组织为一个存放在磁盘上的N个连续字节大小单元组成的数组也就是虚拟内存给每个程序都画大饼一样告诉它它有一个巨大连续的内存空间可以用。每个字节都有一个唯一的虚拟地址作为到数组的索引磁盘上的数组被缓存在主存中即操作系统会把程序当前正在用的那部分数据从硬盘复制到快速的 RAM 里。在这里RAM 被当成了硬盘的高速缓存Cache。磁盘上的数据被分割成块这些块作为磁盘和主存之间的传输单元系统将虚拟内存分割成大小固定的块称为虚拟页VP物理内存被分割为物理页PP大小和虚拟页相同在任何时刻虚拟页面的集合都分为三个不相交的子集未分配的页操作系统还未分配或创建的页未分配的块没有任何数据和他们相关联所以也不占用任何磁盘空间缓存的页当前已缓存在物理内存中的已分配页未缓存的页未缓存在物理内存中的已分配的页相当于程序还未用到这部分数据DRAM缓存的组织结构在存储层次结构中主存DRAM其实是磁盘的缓存。它把磁盘上常用的数据缓存在自己这里供 CPU 快速访问。SRAM 缓存来表示位于 CPU 和主存之间的 L1、L2 和 L3 高速缓存。SRAM 速度极快但很贵所以容量小用来做 CPU 的高速缓存。命中命中 (Cache Hit)意思CPU 想要找的数据正好就在上一级更快的存储器里。结果CPU 直接拿到数据速度飞快。例子你想喝水水杯正好就在你手边SRAM你直接喝到了。这就是“SRAM 命中”。不命中 (Cache Miss)意思CPU 想要找的数据不在上一级更快的存储器里。结果CPU 必须去下一级更慢的存储器里找并且通常要把这一块数据搬运到上一级存储器里为了下次再用。这个过程需要等待会有“开销”或“惩罚”。例子你想喝水手边没水SRAM 不命中你得起身去厨房饮水机接水去 DRAM 找。DRAM 比 SRAM 慢约10倍磁盘比 DRAM 慢约100,000倍十万倍所以DRAM 不命中去读磁盘的代价太大了所以虚拟内存系统DRAM 缓存的设计必须非常小心虚拟页很大4KB~2MB既然去磁盘取一次那么慢那就一次多取点整个页利用局部性原理尽量让取回来的数据都能被用到。全相联映射任何虚拟页可以放在任何物理页位置灵活性高减少冲突。复杂的替换算法当内存满了要换出数据时必须精心挑选那个“最不可能再被用到”的页换出去否则如果刚换出去马上又要用那就得再去读磁盘代价太高了。写回Write-back修改数据时先只改内存等真的要被换出时才写回磁盘减少对磁盘的写入次数。页表页表是一个存放在物理内存中的数据结构页表将虚拟页映射到物理页每次地址翻译硬件将一个虚拟地址转换成物理地址时都会读取页表。操作系统负责维护页表的内容以及在磁盘中与DRAM之间来回传送页页表就是一个页表条目的数组Page Table EntryPTE虚拟地址空间中的每个页在页表中一个固定偏移量处都有一个PTE每个PTE由一个有效位和一个n位地址字段组成的。有效位表明了该虚拟页当前是否被缓存在DRAM内存中。如果设置了有效位为1那么地址字段就表示DRAM中相应的物理页起始地址这个物理页缓存了该虚拟页如果没有设置有效位即为0那么一个空地址表示这个虚拟页还未被分配如果有效位为0而地址字段不为空那么就说明这页虚拟页未被缓存还在磁盘中地址指向该虚拟页在磁盘上的起始地址页命中页命中指的是CPU想要读取某个虚拟内存中的某个数据时该数据有一个虚拟地址而该虚拟内存刚好被缓存在DRAM物理内存中。比如上图CPU想要读取VP2中的一个数据VP2被缓存在物理内存中。地址翻译硬件将虚拟地址作为一个索引来定位到PTE2而PTE2设置了有效位那么地址翻译硬件就知道VP2缓存在内存中所以它使用PTE中的物理内存地址也就是PP1的起始地址构造出这个数据的实际物理地址缺页DRAM真实内存缓存不命中就称为缺页。比如CPU要使用VP3中的一个数据但是VP3并没有缓存在DRAM中地址翻译硬件通过数据的虚拟地址作为索引定位到PTE3然后从有效位判断出VP3未被缓存并且会触发一个缺页异常缺页异常调用内核中的缺页异常处理程序该程序会选择一个牺牲页在这里就是存放在PP3中的VP4。如果VP4已经被修改了那么内核就会将它复制回磁盘。接下来内核从磁盘复制VP3到内存中的PP3更新PTE3然后返回。当异常处理程序返回时它会重新启动导致缺页的指令该指令会把导致缺页的虚拟地址重新发送到地址翻译硬件。但是现在VP3已经缓存在主存中了那么页命中也能由地址翻译硬件正常处理了。常用术语页面调度内存和磁盘之间传送页的活动按需页面调度页从磁盘换入页面调入DRAM和从DRAM换出页面调出磁盘后会一直等待直到最后时刻也就是当由不命中发生时才换入页面程序的局部性程序运行时有“局部性原理”这又分为时间局部性、空间局部性。时间局部性在某个时间点访问了存储器的特定位置很可能在一小段时间里会反复地访问这个位置。空间局部性访问了存储器的特定位置很可能在不久的将来访问它附近的位置。如果程序的活动页面超过了物理内存大小那么这个现象叫做抖动这时页面将不断地换进换出虚拟内存作为内存管理的工具操作系统为每一个进程都提供了一个单独的页表将每个进程独立的虚拟地址空间映射到物理地址空间。多个虚拟页面可以映射到同一个共享物理页面简化链接独立的地址空间允许每个进程的内存映像使用相同的基本格式而不管代码和数据实际存放在物理内存的何处简化加载虚拟内存还使得容易向内存中加载可执行文件和共享对象文件。简化共享独立地址空间为操作系统提供了一个管理用户进程和操作系统自身之间共享的一致机制。简化内存分配虚拟内存向用户进程提供一个简单的分配额外内存的机制。当一个运行在用户进程中的程序要求额外的堆空间时操作系统分配一个适当数字例如k个连续的虚拟内存页面并且将它们映射到物理内存中任意位置的k个任意的物理页面。由于页表的工作方式操作系统没有必要分配k个连续的物理内存页面页面可以随机地分散在物理内存中虚拟内存作为内存保护的工具操作系统不允许一个用户进程修改它的只读代码段不允许它读或修改任何内核中的代码和数据结构或是读写其他进程私有数据以及其他进程的共享虚拟页面使用地址翻译机制可以扩展得到更好的访问控制其大致思想如下每个PTE中允许添加三个许可位SUP位表示进程是否必须允许在内核超级用户模式下才能访问该页。运行在内核模式中的进程可以访问任何页面但运行在用户模式中的进程只允许访问那些SUP位0的页面READ和WRITE位控制对页面的读写访问如果一条指令违反了这些条件CPU就会触发一个一般保护故障将控制传递给内核中的异常处理程序。Linux shell一般将这种异常报告为段错误segmentation fault地址翻译基本参数符 号描 述N 2^n虚拟地址空间中的地址数量M 2^m物理地址空间中的地址数量P 2^p页的大小字节虚拟地址VA的组成部分符 号描 述VPO虚拟页面偏移量字节VPN虚拟页号TLBITLB 索引TLBTTLB 标记物理地址PA的组成部分符 号描 述PPO物理页面偏移量字节PPN物理页号CO缓冲块内的字节偏移量CI高速缓存索引CT高速缓存标记地址翻译是一个N元素的虚拟地址空间和一个M元素的物理地址空间之间的映射如图展示了MMU如何利用页表实现这种映射。CPU中一个控制寄存器页表基址寄存器PTBR指向当前页表。n位的虚拟地址包含两部分一个p位的虚拟页面偏移和一个n-p位的虚拟页号VPNMMU用VPN来选择适当的PTE将页表条目中物理页号和虚拟地址终端虚拟页偏移量VPO串联起来就得到了物理地址。注意因为物理和虚拟页面都是P字节所以VPO和PPO是相同的下图展示了页面命中和缺页时两种情况的地址翻译页面命中完全硬件处理处理器生成一个虚拟地址传递给MMUMMU生成PTE页表条目地址并从高速缓存或主存请求得到它告诉缓存或主存向MMU返回PTEMMU根据PTE进行地址翻译将得到的物理内存传送给告诉缓存或内存高速缓存或内存返回所请求的数据给处理器缺页硬件和操作系统内核协作处理器生成一个虚拟地址传递给MMUMMU生成PTE页表条目地址并从高速缓存或主存请求得到它告诉缓存或主存向MMU返回PTEPTE中有效位为0所以MMU触发一次异常传递给CPU中的控制到操作系统内核中的缺页异常处理程序缺页处理程序确定出物理内存中的牺牲页如果这个页面已经被修改了则把它换出磁盘缺页处理程序页面调入新的页面并更新内存中的PTE缺页处理程序返回到原来的进程再次执行导致缺页的指令。CPU将引起缺页的虚拟地址重新发送给MMU因为虚拟页面现在缓存在物理内存中所以就会命中然后MMU将物理地址给高速缓存或内存然后高速缓存或内存再把数据返回给CPU结合高速缓存在既使用虚拟内存又使用SRAM高速缓存的系统中大多数系统选择物理寻址TLB在上述所述中MMU每次接收到虚拟地址之后都需要先去查阅一个PTEA然后得到PTE后再翻译成物理地址给内存这种每次从内存多取一次内存需要几十到几百个周期而哪怕PTE刚好在告诉缓存中也需要12个周期对于很多系统试图消除这样的开销在MMU中包括了一个关于PTE的小缓存称为翻译后备缓冲器TLBTLB是一个小的虚拟寻址的缓存其中每一行都保存着一个由单个PTE组成的块。TLB用于组选择和行匹配的索引和标记字段是从虚拟地址中的虚拟页号VPN提取出来的TLB命中情况处理器产生一个虚拟地址给MMUMMU收到虚拟地址翻译成虚拟页号VPN然后去TLB寻找在TLB找到对应的页表条目PTE将这个虚拟地址翻译成物理地址发送给高速缓存/主存高速缓存/主存将数据返回给CPUTLB未命中情况MMU需要从高速缓存/主存取出相印的PTE并且新取出的PTE会存放在TLB中可能会覆盖掉一个已经存在的条目多级页表前面我们一直只用一个单独的页表进行地址翻译但是在实际情况中是使用的多级页表结构。如果二级页表中的每一片都没有被分配那么一级页表中指向这个二级页表的PTE就是空的这种多级页表方式从两个方面减少了内存要求如果一级页表中的一个PTE是空的那么相应的二级页表就根本不会存在这就是一种巨大的潜在节约因为对于一个典型的程序4GB的虚拟地址空间的大部分都是未分配的只有一级页表才总是需要在主存中虚拟内存系统可以在需要时创建页面调入调出二级页表减少主存压力。只有最经常用的二级页表才需要缓存在主存中在这里我曾经有些疑问一级页表PTE存储的是二级页表的地址那么二级页表不在内存中还是需要去磁盘访问二级页表不是吗那么不是每一次访问一级页表都需要访问二级页表最后找到简要答案是虚拟地址访问 ↓ 查一级页表必须在内存 ↓ 一级页表PTE → 指向二级页表的位置 ↓ ┌─────────────┬──────────────┐ │ 情况A │ 情况B │ │ 二级页表 │ 二级页表 │ │ 在内存 │ 不在内存 │ ├─────────────┼──────────────┤ │ 直接访问 │ 缺页异常 │ │ 二级页表 │ → 操作系统 │ │ │ 从磁盘加载 │ │ │ 二级页表 │ └─────────────┴──────────────┘ ↓ 查二级页表找到最终物理页面正常情况下第1次内存访问读一级页表第2次内存访问读二级页表第3次内存访问读实际数据这就是为什么需要TLB来缓存页表项避免每次都两次访问内存如果二级页表不在内存会产生缺页异常操作系统会先把二级页表从磁盘加载到内存然后再继续访问。ARM 架构内存映射简介ARM 架构支持一级页表映射也就是说MMU根据CPU发来的虚拟地址可以找到第1个页表从第1个页表里就可以知道这个虚拟地址对应的物理地址。一级页表里地址映射的最小单位是1M。ARM 架构还支持二级页表映射也就是说MMU根据CPU发来的虚拟地址先找到第1个页表从第1个页表里就可以知道第2级页表在哪里再取出第2级页表从第2个页表里才能确定这个虚拟地址对应的物理地址。二级页表地址映射的最小单位有4K、1KLinux使用4K。一级页表项里的内容决定了它是指向一块物理内存还是指问二级页表一级页表映射过程(arm32)一级页表中每一个表项用来设置1M的空间对于32位的系统虚拟地址空间有4G4G/1M4096。所以一级页表要映射整个4G空间的话需要4096个页表项。第0个页表项用来表示虚拟地址第0个1M(虚拟地址为00xFFFFF)对应 哪一块物理内存并且有一些权限设置第1个页表项用来表示虚拟地址第1个1M(虚拟地址为 0x100000 0x1FFFFF)对应哪一块物理内存并且有一些权限设置依次类推。使用一级页表时先在内存里设置好各个页表项然后把页表基地址告诉MMU 就可以启动MMU了。以下图为例介绍地址映射过程CPU发出虚拟地址vaddr假设为0x12345678MMU根据**vaddr[31:20]找到一级页表项**:虚拟地址0x12345678是虚拟地址空间里第0x123个1M所以找到页表里第0x123项根据此项内容知道它是一个段页表项。段内偏移是0x45678。从这个表项里取出物理基地址Section Base Address假设是0x81000000页表项中的物理地址Section Base Address存储多少位在 ARMv7 的一级页表段描述符Section Descriptor中物理基地址Section Base Address占据高 12 位bit[31:20]。因为段大小是1MB 2²⁰ 字节所以物理地址的低 20 位bit[19:0]总是 0不需要存储。因此页表项中只存储物理地址的 bit[31:20]共 12 位代表一个1MB 对齐的物理地址。举个例子如果物理基地址是0x81000000其二进制低 20 位全是 0因为 0x81000000 0b10000001000000000000000000000000那么页表项中存储的就是0x810即0x81000000 20放在描述符的 bit[31:20]位置。这种1MB 段映射是 ARM32 的一种快速、粗粒度的映射方式适用于内核或大块连续内存。如果需要 4KB 粒度则需使用两级页表页表 页目录此时一级页表项指向二级页表Page Table而不是直接指向物理段二级页表映射过程 (arm32)首先设置好一级页表、二级页表并且把一级页表的首地址告诉MMU。CPU发出虚拟地址vaddr假设为0x12345678MMU根据vaddr[31:20]找到一级页表项虚拟地址0x12345678是虚拟地址空间里第0x123个1M所以找到页表里第0x123项。根据此项内容知道它是一个二级页表项。从这个表项里取出地址假设是address这表示的是二级页表项的物理地址vaddr[19:12]表示的是二级页表项中的索引index即0x45在二级页表项中找到第0x45项二级页表项格式如下里面含有这4K或1K物理空间的基地址page base addr假设是 0x81889000它跟**vaddr[11:0]**组合得到物理地址0x81889000 0x678 0x81889678。所以CPU要访问虚拟地址0x12345678时实际上访问的是0x81889678的物理地址ARM64页表在ARM64AArch64架构中页表机制与 ARM32 有本质不同不再支持“段映射”Section Mapping而是统一使用多级页表通常为 4 级且页大小可配置常见为 4KB、16KB 或 64KB。下面以最常见的4KB 页面 4 级页表配置为例说明。一、ARM64 虚拟地址与页表层级4KB 页面虚拟地址48 位有效通常使用 48 位高位符号扩展格式如下4KB 页面| 63..48 | 47..39 | 38..30 | 29..21 | 20..12 | 11..0 | | 符号 | L0 | L1 | L2 | L3 | 页内偏移 |L0: 页全局目录PGD索引9 位L1: 页上级目录PUD索引9 位L2: 页中间目录PMD索引9 位L3: 页表PTE索引9 位页内偏移: 12 位因为 2¹² 4KB注意ARM64 支持“块映射”Block Mapping可在任意层级直接映射大块内存如 L2 映射 2MBL1 映射 1GB但底层仍是页表项结构不是 ARM32 的“段”。二、页表项PTE中的物理地址位数在 ARM64 中**每个页表项无论是哪一级都是 64 位8 字节**其中对于最后一级页表项L3即 PTE——映射 4KB 页面物理页帧号PFN存储在 bit[47:12]共 36 位因为页面对齐4KB 2¹²物理地址低 12 位为 0无需存储。所以页表项中存储的是物理地址的 bit[47:12]共36 位。最大支持2³⁶ × 4KB 256TB 物理内存实际受 SoC 地址总线限制。对于中间级页表项如 L2——若用作“块映射”2MB 块此时该表项不再指向下一级页表而是直接映射一个2MB 大页Block物理基地址存储在 bit[47:21]共 27 位因为 2MB 2²¹低 21 位为 0所以存储物理地址的 bit[47:21]共27 位对于 L1 表项——若用作“块映射”1GB 块物理基地址存储在bit[47:30]共 18 位1GB 2³⁰低 30 位为 0✅ 关键点ARM64 页表项中物理地址的位数 取决于映射粒度4KB 页面 → 存 36 位bit[47:12] 2MB 块 → 存 27 位bit[47:21] 1GB 块 → 存 18 位bit[47:30]Linux虚拟内存一个Linux虚拟内存结构如图内核虚拟内存包含内核中的代码和数据结构内核虚拟内存中的某些区域被映射到所有进程共享的物理页面例如每个进程共享内核的代码和全局数据结构内核虚拟内存的其他区域包含每个进程都不同的数据比如页表内核在进程上下文中执行代码时使用的栈以及记录虚拟地址空间当前组织的各种数据结构mm_struct结构体Linux将虚拟内存组织成一些区域也叫段的集合一个区域就是已经存在的已分配的虚拟内存的连续页。如上图中代码段数据段堆共享库段用户栈都是不同的区域每个存在的虚拟页面都保存在某个区域中不属于某个区域的虚拟页是不存在的不能被进程引用因为区域的概念所以允许虚拟地址空间有空隙内核不用记录哪些不存在的虚拟页并且这样的页不会占据内存或磁盘空间 每一个APP在内核里都有一个task_struct这个结构体中保存有内存信息mm_struct。而虚拟地址、物理地址的映射关系保存在页目录表中如下图所示mm_struct中的pgd指向第一级页表页表全局目录的基址而mmap指向一个vm_area_structs区域结构链表其中的每一个vm_area_structs描述了当前虚拟地址空间中的一个区域当内核允许这个进程时就将pgd放入CR3控制寄存器中x86CR3的值是每个进程上下文的一部分当上下文切换时CR3的值会被回复vm_area_structs包含下面字段vm_start指向这个区域的起始地址vm_end指向这个区域的终止地址vm_prot描述这个区域内包含的所有页的访问权限vm_flags:描述这人区域内的页面是与其他进程共享的还是这个进程私有的以及一些其他信息vm_next指向链表中下一个区域结构Linux缺页异常处理当MMU试图翻译某个虚拟地址A时触发了一个缺页。这个异常导致控制转移到内核的缺页处理程序处理程序会进行下列步骤虚拟地址A是否合法即A是否在某个区域内部。缺页处理程序会搜索区域结构的链表把A和每个区域结构中的vm_start和vm_end作比较如果指令不合法则报一个段错误进行的内存访问是否合法即该进程是否有读写执行这个区域内页面的权限。比如可能是对代码段里面只读页面进行写操作造成的用户模式试图访问内核内存造成的若前两条都正常那么这个缺页就是对于合法的虚拟地址的合法操作造成的。那么就会执行正常的缺页处理选择一个牺牲页面如果牺牲页面被修改过就把它交换出去换入新页面。然后当缺页处理程序返回时CPU重新启动引起缺页的指令这时候重新发送虚拟地址A给MMUMMU正常翻译地址A