CSAPP:第九章——虚拟内存(上)

概述

在一个系统中,各个进程间是共享CPU和主存资源的,然而主存共享伴随着很多挑战和问题,为了更加有效的管理主存,就有了虚拟内存这个概念。文件是对IO设备的抽象,虚拟内存是对主存和文件的抽象,进程是对处理器、内存和文件的抽象。

虚拟内存是硬件异常,硬件地址翻译,主存,磁盘文件和内核软件的完美交互,为每个进程提供一个大的一致的私有的地址空间,它主要有三个主要能力:

  1. 将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保留了活动区域,并根据需要在磁盘和主存间来回传送数据,从而高效使用主存。
  2. 为每个进程提供一致的地址空间。内存的每个字节由一个唯一的数字表示,称为它的地址,所有可能地址的集合称为虚拟地址空间。
  3. 保护了每个进程的地址空间不被其他进程破坏,即地址空间私有。

一、物理和虚拟寻址

计算机系统的主存被组织为M个连续的字节大小的单元组成的数组,每个字节都有一个唯一的物理地址,CPU通过物理地址寻址的方式称为物理寻址

如下图,示例上下文是一条加载指令,它从物理地址4处开始读取4个字节的数据字。当CPU执行这条指令的时,会生成一个有效物理地址,通过内存总线传递给主存。主存取出从物理地址4处开始的4个字节的数据字,并将其返回给CPU,CPU会将它放在一个寄存器里。
在这里插入图片描述

虚拟寻址,CPU通过生成一个虚拟地址来访问主存,这个虚拟地址被送到内存之前先转成相应的物理地址。将一个虚拟地址转成物理地址的任务叫做地址翻译
在这里插入图片描述

二、地址空间

地址空间(address space)是一个非负整数地址的有序集合。如果地址空间中整数是连续的,我们说它是线性地址空间(linear address space)。

  • 在一个带虚拟内存的系统中,CPU从一个有N = 2^n个地址的地址空间中生成虚拟地址,这个地址空间称为虚拟地址空间(virtual address space)。一个地址空间大小是由表示最大地址所需要的位数来描述的,如N = 2^n个地址的虚拟地址空间叫做n位地址空间,现在操作系统支持32位或64位。

  • 一个系统还有物理地址空间,它与系统中物理内存的M = 2^m(M不要求是2的幂,这里假设为2的幂)个字节相对应。

【注】地址空间区分了数据对象(字节)和它们的属性(地址),每个数据对象有多个独立的地址(如上面提到的一条加载指令有四个字节的数据字),其中每个地址都选自一个不同的地址空间,这就是虚拟内存的基本思想。主存中的每个字节都有一个选自虚拟地址空间的虚拟地址和选自物理地址空间的物理地址,且虚拟地址和物理地址互相对应。

三、虚拟内存作为缓存的工具

虚拟内存是计算机系统内存管理的一种技术。它使得进程认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。——虚拟内存与物理内存的联系与区别

虚拟内存被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组,每个字节都有一个唯一的虚拟地址,作为到数组的索引,磁盘上数组的内容被缓存到主存。

磁盘上的数据被分割成块,这些块作为磁盘和主存之间的传输单元。VM系统通过将虚拟内存分割为称为虚拟页(virtual page,VP)的大小固定的块来处理这个问题,每个虚拟页大小为P = 2^p个字节。类似的物理内存被分割为物理页(physical page,PP),大小也为P字节(物理页也被称为页帧)。

虚拟页面的集合都被分为三个不相交的子集:

  • 未分配的:未分配的页没有任何数据与他们相关联,所以不占用物理内存;
  • 缓存的:已缓存在物理内存的已分配页;
  • 未缓存的:未缓存在物理内存中的已分配页;

在这里插入图片描述

3.1 页表

SRAM缓存表示位于CPU和主存之间L1L2L3高速缓存,DRAM缓存表示虚拟内存系统的缓存,它在主存中缓存虚拟页。

当计算机使用到一个虚拟页时,虚拟内存系统(VM)首先判定虚拟页在DRAM是是否存在,如果命中,VM再确定这个虚拟页放在哪个物理页中;如果不命中,VM会判断这个虚拟页放在磁盘的哪个位置,找到它并在DRAM中选择一个牺牲页,将虚拟页复制到该牺牲页中(牺牲页中可能缓存着其他虚拟页,所以原来的内容会被替换),如果牺牲页的内容被修改,内核就会将其中的内容复制到磁盘。

页表是一个存放在内存中的页表条目(page table entry,PTE)的数组,负责将虚拟页映射到物理页。虚拟地址空间中的每个页在页表中的一个固定偏移量处都有一个PTEPTE有效位n位地址字段组成:

  • 有效位表明该虚拟页是否被缓存在DRAM中;
  • 如果在,地址字段就表示DRAM中相应物理页的起始位置;
  • 如果不在,地址字段就指向该虚拟页在磁盘上的起始位置;
    在这里插入图片描述

3.2 页命中与缺页

  • 页命中:一个页命中的过程,就是一个虚拟地址转换为物理地址的过程。

  • 缺页DRAM缓存不命中称为缺页(page fault),缺页会触发一个缺页异常,异常调用内核中的缺页异常处理程序,在DRAM中选择一个牺牲页。

3.3 分配页面

操作系统分配一个新的虚拟内存时,如调用malloc,首先在磁盘上面创建空间并更新页表PTE,使其中某个PTE从原来指向null,变成指向磁盘上新创建的页面,此时虚拟页从未分配状态变为未缓存

四、虚拟内存作为内存管理的工具

页表的功能是将虚拟地址空间映射到物理地址空间,操作系统为每个进程提供了一个独立的页表,因而也就是一个独立的虚拟地址空间,且每个进程的虚拟地址空间结构都一样。多个虚拟页可以映射到同一个共享物理页面上,如下图,进程iVP2和进程jVP1映射到物理内存的共享页面PP7
在这里插入图片描述
简化连接。独立地址空间允许每个进程的内存映像使用相同的基本格式,而不管代码和数据实际存放在物理内存的何处。

简化加载。加载可执行文件和共享文件,要把目标文件中的.text.data节加载到一个新创建的进程中,加载器为代码段和数据段分配虚拟页,并将其标记为无效(未缓存),将页表条目指向目标文件中适当的位置,加载器不会将任何数据从磁盘复制到内存。

简化内存分配。当一个运行在用户进程中的程序要求额外的堆内存时(调用malloc),操作系统会分配k连续的虚拟内存页面,并将它们映射到物理内存中任意位置的k个物理页面。即只需虚拟页面连续,而物理页面随机。

五、虚拟内存作为内存保护的工具

每次CPU生成一个地址时,地址翻译硬件都会读一个页表条目PTE,为了防止内存被非法访问,会在PTE上添加一些额外的许可位来控制对一个虚拟页面内容的访问。

  • sup,进程是否运行在内核模式下才能访问。
  • readwrite,读写控制访问。
    在这里插入图片描述

六、地址翻译

地址翻译就是把一个N元素虚拟地址空间(VAS)中的元素映射到M元素物理地址空间(PAS)中的元素上。

  • CPU中有一个页表基地址寄存器(Page Table Base Register, PTBR)指向当前页表,用于快速定位。
  • n位的虚拟地址划分为p位的虚拟地址偏移VPO和(n - p)位的虚拟页号VPN。
  • m物理地址划分为p位的物理地址偏移PPO和(m - p)位的物理页号PPN。
  • 因为虚拟页和物理页大小都是P字节,所以VPOPPO是相同的。MMU利用VPN来选择PTE,进而得到PPN,由于VPOPPO相同,因此将PPNVPO串联起来就得到了物理地址。
    在这里插入图片描述

当页面命中时,CPU硬件的执行步骤:

  • 1)处理器生成一个虚拟地址,并把它传送给MMU
  • 2)MMU生成PTE地址,并从高速缓存/主存请求得到它
  • 3)高速缓存/主存向MMU返回PTE
  • 4)MMU构造物理地址,并把它传送给高速缓存/主存
  • 5)高速缓存/主存返回所请求的数据字给处理器
    在这里插入图片描述

处理缺页则要求硬件和操作系统内核协作完成,具体步骤:

  • 1-3)前三步与处理页命中的步骤相同
  • 4)PTE中的有效位为0,所以MMU触发了一次异常,传递CPU中的控制到操系统内核中的缺页异常处理程序
  • 5)缺页处理程序确定出物理存储器中的牺牲页,如果这个页面被修改了,则把它换出到磁盘。
  • 6)缺页处理程序页面调入新的页面,并更新存储器中的PTE
  • 7)缺页处理程序返回到原来的进程,再次执行导致缺页的指令。CPU将引起缺页的虚拟地址重新发送给MMU
    在这里插入图片描述

6.1 结合高速缓存和虚拟内存

结合高速缓存和虚拟内存,大多数系统都是选择物理地址来访问高速缓存。下图显示物理寻址的高速缓存如何和虚拟内存结合,主要思路是地址翻译发生在高速缓存查找之前,注意页表条目可以缓存。
在这里插入图片描述

6.2 利用TLB加速地址翻译

每次CPU产生一个虚拟地址,MMU就必须查阅一个PTE,以便将虚拟地址翻译为物理地址。在糟糕时会要求从内存取一次数据,这样的代价是几十到几百个周期。如果PTE碰巧在L1中,那么开销就下降到1个或2个周期。为了消除这种开销,提高地址翻译的效率,许多系统在MMU中还引入了一个关于PTE的小的缓存,称为翻译后备缓冲区TLB(translation lookaside buffer)。TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。
在这里插入图片描述
使用TLB后在页命中时,所有地址翻译步骤都在芯片上MMU中执行,因此非常块。TLB页命中流程:

  • 1)CPU产生一个虚拟地址
  • 2)MMUTLB中取出相应的PTE
  • 3)MMU将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存/主存
  • 4)高速缓存/主存将所请求的数据字返回给CPU

TLB不命中时,MMU必须从L1缓存中取出相应的PTE,新取出的PTE存放在TLB中,可能覆盖一个已经存在的条目。
在这里插入图片描述

6.3 多级页表

如果有一个32位的地址空间,页面大小为4KBPTE大小为4字节,则一共有232/4K = 220= 1M个页面,也就需要1MPTE,也就需要一个(4 * 1)MB的页表,那么即使应用所引用的只是虚拟地址空间中的很小的一部分,也总是需要一个4MB的页表驻留在存储器中。

用来压缩页表的常用方法是使用层次结构的页表。以上述情况为例,还假设在这一时刻,虚拟地址空间有如下形式:虚拟内存的前2K个页面分配给了代码和数据,接下来的6K个页面还未分配,再接下来的1023个页面页未分配,接来下的1个页面分配给了用户栈。
在这里插入图片描述

为这个虚拟地址构造一个两级的页表层次结构:

  • 一级页表的每个PTE负责映射虚拟地址空间中一个4MB的片,这里每一片都是由1024个连续的页面组成的。这样一来,在32位的地址空间中,1024PTE足以覆盖整个空间。(232/4K) / 210 = 210 = 1K
    如果片 i 中的每个页面都未被分配,那么一级PTE i 就为空,例如图中片2~7。如果在片 i 中至少有一个页是分配了的,那么一级PTE i 就指向一个二级页表的基址。例如图中片0、1、8
  • 二级页表中的每个PTE都负责映射一个4KB的虚拟页面。注意,使用4字节的PTE,每个一级和二级页表都是4KB字节,这刚好和一个页面的大小一样。

使用多级页表的方式从两个方面减少了对内存是需求:

  • 1)如果一级页表中的一个PTE是空的,那么相应的二级页表就根本不会存在,这代表这一种巨大的潜在节约;
  • 2)只有一级页表才需要总是存在主存中,虚拟内存系统可以在需要时创建、页面调入或调出二级页表,这就减少了主存的压力,只有最经常使用的二级页表才需要缓存在主存中。

使用 k 级页表层次结构的地址翻译如下图所示。虚拟地址被划分为 kVPN1VPO,每个VPN i都是一个到第 i 级页表的索引,其中1 <= i <= k。第k级页表中的每个PTE包含某个物理页面的PPN或一个磁盘块的地址。在能够确定PPN之前,MMU必须访问KPTE。对于只有一级的页表结构,PPOVPO是相同的。
在这里插入图片描述
虽然要访问 kPTE,但由于TLB的作用,不同层次上页表的PTE被缓存起来。因此多级页表的地址翻译并不比单级页表的慢很多。

七、Linux 内存系统

Linux为每个进程维护了一个单独的虚拟地址空间,结构如下图所示:
在这里插入图片描述
内核虚拟内存:

  • 内核虚拟内存的某些区域被映射到所有进程共享的物理页面。例如,每个进程共享内核的代码和全局数据结构。Linux也将一组连续的虚拟页面(大小等于DRAM的总量)映射到相应的一组连续的物理页面,为内核提供了一种便利的方法来访问物理内存中任何特定的位置。
  • 内核虚拟内存的其他区域包含每个进程都不相同的数据。例如,页表、内核在进程上下文中执行代码时使用的栈、以及记录虚拟地址空间当前组织的各种数据结构。

Linux虚拟内存区域:Linux将虚拟内存组织成一些区域(也叫做段)的集合,一个区域就是已经存在的(已分配的)虚拟内存的连续片,这些页是以某种方式相关联的。如代码段、数据段、堆、共享库段和用户区都是不同是区域。

  • 只要是存在的虚拟页就保存在某个区域中,不属于某个区域的虚拟页面是不存在的,并且不能被进程所引用。
  • 区域的存在允许虚拟地址空间有间隙。
  • 内核不记录不存在的虚拟页,而这样的页也不占用内存、磁盘或者内核本身中的任何额外资源,由此节省空间。

下图强调了记录一个进程中虚拟内存区域的内核数据结构。内核为系统中的每个进程维护一个单独的任务结构(源代码中的task_struct)。任务结构中的元素包含或者指向内核运行该进程所需要的所有信息(例如,PID、指向用户栈的指针、可执行目标文件的名字以及程序计数器)。
在这里插入图片描述
任务结构task_structmm指针指向了mm_struct,该结构描述了虚拟内存的当前状态。mm_structpgd指针指向该进程的第一级页表(页全局目录)的基址。mmap指针指向了vm_area_struct(区域结构)链表,每个vm_area_struct都描绘了当前虚拟地址空间的一个区域。当内核运行这个进程时,它就将pgd存放在CR3控制寄存器中。区域结构包含以下几个部分:

  • vm_start:指向这个区域的起始处
  • vm_end:指向这个区域的结束处
  • vm_port:描述这个区域内包含的所有页的读写许可权限
  • vm_flags:描述这个区域内的页面是与其他进程共享的,还是这个进程私有的
  • vm_next:指向链表中下一个区域结构

7.1 Linux 缺页异常处理

MMU翻译一个虚拟地址A时发生发生缺页异常,该异常使控制转移到内核的缺页处理程序,程序执行如下步骤:

  • 1)判断A是否合法,即A是否在某个区域内。缺页处理程序搜索区域结构的链表,将虚拟地址与每个区域结构的vm_startvm_end进行比较,由此判断虚拟地址是否合法,即是否在某个区域结构定义的区域内。若不合法,则缺页处理程序触发段错误,终止进程。
  • 2)判断进程是否有读、写、执行这个区域内页面的权限,即内存访问是否合法。若不合法,则触发一个保护异常,终止进程。
  • 3)若是通过了上述两步,则说明该缺页是对一个合法地址的合法操作导致的,由此则可以处理缺页。选择一个牺牲页,如果牺牲页被修改过,那么把它交换出去,换入新的页面并更新页表。缺页处理程序返回时,CPU重新启动引起缺页的指令,这条指令将再次发送AMMU
    在这里插入图片描述

八、内存映射

虚拟内存区域是和磁盘中的文件对应的,Linux通过将一个虚拟内存区域与一个磁盘上的对象(也就是文件)关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射(memory mapping)。虚拟内存区域可以映射到以下两类对象:

  • 1)Linux文件系统中的普通文件:一个区域可以映射到一个普通磁盘文件的连续部分,比如一个可执行文件。文件区被分成页大小的片,每一片包含一个虚拟页面的初始内容。因为按需进行页面调度,所以这些虚拟页没有实际交换进入物理内存,直到CPU第一次引用虚拟页面(发射一个虚拟地址,落在这个页面的地址空间范围之内)。
  • 2)匿名文件:一个区域也可以映射到一个匿名文件,匿名文件是由内核创建,内容全是二进制零。CPU第一次引用这样一个区域的虚拟内存时,内核就在物理内存中找一个合适的牺牲页面,如果页面被修改过就将页面换出来,用二进制零覆盖牺牲页面并更新页表,并将这个页面标记为留在内存中。在磁盘和内存间没有实际的数据传输,因此映射到匿名文件中的页叫做请求二进制零的页(demand-zero page)

无论哪种情况,一旦一个虚拟页面被初始化了,它就在一个由内核维护的专门的交换文件(swap file)之间换来换去。交换文件也叫交换空间、交换区域。注意该空间限制当前运行的进程能够分配的虚拟页面的总数。

8.1 共享对象

操作系统为每个进程提供私有的虚拟地址空间,可以免受其他进程读写的干扰。但对于每个进程都要访问的相同的只读代码区域,如果每个进程在物理内存中保存一份副本,那就是极大的浪费。因此还是希望进程能够共享某些对象,即多个进程共享内存中的同一份资源。

一个对象可以被映射到虚拟内存的一个区域,要么作为共享对象,要么作为私有对象。一个映射到共享对象的虚拟内存区域叫做共享区域,类似的,也有私有区域

  • 若进程将一个共享对象映射到虚拟内存的一个区域,则该进程对这个区域的任何写操作,对那些也把该共享对象映射到虚拟内存的其他进程而言是可见的。并且,对象的变化会反映到磁盘上的原始对象上。对于下图有三个说明:1)共享文件映射到进程1和进程2的地址空间不一定相同;2)共享对象被映射到多个共享区域,但内存中只需存放一份共享对象的副本即可;3)物理页面一般不连续,但是虚拟页面连续。
    在这里插入图片描述
  • 反之,进程对映射到私有对象的区域所做的改变对其他进程不可见,且进程对该区域所做的任何写操作也不会反应在磁盘上的对象上,私有对象使用一种叫做写时复制的巧妙技术被映射到虚拟内存中。对于下图有两个说明:1)在物理内存中保存私有对象的一个副本,只要没有进程试图写私有区域,进程就可以一直共享物理内存中对象的一个单独副本,且每个进程私有区域的页表条目都被标记为只读,区域结构被标记为私有写时复制。2)若有进程试图写私有区域的某个页面,会触发一个保护故障,它会在内存中创建这个被写页面的新副本,然后更新页表条目指向新副本,并恢复这个页面的可写权限。
    在这里插入图片描述

8.2 fork 函数

Linux下可以使用fork函数创建新的进程,显然创建的新进程带有自己独立的虚拟地址空间。在当前进程调用fork函数时,内核为新进程创建各种数据结构,并为其分配唯一的进程ID。为给新进程创建虚拟内存,创建当前进程的mm_struct、区域结构和页表的副本。两个进程中的每个页面都标记为只读,两个进程中每个区域结构都标记为私有写时复制。

当fork在新进程中返回时,新进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同。当这两个进程中某个进行写操作时,写时复制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。

8.3 execve 函数

execve函数在当前进程中加载、运行包含在可执行文件a.out中的程序,用a.out有效代替当前程序,步骤如下:

  • 1)删除当前进程虚拟地址的用户部分已存在的区域结构。
  • 2)映射私有区域,如代码区、数据区分别映射a.out文件的.text.data区等。
  • 3)映射共享区域,如a.outlibc.so库链接,则将库映射到用户空间的共享区域。
  • 4)设置当前进程上下文的程序计数器,使之指向代码区域的入口。
    在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_42570601/article/details/117264753
今日推荐