首页 > 系统相关 >每个程序员都应该了解的内存知识(三): 虚拟内存

每个程序员都应该了解的内存知识(三): 虚拟内存

时间:2024-03-29 20:13:06浏览次数:25  
标签:TLB 虚拟地址 程序员 地址 内存 Page 虚拟内存 页表

虚拟内存

概念

wiki 解释

它使得应用程序认为它拥有连续可用的内存(一个连续完整的地址空间),而实际上物理内存通常被分隔成多个内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。

阅读链接

虚拟内存篇 (原文地址)

详解内存映射(做的图非常好, 一定要重点看一看)

MMU

(Memory Management Unit,内存管理单元)

  1. 地址翻译:将 CPU 生成的虚拟地址(或逻辑地址)翻译成物理内存中的物理地址。这种映射通常通过页表来完成,页表存储了虚拟地址到物理地址的映射信息。
  2. 内存保护:MMU 可以确保每个进程只能访问自己的内存空间,不会影响到其他进程的内存空间。这通过控制页表中的权限位来实现,比如读/写/执行权限。
  3. 内存共享:通过设置相同的映射,MMU 可以使得多个进程共享相同的物理内存区域。这对于进程间通信和共享代码库(例如,操作系统的库或者动态链接库)非常有用。
  4. 分页和分段:MMU 支持分页机制,可以将虚拟内存空间分为多个固定大小的页,并独立地将每个页映射到物理内存空间的任意位置。分段机制则允许将内存分为不同大小的段。
  5. 虚拟内存管理:MMU 的存在使得操作系统可以实现虚拟内存,即使物理内存已满,系统依然可以通过页面置换算法,将部分数据临时存储到硬盘的交换空间(swap space),从而扩展可用的内存空间。
  6. 缓存管理:MMU 还负责缓存控制,包括 TLB(Translation Lookaside Buffer)的管理。TLB 是一个专用的缓存,用于加速虚拟地址到物理地址转换过程中的页表查找。

地址转换

地址转换这部分的逻辑稍微有些复杂, 我们一点点的捋出来

整体来说, 内存的地址转换需要考虑一下几个方面

  1. 效率
  2. 动态
  3. 权限控制

Base + Bound (基址加界限)

这是一个最容易想到的方法, 就是直接使用一个 Base 作为物理内存的起点, Bound 表示其在物理内存的重点, 传入的虚拟地址表示偏移

image

当一个进程尝试访问内存时,CPU 会将进程产生的逻辑地址(相对地址)与基址寄存器的内容相加,转换成物理地址。如果计算出的物理地址超出了界限寄存器设定的范围,则会触发一个越界错误,通常会导致操作系统的干预。

这个方法虽然简单,但也有以下几个缺点:

  1. 固定分区大小:每个进程都需要在启动时确定其内存需求,这可能导致内存的浪费,因为进程可能不会使用到所有分配给它的内存。
  2. 内存碎片:随着进程的加载和卸载,内存中会出现无法使用的小空间,即产生碎片。
  3. 内存保护问题:如果操作系统中有 bug 或者某个进程能够篡改基址或界限寄存器的值,就可能破坏内存保护机制,访问或者污染其他进程的内存空间。
  4. 不支持多任务:这种方法不容易支持多任务操作,因为进程间的内存切换会变得复杂和开销较大。

基于 Segment

image

基于段(Segmentation)的内存管理是对基址加界限方法的一种改进。它的核心思想是将程序的逻辑地址空间划分为多个有意义的单元,称为“段”(Segment)。通常,每个段对应程序的一个逻辑结构,例如代码段、数据段、堆栈段等。这样的划分更加符合程序的逻辑结构,也有助于实现更细粒度的内存保护和共享。

在基于段的内存管理中,每个段都由一个段基址(Segment Base)和一个段界限(Segment Limit)来定义,类似于基址加界限方法中的基址寄存器和界限寄存器。但与基址加界限方法不同的是,基于段的方法允许每个段有不同的长度,更加灵活。

过程:

  1. 当程序想要访问内存时,它会生成一个由段号(Segment Number)和段内偏移(Offset)组成的逻辑地址。
  2. CPU 中的内存管理单元(MMU)会根据段号找到对应的段基址,将段内偏移与段基址相加得到物理地址。
  3. 同时,MMU 会检查偏移是否超过了段界限,以保证不会访问到当前段之外的内存区域。

关键特性:

  1. 保护和共享:通过对不同段设置不同的访问权限,可以实现内存保护。例如,代码段可以设置为只读,以防止程序代码被意外修改。另外,多个进程可以共享同一个段,如共享库等。
  2. 逻辑结构对齐:由于每个段通常对应程序中的一个逻辑部分,因此内存管理和程序结构紧密对应,这有助于程序的模块化和维护。
  3. 动态增长:某些段如堆栈段和堆段可以在运行时动态增长,因此段机制能够更好地支持动态内存分配。
  4. 减少内存碎片:虽然段式管理仍然可能产生外部碎片,但是由于可以根据需要为每个段分配合适大小的内存块,因此可以在一定程度上减少内存碎片。
  5. 地址隔离:不同的段可以独立定位,各个段之间地址空间隔离,提高了内存的使用效率。

缺陷:

  1. 内部碎片(Internal Fragmentation):由于每个段是连续分配的,当程序在一个段中申请的内存没有完全使用时,剩余的部分会产生内部碎片。例如,如果一个数据段分配了 1KB 的内存,而实际只用了 900B,那么剩下的 100B 就无法被其他段利用,从而造成浪费。
  2. 段的大小调整问题:如果一个段需要扩展,可能因为紧邻该段的内存空间已被占用,无法直接扩展,这时不得不移动该段到一个更大的空间去,这种操作会增加系统的开销,并可能导致额外的碎片产生。

基于 Page

image

分页(Paging)是现代操作系统中广泛采用的内存管理技术。与基于段的内存管理不同,分页管理将物理内存和虚拟内存都划分成了大小相同的块,称为“页”(Page)和“页框”(Page Frame)。

在分页系统中,虚拟内存空间由一系列连续的页组成,物理内存则由一系列连续的页框组成。虚拟地址到物理地址的映射是通过一个中间的映射结构——页表(Page Table)来完成的。每个运行的进程都有自己的页表。

关键概念:

  1. 页(Page) :是虚拟内存被划分的基本单位。
  2. 页框(Page Frame) :是物理内存被划分的基本单位,页和页框的大小是一致的。
  3. 页表(Page Table) :是操作系统维护的一个数据结构,用于记录虚拟页面到物理页框的映射关系。
  4. 虚拟地址(Virtual Address) :由进程生成,由页号(Page Number)和页内偏移(Offset)组成。
  5. 物理地址(Physical Address) :是实际存储数据的物理内存地址,由页框号(Frame Number)和页内偏移组成。

优点

  1. 消除外部碎片:由于所有的页和页框大小相同,不会像基址加界限或分段管理那样产生外部碎片。
  2. 灵活的内存分配:可以非连续地分配内存,一个进程的不同页可以分散在物理内存中的任何位置。
  3. 保护和隔离:每个页表条目可以有自己的保护位,确保进程间的内存是彼此隔离和保护的。

缺点

  1. 内部碎片:每个页面的内存利用可能不是 100%,最后一页尤其可能有未使用的空间。

  2. 地址转换开销:访问内存的次数变得更多了, 会导致耗时变长.

  3. 页表可能很大:页表大小与虚拟地址空间的大小成正比,对于具有大量内存的系统,页表占用的内存可能相当可观。

    我们可以稍微估算下, 假如 Frame 大小为 16KB(即 214), 那么页表的大小会达到 250

Segment + 单层 Page

image

进程的内存先分成多个段,每个段再按照页来分配。

地址分三个部分:段号 + 页号 + 页内偏移

根据段号去段表里面找对应的页表地址,得到页表的地址后,根据页号找到对应的物理内存的 Page Frame 地址,最后再结合页内偏移计算得到实际地址,其中段表中的 Size 是指某一段对应的页表的长度,即物理页的数目。

以 32 位虚拟地址空间,4KB 大小的 Page 为例,前 10bit 用于段号,中间 10 位用于页号,后 12 位用于页内偏移,假设页表的每一行的大小是 4 个字节,则一个物理 Page Frame 正好可以容下每个段的页表。

多层页表

image

多级页表(Multi-level Page Table)是为了解决单级页表因虚拟地址空间过大导致页表本身也非常庞大的问题。在32位或64位的系统中,虚拟地址空间可能非常巨大,如果使用单级页表,那么页表必须为整个虚拟空间中的每个页都保留一个条目,这样的页表将非常巨大,占用大量的物理内存。

多级页表通过层次化的结构,将这个大页表分解为更小的表,以此减少所需的物理内存。常见的有两级页表(也称二级页表)、三级页表、甚至四级页表。其中,二级页表是最常见的结构。

二级页表的工作原理:

  1. 顶级页表(Page Directory) :虚拟地址的高位部分用来索引顶级页表,顶级页表的每个条目指向第二级页表。
  2. 第二级页表(Page Table) :顶级页表中的条目会指向一个第二级页表,虚拟地址的中间位用来索引这个第二级页表。
  3. 页框号(Page Frame Number) :通过第二级页表可以得到页框号,加上虚拟地址中的低位偏移量,就可以得到完整的物理地址。

优点:

  • 减少内存占用:如果某个顶级页表条目对应的整个第二级页表都未被使用,则无需为其分配内存,从而节省了内存资源。
  • 灵活性:可以根据虚拟地址的使用情况动态地添加或删除第二级页表,适应不同程序的内存需求。

缺点:

  • 增加访问时间:由于需要多次内存访问来解析地址,因此可能会稍微增加虚拟地址到物理地址转换的时间。
  • 复杂性:多级页表的管理比单级页表复杂,需要更多的算法和数据结构来维护。

Segment + 多层页表(X86-64的处理方式)

image

在64位模式下,段表主要提供兼容性支持和某些特定的安全功能,而内存管理的主要职责已经转移到了分页系统。

段表在x86-64中的角色:

在64位模式下,x86-64架构几乎不使用传统的段式内存管理。

各个段寄存器(如CS, DS, ES, SS等)仍然存在,但主要用于特权级检查和存储一些状态信息。段的基地址固定为0,限长固定为最大可能值,这样实际的线性地址就等同于虚拟地址。这意味着,尽管段仍然存在,但它们不再用于分割或隔离内存,而是提供了一种简单的安全和特权级别检查方式。

多级页表在x86-64中的应用:

在x86-64架构下,分页是现代操作系统进行内存管理的核心。x86-64架构的分页系统支持多达4个级别的页表,通常指的是:

  1. PML4(Page Map Level 4) :这是最顶级的页表,每个PML4条目指向一个PDP(Page Directory Pointer table)。
  2. PDP(Page Directory Pointer) :PDP的每个条目指向一个PD(Page Directory)。
  3. PD(Page Directory) :PD的每个条目指向一个PT(Page Table),或者在启用1GB大页的情况下直接映射到一个页框。
  4. PT(Page Table) :PT的每个条目指向一个4KB的页框,或者在启用2MB大页的情况下直接映射到一个页框。

转换过程:

当CPU需要访问虚拟内存时,它会通过以下步骤来将虚拟地址转换为物理地址:

  1. 使用虚拟地址中的PML4部分(通常是最高的几位)来索引PML4表,找到对应的PDP。
  2. 使用虚拟地址中的PDP部分来索引PDP表,找到对应的PD。
  3. 使用虚拟地址中的PD部分来索引PD表,找到对应的PT(对于1GB的大页,这一步中PD条目直接指向物理内存)。
  4. 使用虚拟地址中的PT部分来索引PT表,找到4KB页框(对于2MB的大页,这一步中PT条目直接指向物理内存)。
  5. 最后,虚拟地址中的偏移量被加到页框物理地址中,得到完整的物理地址。

总结

通过以上的方式, 我们最终解决了表过大, 内存分配不灵活等等问题, 但是同时又出现了新的问题

Segment + 多层页表​的方式来举例, 我们想要定位到一个物理页时, 要经过几次的查表才能获得结果(Segment Table, PML4, PDP, PD), 换言之, 我们为了虚拟内存的一次访问, 实际的成本可能是3次/4次, 这无疑是存在巨大的问题的.

TLB (Translation lookaside buffer)

参考自: https://zhuanlan.zhihu.com/p/108425561

其实我们可以想到, 在一个进程中, 程序执行并不是跳跃性的, 而是按照一定的逻辑

换言之, 内存的需求是有一定的内聚性的, 即在一段时间内, 一块内存如果被访问, 那么很大概率它会被接着访问

在说的简单点: Cache, 我们可以将 虚拟内存地址 -> 物理内存的过策看做是一个求职函数 $Tranlaction()$, 传入虚拟地址, 返回物理地址, 这就使缓存​成为了可能.

TLB就像一个中间代理,它缓存了最近使用的一部分页表条目。当虚拟地址需要转换时,CPU首先会检查TLB是否包含该地址的转换信息。如果找到(这称为TLB命中),那么地址转换几乎是即时的。如果没有找到(TLB未命中),则需要从主内存中的页表中检索转换信息,并将其加载到TLB中以供后续使用。

TLB的作用:

  1. 减少延迟:TLB大大减少了虚拟地址转换为物理地址所需的时间,因为它避免了对主内存中页表的频繁访问。
  2. 提高效率:由于程序访问内存的局部性原理,即程序倾向于在短时间内多次访问同一块内存区域,TLB可以高效地服务于这些频繁访问的地址转换。
  3. 降低内存压力:通过减少对页表的查询次数,TLB可以减轻主内存的访问压力,从而提高系统的总体性能。

TLB的设计和优化:

  • 条目数量:TLB可以存储的页表条目的数量。这个大小直接影响TLB的命中率,但是更大的TLB意味着更高的成本和可能的能耗。
  • 关联度:TLB可以是全关联、组关联或直接映射。全关联TLB允许任何条目可以存储在TLB的任何位置上,这提供了最高的灵活性,但实现起来也最昂贵和复杂。
  • 替换策略:当TLB满了而需要加载新的地址转换信息时,它需要决定哪个旧条目被替换。常见的替换策略包括最近最少使用(LRU)和随机替换。

TLB失效和维护:

当进程上下文切换发生或者页表更新时,TLB中的某些条目可能会变得陈旧或无效。在这种情况下,操作系统需要更新TLB,这可以通过刷新整个TLB或仅刷新受影响的条目来完成。为了减轻操作系统的负担,现代处理器还提供了硬件辅助的TLB管理功能。

参考链接

  1. MMU 机制: https://zhuanlan.zhihu.com/p/464767887

标签:TLB,虚拟地址,程序员,地址,内存,Page,虚拟内存,页表
From: https://www.cnblogs.com/pDJJq/p/18104514/virtual-memory-17ne14

相关文章

  • 每个程序员都应该了解的内存知识(四): NUMA
    NUMA第四章https://lwn.net/Articles/254445/概念NUMA(Non-UniformMemoryAccess,非一致性内存访问)是一种计算机内存设计的架构,用在多处理器的系统中,以增加处理器访问内存的速度。在NUMA架构中,每个处理器或处理器组都有自己的本地内存,并且处理器可以直接访问本地内存以获得更......
  • 每个程序员都应该了解的内存知识(五): 代码优化
    代码优化多线程优化尽量使用顺序读写因为分支预测的关系,顺序读写通常能够带来更好的性能.共享变量将只读变量和读写变量分离有可能因为缓存行的原因导致读写变量的更新影响到读变量,进而影响了运行速度提升数据的局部性,将一起使用的读写变量分组到一个结构中缓......
  • 每个程序员都应该了解的内存知识(一): 南桥&北桥&内存
    南桥&北桥&内存结构每个程序员都应该了解的内存知识.pdf-p5-每个程序员都应该了解的内存知识-P5-20240327103419​​功能每个程序员都应该了解的内存知识.pdf-p5-每个程序员都应该了解的内存知识-P5-20240327104347​​北桥主要是连接CPU以及RAM以及南桥,作为连......
  • 每个程序员都应该了解的内存知识(二): CPU Cache
    CPUCache架构每个程序员都应该了解的内存知识.pdf-p22-每个程序员都应该了解的内存知识-P22-20240328112647​​每个程序员都应该了解的内存知识.pdf-p23-每个程序员都应该了解的内存知识-P23-20240328112807​​每个程序员都应该了解的内存知识.pdf-p23-每个......
  • 程序员35岁会失业吗?
    程序员35岁会失业吗?年龄并不是决定程序员就业机会的唯一因素。尽管年龄可能会对就业产生一定影响,但仍然有很多因素会影响一个程序员是否会失业。以下是一些需要考虑的因素:技能水平:技能和经验是程序员就业的关键因素。如果一个程序员保持学习和更新技能,掌握最新的技术和工......
  • Android数据库升级,阿里程序员的Android之路
    publicsynchronizedstaticDBHelpergetInstance(Contextcontext){if(instance==null){instance=newDBHelper(context);}returninstance;}@OverridepublicvoidonCreate(SQLiteDatabasedb){db.execSQL(SQL.CREATE_TABLE_FAVORITE);//若不是第一个......
  • 程序员转行做什么比较可行?
    ​前言现阶段互联网发展迎来新机遇和新挑战,随着时代的发展网络安全已成刚需,也成为了社会治理现代化的重要一环。也是目前互联网行业的未来发展重点方向。而目前,网络安全人才极度匮乏,门槛也相对较低,对于求职者来说发展空间相当之大。根据《互联网安全报告》显示,我国网络......
  • Lec3 Lec4: 虚拟内存和RISC-V寄存器
    虚拟内存使用虚拟内存主要为了实现隔离内存隔离,所有程序指令存放在一个物理内存上,如果一个指令的操作位刚好是另一个指令的地址,那么会造成指令的丢失为了解决这个问题使用地址空间地址空间为每一个指令程序分配自己的地址空间,每个指令程序只能在自己的地址空间上操作。我们需......
  • 使用Andorid Studio解决app内存泄漏问题方法与实践
    某项目的app运行一段时间(切换页面、触发交互事件等)后就开始严重卡顿,使用top查看内存的使用情况,发现每次操作过后内存都有小幅增长,且永远不下降,存在内存泄露问题。目录1AndoridStudio内存泄露检测工具使用方法2内存泄露实例分析2.1页面切换后未主动释放​编辑2.2回调......
  • 如何在Java中读取超过内存大小的文件
    读取文件内容,然后进行处理,在Java中我们通常利用Files类中的方法,将可以文件内容加载到内存,并流顺利地进行处理。但是,在一些场景下,我们需要处理的文件可能比我们机器所拥有的内存要大。此时,我们则需要采用另一种策略:部分读取它,并具有其他结构来仅编译所需的数据。接下来,我们就来......