首发于embedded guy
内存管理1:为什么需要虚拟内存?

内存管理1:为什么需要虚拟内存?

目录


(文中图片均来自《深入理解计算机系统(原书第三版)》,侵删)


1 先上结论

为什么需要虚拟内存?因为虚拟内存提供了三项能力,解决了物理内存系统面临的两个问题。

内存系统面临哪些问题?

  1. 内存(memory)资源永远都是稀缺的,当越来越多的进程需要越来越来内存时,某些进程会因为得不到内存而无法运行;
  2. 内存容易被破坏,一个进程可能误踩其他进程的内存空间;

虚拟内存提供了哪些能力?

正如软件工程中的其他抽象,虚拟内存是操作系统物理内存和进程之间的中间层。它为进程隐藏了物理内存这一概念,为进程提供了更加简洁和易用的接口。这个中间层提供了三个重要的能力:

  1. 高效使用内存:VM将主存看成是存储在磁盘上的地址空间的高速缓存,主存中保存热的数据,根据需要在磁盘和主存之间传送数据;
  2. 简化内存管理:VM为每个进程提供了一致的地址空间,从而简化了链接、加载、内存共享等过程;
  3. 内存保护:保护每个进程的地址空间不被其他进程破坏。 下文详述这虚拟内存三项能力。

2 何为虚拟寻址?

虚拟寻址架构图

CPU通过发出虚拟地址(Virtual Address, VA)来访问主存,在被送达主存前通过内存管理单元(Memory Management Unit, MMU)翻译成物理地址,最后主存将读取到的数据传递给CPU。其中,MMU利用存放在主存中的查询表(即页表,Page Table)来动态翻译虚拟地址,该表的内容由操作系统管理

3 VM能力1:虚拟内存作为缓存的工具

要想说清楚该能力,需要涉及以下7个方面的内容。

3.1 虚拟页

VM系统通过将虚拟内存分割为虚拟页(Virtual Page,VP)的大小固定的块来作为磁盘和主存之间的传输单元;类似地,物理内存被分割为物理页(Physical Page,PP),物理页也被称为页帧(page frame)。

物理页缓存在DRAM中

虚拟页(PP)有且仅有三个状态:

  • 未分配的:VM系统还未分配(或创建 )的页,未分配的块没有任何数据和它们相关联,因此也就不占用任何磁盘空间
  • 缓存的:当前已缓存在物理内存中的已分配页;
  • 未缓存的:未缓存在物理内存中已分配页,在磁盘上有对应的页。

3.2 页表(PTE)

虚拟内存系统需要有办法判断虚拟页是否缓存在DRAM的某个地方:如果命中,需要能找到虚拟页对应的物理页;如果不命中,需要能找到虚拟页存放在磁盘哪个位置。这些内容是通过页表实现的。

页表架构图

虚拟地址空间中的每个页在一个固定偏移量处都有一个PTE(实际实现中,通过虚拟地址的多级页表,层层索引到PTE)。每个PTE的组成: 一个有效位(valid bit)和 一个n位地址字段。

  • 如果设置了有效位,地址字段表示内存中相应物理页的起始位置;
  • 如果没有设置有效位,空地址表示该虚拟页未分配(未被分配的);
  • 如果没有设置有效位,非空的地址字段指向虚拟页在磁盘上的起始位置(未被缓存的)。

3.3 地址翻译

使用页表的地址翻译

地址翻译的过程就是通过虚拟地址查找物理地址的过程。CPU中存在一个控制寄存器,叫做页表基址寄存器(X86中称为Page Table Base Register,PTBR; ARM中称为Translation Table Base Register,TTBR,另外TTBR0存放用户空间一级页表基址,TTBR1存放内核空间一级页表基址)指向当前页表。虚拟地址中的虚拟页号VPN即为相对TTBR的偏移,此时则能在页表中定位到一个条目,如果该条目的有效位为1,那该条目的地址字段为物理页号(PPN),最后组合上虚拟页偏移量VPO,即为物理地址。

3.4 页命中


页命中过程示意

页命中的五步走:

  1. CPU发生访存需求,发出虚拟地址到MMU;
  2. MMU根据多级页表,生成PTE地址(PTEA),向内存发出请求希望获得PTE;
  3. 内存向MMU返回PTE;
  4. PTE中有效位为1,MMU将PTE中的PPN拿出来,构造物理地址(通过物理页号PPN + 虚拟地址中的页偏移量VPO),并把它传给主存;
  5. 主存返回所请求的数据给CPU。

3.5 缺页(指某个虚拟地址对应的物理地址没有缓存在内存中,只在磁盘里)

当有效位为0时,触发缺页异常,缺页异常流程会替换内存中一个牺牲页(如果该页被修改过,即脏页,会被写回到硬盘),并更新页表索引。然后返回重新执行导致缺页的命令,该命令会重新把导致缺页的虚拟地址发送到MMU,这时就能命中了。 页面命中完全是由硬件来处理的,而缺页要求硬件和操作系统内核协作完成。

缺页过程示意

缺页的七步走:

  1. 1到3步与页命中步骤相同;
  2. PTE中有效位为0,MMU触发一次异常,将PC指针转移到OS中的缺页异常处理程序;
  3. 缺页异常处理程序明确物理内存中需要牺牲的页面,如果该页面是脏页,将其换出到磁盘;
  4. 缺页异常处理程序调入新的页面,并更新内存中的PTE;
  5. 缺页异常处理程序返回原来的进程,再次执行导致缺页的指令。此时重新走页面命中的流程。

3.6 使用TLB加速地址翻译

每次CPU访问一个虚拟内存,MMU都需要做一次访存操作查询PTE。为了提高效率,我们再引入一个缓存---TLB,它是页表的缓存,包含在MMU硬件中。

TLB架构图

TLB命中就不多说了;TLB不命中时,MMU还是要去内存中取PTE,但是返回的PTE首先刷新TLB,然后再在MMU中做地址翻译。

3.7 多级页表

以32位地址空间、4KB页大小、一个PTE4字节来算,即使应用程序只使用虚拟地址空间的一部分地址,那也需要4MB的页表驻留在内存中。一个应用就占4MB,应用多起来岂不爆炸,因此有了多级页表。

使用多级页表的地址翻译

TTBR中存着一级页表的基地址,VPN1是一级页表的偏移,这样就定位到了2级页表的基地址,然后再根据偏移VPN2定位到3级页表的基地址,以此类推最终找到PPN,再结合VPO形成物理地址。 这里有两点需要强调:

只有一级页表才需要总是在主存中;虚拟内存系统可以在需要时创建、页面调入或调出二级页表,这就减少了主存的压力;只有最经常使用的二级页表才需要缓存在主存中。 访问k个PTE,第一眼看上去昂贵而不切实际。然后,这里TLB能够起作用,正是通过将不同层次上页表的PTE缓存起来。实际上,带多级页表的地址翻译并不比单级页表慢很多。

4 VM能力2:虚拟内存作为内存管理的工具

操作系统为每个进程提供了独立的页表,也就意味着提供了独立的虚拟地址空间。

每个进程有自己独立的页表

VM简化了链接、加载、代码和数据共享以及应用程序的内存分配。

  • 简化链接:对每个应用程序来说,代码段、数据段、堆、栈的安排都是一致的,这样的一致性极大的简化了链接器的设计与实现。允许链接器生成完全链接的可执行文件,这些可执行文件是独立于物理内存中代码和数据的最终位置的。
  • 简化加载:虚拟内存还使得容易向内存中加载可执行文件和共享对象文件。在把目标文件的.text和.data加载到一个新创建的进程中时,linux加载器会为代码段和数据段分配虚拟页。**同时把他们标识为无效的(即未被缓存的),将页表条目指向目标文件中适当的位置(这个操作是为了在发生缺页时,将目标文件在磁盘中相应位置的内容缓冲到内存中)。**可以看到,加载器并不会从硬盘向内存复制任何数据,在每个页初次被引用时,虚拟内存系统会通过缺页处理程序自动将相应页加载到内存中。
  • 简化共享:拿linux内核代码和C标准库来说,每个进程都会用到,为了省物理内存,这个需求需要所有进程共享实现。OS会将不同进程中适当的虚拟页面映射到相同的物理页面,从而安排多个进程共享这部分代码的一个副本(只占用了一份物理内存空间),而不是在每个进程中都包括单独的内核和C标准库的副本。
  • 简化内存分配:在使用malloc等内存分配函数时,由于页表的存在,操作系统没必要分配连续的物理页面给进程,而是在进程自己的虚拟地址空间找一块连续的虚拟地址分配给进程,当实际访问这块新分配的虚拟地址时,缺页相关代码会将其映射到实际的物理内存中。

5 VM能力3:虚拟内存作为内存保护的工具

一个进程不能修改它的只读代码段、内核的代码段和数据段、其他进程的私有内存等,这需要一定的保护机制。虚拟内存系统为每个进程实现的独立地址空间为这种保护机制提供了自然的实现方式。

用虚拟内存实现内存保护

通过在PTE中添加标志位,可以限制是否必须超级用户(SUP)才能访问,是够可读、可写。 如果一条指令违反了这些条件,CPU会触发一个保护故障,将PC传递给一个内核中的异常处理程序,这经常是所谓的“段错误(segmentation fault)”。

6 linux虚拟内存系统

一个linux进程的虚拟页表

linux为每个进程维护了一个虚拟地址空间,内核虚拟内存包含内核中的代码和数据结构,其中所有进程共享内核的代码和全局数据结构,另外,内核虚拟内存中也有每个进程都不相同的,包括页表、内核在进程的上下文中执行代码时使用的栈,以及记录虚拟地址空间当前组织的各种数据结构。

6.1 linux是如何组织虚拟内存的?

linux如何组织虚拟内存

一个进程对应一个task_struct结构体,其中的mm成员描述该进程虚拟内存的状态,mm中pgd和mmap是两个比较重要的成员。

  • pgd:指向第一级页表(页全局目录)的基址,running进程切换到该进程时,TTRB寄存器的值会被pgd成员覆盖,用于寻找PTE。
  • mmap:mmap是一个虚拟内存区域的链表,这里都是该进程已经申请的虚拟内存区域。

理解了mmap,可以再聊一下缺页处理的过程。

MMU当试图翻译某虚拟地址A时,如果相应PTE的有效位为0时,会触发缺页。此时会有以下步骤:

  1. 判断虚拟地址A是否为不存在的页面:遍历vm_area,如果该虚拟地址不在任何vm_area中,则报段错误;
  2. 判断虚拟地址A是否有相应的读写权限:遍历vm_area,确认读写操作与相应页面的权限是一致的;
  3. 如果不是以上的情况,那么就选择牺牲页,向内存中换入新的页面,并更新页表。

7 总结

回到我们最初的问题,为什么需要虚拟内存? 因为计算机内存系统面临两个棘手的问题:内存短缺 + 内存访问需要做保护。而虚拟内存系统提供的三个重要功能,可以有效解决这两个问题。 (1)虚拟内存系统在内存中自动缓存最近使用的存放在磁盘上的虚拟地址空间的内容(通过缺页实现); (2)虚拟内存系统简化了内存管理,进而又简化了链接、进程间共享数据、进程的内存分配和程序加载。 (3)虚拟内存系统通过在页表条目中加入保护位,从而简化了内存保护。


更多linux内核文章,点击传送门 Haonan:embedded guy专栏文章目录,会坚持更新!

参考文档

  1. draveness.me/whys-the-d
  2. 《深入理解计算机系统(原书第三版)》
欢迎在评论区和我讨论,在交流中一起进步。
编辑于 2022-04-18 00:31