当前位置: 首页 > article >正文

深入理解计算机系统—虚拟内存(2)

9.6.4 综合:端到端的地址翻译

        通过一个具体端到端的地址翻译实例,综合上述内容,这个示例运行在有一个 TLB 和 L1 d-cache 的小系统上。做出如下假设:

        ① 内存是按字节寻址的。 ② 内存访问是针对 1 字节的字的(不是4字节)。③ 虚拟地址是14位长的(n=14)④ 物理地址是 12 位长的(m=12)⑤ 页面大小是 64 字节(P=64)⑥ TLB 是四路组相联的,总共有 16 个条目。 ⑦ L1 da-cache 是物理寻址,直接映射的,行大小为 4 字节,而总共有 16组。

        图 9-19 展示了物理地址和虚拟地址的格式。因为每个页面都是 2^6=64 字节,所以虚拟地址和物理地址的低 6 位分别作为 VPO 和 PPO。 

图 9-20 展示了小内存系统的一个快照,包括 TLB(9-20a),页表的一部分(9-20b)和 L1 高速缓存 (图9-20c)。在 TLB 和 高速缓存的图上,还展示了访问这些设备时硬件是如何划分虚拟地址和物理地址的位的。 

        ① TLB: TLB 是利用 VPN 的位进行虚拟寻址的。因为 TLB 有 4(2^2=4)个组,所以 VPN 的低 2 位作为组索引(TLBI)VPN 中剩下的高 6 位作为标记(TLBT),用来区别可能映射到同一个TLB 组的不同VPN。 

        ② 页表。这个页表是一个单级设计,一共有 2^8=256 个页表条目(PTE)。然而,只对这些条目中的开头 16 个感兴趣。为了方便,用索引它的 VPN 来标识每个 PTE;但记住这些 VPN 并不是页表的一部分,也不存储在内存中。另外,注意每个无效 PTE 的 PPN 都用一个破折号来表示,加强一个概念:无论刚好这里存储的是什么位值,都是没有任何意义的

        ③ 高速缓存: 直接映射的缓存是通过物理地址中的字段来寻址的。因为每个块都是 4 字节。所以物理地址的低 2 位作为块偏移(CO)。因为有 16 组,所以接下来的 4位用来表示 组索引(CI),剩下的 6 位做标记(CT)。 

         给定这种初始化设定,当 CPU 执行一条读地址 0x03d4 处字节的加载指令发生::(假定 CPU 读取 1字节的字,而不是 4字节的字)为了开始这种手工模拟,发现写下虚拟地址的各个位,标识出我们会需要的各种字段,并确定它们的 16 进制值。当硬件解码地址时,它也执行相似的任务。

        

开始时, MMU 从虚拟地址中抽取出 VPN (0x0F),并且检查 TLB,看它是否因为前面的某个内存引用缓存了 PTE 0x0F 的一个副本。 TLB 从 VPN 中抽取出 TLB 索引(0x03)TLB 标记(0x03)组 0x3 的第二个条目中有效匹配,所以命中,然后将缓存的 PPN (0x0D)返回给 MMU。

        如果 TLB 不命中,那么 MMU 就需要从主存中取出相应的 PTE。然而,此时 TLB 命中。现在,MMU 有了形成物理地址所需要的所有东西。它通过将来自 PTE 的  PPN(0x0D)和来自虚拟地址的 VPO (0x14)连接起来 ,形成了物理地址(0x354)

 

        接下来,MMU  发送物理地址给缓存,缓存从物理地址中抽取出缓存偏移 CO (0x0),组索引CI(0x5)以及缓存标记CT (0x0D)

        因为 组 0x5 中的标记与 CT 相匹配,所以缓存检测到一个命中,读出在偏移量 CO 处的数据字节(0x36),并将它返回给 MMU,随后 MMU 将它传递回 CPU。

翻译过程的其他路径是可能的。例如,TLB 不命中,那么 MMU 必须从页表中的 PTE 中取出 PPN。 如果得到的 PPE 是无效的,那么就产生一个缺页,内核必须调入合适的页面,重新运行这条加载指令另一种可能性是 PTE 是有效的,但是所需要的内存块在缓存中不命中。 

9.7 案例研究: Intel Core i7/Linux 内存系统

        以一个实际系统的案例研究来总结对虚拟内存的讨论:一个运行 Linux 的 Intel Core i7。虽然底层的 Haswell 微体系结构允许完全的 64 位虚拟和物理地址空间,而现在的(以及可预见的未来的)Core i7 实现支持 48位(256TB)虚拟地址空间和 52位(4PB)物理地址空间,还有一个兼容模式,支持 32位(4GB)虚拟和物理地址空间。 

        图 9-21 给出了 Core i7 内存系统的重要部分处理器封装包括 4个核,一个大的所有核共享的 L3 高速缓存,以及一个 DDR3 内存控制器。每个核包含一个层次结构的 TLB,一个层次结构数据和指令高速缓存,以及一组快速的点到点链路,这种链路是基于 QuickPath 技术,是为了让一个核与其他核和外部 I/O桥直接通信。TLB 是虚拟寻址的,是四路组相联的,L1 L2 L3 高速缓存是物理寻址的,块大小为 64字节。 L1 和 L2 是 8路组相联的,而 L3 是 16路组相联的。页大小可以在启动时被配置为 4KB 或 4MB。 Linux 使用的是 4KB 的页。 

9.7.1 Core i7地址翻译

        图 9-22 总结了完整的 Core i7 地址翻译过程,从 CPU 产生虚拟地址的时刻一直到来自内存的数据字到达 CPU。 Core i7 采用四级页表层次结构。每个进程有它私有的页表层次结构。当一个 Linux 进程在运行时,虽然 Core i7 体系结构允许页表换进换出,但是与已分配了的页相关联的页表都是驻留在内存中的。CR3 控制寄存器指向第一级页表(L1)的起始位置。CR3 的值是每个进程上下文的一部分,每次上下文切换时,CR3 的值都会被恢复。

        图 9-23 给出了第一级,第二级或第三级页表中条目的格式。当 P=1 时(Linux中总是如此 ) ,地址字段包含一个 40 位物理页号(PPN),它指向适当的页表的开始处。这里强加了一个要求:要求物理页表 4KB 对齐。 

         图 9-24 给出了第四级页表中条目的格式。当 P=1,地址字段包括一个 40 位 PPN,它指向物理内存中某一页的基地址。这又强加了一个要求:要求物理页 4KB 对齐。

         PTE 有三个权限位,控制对页的访问。R/W 位确定页的内容是可以读写还是只读的 U/S 位确定是否能够在用户模式中访问该页,从而保护操作系统内核中的代码和数据不被用户程序访问。 XD (禁止执行)位是在 64 位系统中引入的,可以用来禁止从某些内存页取指令。这是一个新特性,通过限制只能执行只读代码段,使得操作系统内核降低了缓冲区溢出攻击的风险。        

        当 MMU 翻译每一个虚拟地址时,它还会更新另外两个内核缺页处理程序会用到的位。每次访问一个页时,MMU 都会设置 A 位,称为 引用位 。 内核可以用这个引用位来实现它的页替换算法。每次对一个页进行了写之后,MMU 都会设置 D 位,又称修改位或脏位。修改位告诉内核在复制替换页之前是否必须写回牺牲页内核可以通过调用一条特殊的内核模式指令来清除引用位或修改位

         图 9-25 给出了 Core i7 MMU 如何使用四级的页表来将虚拟地址翻译成物理地址。 36位 VPN 被划分成四个 9 位的片,每个片被用作到一个页表的偏移量。 CR3 寄存器包含 L1 页表的物理地址。VPN 1 提供到一个 L1 PET 的偏移量,这个 PTE 包含 L2 页表的基地址。VPN2 提供一个 L2 PTE的偏移量,以此类推。

9.7.2 Linux 虚拟内存系统

        Linux 为每个进程维护了一个单独的虚拟地址空间,如图 9-26 所示,包括那些熟悉的代码,数据,堆,共享库以及栈段。既然理解了地址翻译,就能够填入更多的关于内核虚拟内存的细节,这部分虚拟内存位于用户栈之上

         内核虚拟内存包含内核中的代码和数据结构。内核虚拟内存的某些区域被映射到所有进程共享的物理页面。例如,每个进程共享内核的代码和全局数据结构。有趣的是,Linux 也将一组连续的虚拟页面(大小等于系统中DRAM的总量)映射到相应的一组连续的物理页面。这就为内核提供了一种便利的方法来访问物理内存中任何特定的位置,例如,当他需要访问页表,或者在一些设备上执行内存映射的 I/O 操作,而这些设备被映射到特定的物理内存位置 时

         内核虚拟内存的其他区域包含每个进程都不相同的数据。比如说,页表、内核在进程的上下文中执行代码时使用的栈,以及记录虚拟地址空间当前组织的各种数据结构。

        1. Linux 虚拟内存区域

        Linux 将虚拟内存组织成一些区域(也叫做段)的集合一个区域(area)就是已经存在着的(已分配的)虚拟内存的连续片(chunk),这些页是以某种方式相关联的。例如,代码段,数据段,堆,共享库段,以及用户栈都是不同的区域。每个存在的虚拟页面都保存在某个区域中,而不属于某个区域的虚拟页是不存在的,并且不能被进程引用。区域的概念很重要,它允许虚拟地址空间有间隙。内核不用记录那些不存在的虚拟页,而这样的页也不占用内存,磁盘或者内核本身中的任何额外资源。

        图 9-27 强调了记录一个进程中虚拟内存区域的内核数据结构内核为系统中的每个进程维护一个单独的任务结构(源代码中的task_struct)。任务结构中的元素包含或者指向内核允许该进程所需要的所有信息(例如 PID,指向用户栈的指针,可执行目标文件的名字,以及程序计数器) 

        任务结构中的一个条目指向 mm_struct,它描述了虚拟内存的当前状态。感兴趣的两个字段是 pgd 和 mmap,其中 pgd 指向第一级页表(页全局目录)的基址,而 mmap 指向一个 vm_area_structs (区域结构)的链表,其中每个 vm_area_structs 都描述了当前虚拟地址空间的一个区域。当内核运行这个进程时,就将 pgd 存放在 CR3 控制寄存器中

        一个具体区域的区域结构包含下面的字段:
① vm_start:指向这个区域的起始处

② vm_end:指向这个区域的结束处

③ vm_prot:描述这个区域内包含的所有页的读写许可权限

④ vm_flags;描述这个区域内的页面是与其他进程共享的,还是这个进程私有的(还描述了其他一些信息)

⑤ vm_next:指向链表中下一个区域结构。 

2. Linux 缺页异常处理

        假设 MMU 在试图翻译某个虚拟地址 A 时,触发了缺页。这个异常导致控制转移到内核的缺页处理程序,处理程序随后执行下面的步骤:

         ① 虚拟地址 A 是合法的吗?换句话说,A 在某个区域结构定义的区域内吗缺页处理程序搜索区域结构的链表,把 A 和每个区域结构中的 vm_start 和 vm_end 作比较,如果这个指令是不合法的,那么缺页处理程序就触发一个段错误,从而终止这个进程。在图9-28中标识为“ 1 ”

        因为一个进程可以创建任意数量的新虚拟内存区域(使用在下一节描述的 mmap 函数),所以顺序搜索区域的链表开销可能会很大。因此在实际中,Linux 使用某些我们没有显示出来的字段,Linux 在链表中构建了一棵树,并在这棵树上进行查找。        

        ② 试图进行的内存访问是否合法?换句话说,进程是否有读,写或者执行这个区域内页面的权限?例如,这个缺页是不是由一条试图对这个代码段里的只读页面进行写操作的存储指令造成的? 这个缺页是不是因为一个运行在用户模式中的进程试图从内核虚拟内存中读取字造成的?如果试图进行的访问是不合法的,那么缺页处理程序会触发一个保护异常,从而终止这个进程。这种情况在图 9-28 中标识为 “2”.

        ③ 此刻,内核知道了这个缺页是由于对合法的虚拟地址进行合法的操作造成的。它是这样处理这个缺页的:选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它交换出去,换入新的页面并更新页表。当缺页处理程序返回时,CPU 重新启动引起缺页的指令,这条指令将再次发送 A 到 MMU。这次,MMU 就能正常地翻译A,而不会产生缺页中断了。

 9.8 内存映射

        Linux 通过将一个虚拟内存区域与一个磁盘上的对象关联起来以初始化这个虚拟内存区域的内容,这个过程称为内存映射。虚拟内存区域可以映射到两种类型的对象中的一种。

        ① Linux 文件系统中的普通文件一个区域可以映射到一个普通磁盘文件的连续部分,例如执行一个可执行文件。 文件区被分成页大小的片每一片包含一个虚拟页面的初始内容。因为按需进行页面调度,所以这些虚拟页面没有实际交换进入物理内存,直到 CPU 第一次引用到页面(即发射一个虚拟地址,落在地址空间这个页面的范围之内)。如果区域比文件区大,那么就用 0 来填充整个区域的余下部分。 

        ② 匿名文件一个区域也可以映射到一个匿名文件,匿名文件是由内核创建的,包含的全是二进制零。 CPU 第一次引用这样一个区域的虚拟页面时,内核就在物理内存中找到一个合适的牺牲页面,如果该页面被修改过,就将这个页面换出来,用二进制零覆盖牺牲页面并更新页表,将这个页面标记为是驻留在内存中的。注意在磁盘和内存之间并没有实际的数据传送。因为这个原因,映射到匿名文件的区域中的页面有时也叫做请求二进制零的页。 

        无论哪种情况,一旦一个虚拟页面被初始化了,他就在一个由内核维护的专门的交换文件之间换来换去。交换文件也叫做 交换空间 或者 交换区域在任何时刻,交换空间都限制着当前运行着的进程能够分配的虚拟页面的总数。 

9.8.1 再看共享对象

        内存映射的概念来源于一个聪明的发现:如果虚拟内存系统可以集成到传统的文件系统中,那么就能提供一种简单而高效的把程序和数据加载到内存的方法。 

        进程这一抽象能够为每个进程提供自己私有的虚拟地址空间可以免受其他进程的错误读写。不过,许多进程由同样的只读代码区域。例如,每个运行 Linux shell 程序 bash 的进程都有相同的代码区域。而且,许多程序需要访问只读运行时库代码的相同副本。例如,每个 C 程序都需要来自标准库的诸如 printf 这样的函数。 那么,如果每个进程都在物理内存中保存这些常用代码的副本,那么就是极端的浪费了。幸运的是,内存映射提供了一种清晰的机制,用来控制多个进程如何共享对象

        一个对象可以被映射到虚拟内存的一个区域,要么作为共享对象,要么作为私有对象如果一个进程将一个共享对象映射到它的虚拟空间的一个区域内那么这个进程对这个区域的任何写操作对于那些也把这个共享对象映射到它们虚拟内存的其他进程而言,也是可见的。而且,这些变化也会反映在磁盘上的原始对象中

        另一方面,对于一个映射到私有对象的区域做的改变,对于其他进程来说是不可见的,并且进程对这个区域所作的任何写操作都不会反映在磁盘上的对象中。一个映射到共享对象的虚拟内存区域叫做 共享区域。类似的,也有虚拟区域

        假设进程1 将一个共享对象映射到它的虚拟内存的一个区域中,如图 9-29a 所示。现在在假设进程2将同一个共享对象映射到它的地址空间(并不一定要和进程1在相同的虚拟地址处,图9-29b) 

        因为每个对象都有一个唯一的文件名内核可以迅速地判定进程 1 已经映射了这个对象,而且可以使进程 2 中的页表条目指向相应的物理页面。关键点在于即使对象被映射到了多个共享区域,物理内存中也只需要存放共享对象的一个副本。 为了方便,我们将物理页面显示为连续的,但是一般情况下当然不是这样。

        私有对象使用一种叫做写时复制的巧妙技术被映射到虚拟内存中。一个私有对象开始生命周期的方式基本上与共享对象的一样,在物理内存中只保存有私有对象的一份副本。如图9-30a,其中两个进程将一个私有对象映射到它们虚拟内存的不同区域,但是共享这个对象同一个物理副本。对于每个映射私有对象的进程,相应私有区域的页表条目都被标记为只读并且区域结构被标记为 私有的写时复制只要没有进程试图写它自己的私有区域,它们就可以继续共享物理内存中对象的一个单独副本。然而,只要有一个进程试图写私有区域内的某个页面,这个写操作就会触发一个保护故障。

        当故障处理程序注意到保护异常是由于进程试图写私有的写时复制区域的一个页面而引起的,它就会在物理内存中创建这个页面的一个新副本更新页表条目指向这个新的副本然后恢复这个页面的可写权限,如图9-30b,当故障处理程序返回时,CPU 重新执行这个写操作,现在在新创建的页面上这个写操作就可以正常执行了 

        通过延迟私有对象中的副本直到最后可能的时刻,写时复制充分地使用了稀有的物理内存。

9.8.2 再看 fork 函数 

         理解虚拟内存和内存映射后,可以知道 fork 函数是如何创建一个带有自己独立虚拟地址空间的新进程的。

        当 fork 函数被当前进程调用时,内核为新进程创建各种数据结构,并分配给它一个唯一的 PID。为了给这个新进程创建虚拟内存,它创建了当前进程的 mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制

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

9.8.3 再看 execve 函数

        虚拟内存和内存映射将程序加载到内存的过程扮演着关键角色。既然已经理解这些概念,就能理解 execve 函数实际上是如何加载和执行程序的。假设运行在当前进程中的程序执行了如下execve 调用: 

        evece("a.out", NULL, NULL);

        正如第八章, execve 函数在当前进程中加载并运行包含在可执行目标文件 a.out 中的程序,用 a.out 程序有效地替代了当前程序。加载并运行 a.out 需要以下几个步骤:

        ① 删除已存在的用户区域。 删除当前进程虚拟地址的用户部分中的已存在的区域结构

        ② 映射私有区域。为新程序的代码,数据,bss 和 栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的代码和数据区域被映射为 a.out 文件中的 .text 和 .data区。  bss区域 是请求二进制零的,映射到匿名文件,其大小包含在 a.out 中。栈和堆区域也是请求二进制零的,初始长度为零。图9-31 概括了私有区域的不同映射。

        ③ 映射共享区域。 如果 a.out 程序与共享对象(或目标)链接,比如标准C库 libc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内

         ④ 设置程序计数器。 execve 做的最后一件事就是设置当前进程上下文中的程序计数器,使之指向代码区域的入口点。

 下一次调度这个进程时,它将从这个入口点开始执行。Linux 将根据需要换入代码和数据页面。

 9.8.4 使用 mmap 函数的用户级内存映射

        Linux 进程可以使用 mmap 函数来创建新的虚拟内存区域,并将对象映射到这些区域中。

 mmap 函数要求内核创建一个新的虚拟内存区域,最好是从地址 start 开始的一个区域并将文件描述符 fd 指定的对象的一个连续的片(chunk)映射到这个新的区域连续的对象片大小为 length 字节,从距文件开始处偏移量为 offset 字节的地方开始。 start 地址仅仅是一个暗示,通常被定义为 NULL。为了目的,总是假设起始地址为 NULL。图 9-32 描述参数的意义。

 参数 prot 包含描述新映射的虚拟内存区域的访问权限位(即在相应区域结构中的 vm_prot 位)

        PROT_EXEC:这个区域的页面由可以被 CPU 执行的指令组成

        PROT_READ:这个区域内的页面可读

        PROT_WRITE:这个区域内的页面可写

        PROT_NONE:这个区域内的页面不可访问

参数 flags 由描述被映射对象类型的位组成。如果设置了 MAP_ANON 标记位,那么被映射的对象就是一个匿名对象,而相应的虚拟页面是请求二进制零的。 MAP_PRIVATE 表示被映射的对象是一个私有的,写时复制的对象,而 MAP_SHARED 表示是一个共享对象

 bufp= Mmap(NULL, size, PROT_READ, MAP_PRIVATE|MAP_ANON, 0, 0);

让内核创建一个新的包含 size 字节的只读,私有,请求二进制零的虚拟内存区域。如果调用成功,那么 bufp 包含新区域的地址。

        munmap 函数删除虚拟内存的区域: 


http://www.kler.cn/a/466692.html

相关文章:

  • 云架构:考量与框架
  • 关于大一上的总结
  • 数据挖掘——集成学习
  • 125个Docker的常用命令
  • 数据挖掘——概论
  • MySQL叶子节点为啥使用双向链表?不使用单向呢?
  • 数据库新建用户后(Host:%),报错:localhost无法连接
  • linux下安装tun模块详细教程
  • 基于FPGA的2FSK+帧同步系统verilog开发,包含testbench,高斯信道,误码统计,可设置SNR
  • 算法-大整数反转
  • UE4_用户控件_10_用图像来显示场景捕获的渲染目标
  • 企业三要素如何用PHP实现调用
  • IIS设置IP+端口号外网无法访问的解决方案
  • 【Python系列】Flask 与 FastAPI:两个 Python Web 框架的对比分析
  • 组合模式详解
  • 依赖冲突`npm install --no-audit --save @testing-library/jest-dom@^5.14.1...` failed
  • CTE与子查询的区别及运行效率比较
  • 使用Dockerfile构建镜像
  • centos8 部署 kubernetes集群
  • 网易云的ip归属地是什么意思?为什么变来变去
  • Segment Anything C++ 项目【Part2:修改源码+自动分割】
  • 我的博客年度之旅:感恩、成长与展望
  • MySQL 表结构在线变更:优雅地解决停机问题
  • 【Rust自学】10.2. 泛型
  • 医学AI公开课第二期|写给癌症研究者的人工智能指南|公开课·25-01-03
  • 论述数据、数据库、数据库管理系统、数据库系统的概念。