CSAPP的阅读笔记。
在系统中所有的进程之间是共享CPU和主存这些内存资源的。当进程数量变多时,所需要的内存资源就会相应的增加。可能会导致部分程序没有主存空间可用。此外,由于资源是共享的,那么就有可能导致某个进程不小心写了另一个进程所使用的内存,进而导致程序运行不符合正常逻辑。
为了更加有效的管理内存并少出错,现代系统提供了一种对主存的抽象的概念,叫做虚拟内存(VM)。
`虚拟内存`是硬件异常、硬件地址翻译、主存、磁盘文件和内核软件间的完美交互,他为每个进程提供了一个大的、一致的和私有的地址空间。
虚拟内存提供了三个重要的能力: 缓存,内存管理,内存保护
1. 将主存视为一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据
2. 为每个进程提供了一致的地址空间,简化内存管理
3. 保护了每个进程的地址空间不被其他进程破坏
1.虚拟内存作为缓存的工具
概念上,虚拟内存被组织为一个由存放在磁盘上的N个连续字节大小的单元组成的数组。每字节都有一个唯一的虚拟地址,作为数组的索引。磁盘上的内容被缓存在主存中。
VM系统将虚拟内存分割为大小固定的块,称为虚拟页(VP),来作为磁盘和主存之间的传输单元。每个虚拟页的大小为P=2^p字节。类似地,物理内存也被分割为物理页(PP)(也称为页帧),大小也是P字节。
虚拟页面分为三个互斥子集:
1. 未分配的:VM系统还未分配(或创建的)页。未分配的块没有任何数据和它们相关联,因此也就不占用任何磁盘空间。
2. 缓存的:当前已缓存在物理内存中的已分配的页。
3. 未缓存的:未缓存在物理内存中的已分配的页。
如上图:有8个虚拟页。虚拟页0和3未分配,因此在磁盘上还不存在。虚拟页1、4和6被缓存在物理内存中。页2、5和7已经被分配了,但是当前并未缓存在主存中。
1.1页表
虚拟内存必须知道一个虚拟页是否放在物理页中,如果在物理页中,也需知道与之关联的物理页的具体物理地址。如果不在物理页中,需要选择一个牺牲页,要将该虚拟页从磁盘复制到DRAM中,并将被替换的牺牲页保存到磁盘中。
这些功能是由软硬件联合提供的。VM系统要实现上面的功能需要操作系统软件、MMU(内存管理单元)中的地址翻译硬件和一个存放在物理内存中叫`页表`的数据结构帮助。页表就是记录了虚拟页和物理页映射关系的一种数据结构。每次地址翻译硬件将一个虚拟地址转换为物理地址时,都需要读取页表。操作系统负责维护页表的内容,以及在磁盘与DRAM之间来回传送页。
上图展示了一个页表的基本组织结构。虚拟地址空间中的每个页在页表中一个固定偏移量处都有一个页表条目(PTE)。假设一个PTE由一个有效位和一个n位地址字段组成的。有效位表示该虚拟页当前是否缓存在主存(DRAM)中。如果设置了有效位,那么一个空地址表示这个虚拟页还未被分配。否则,这个地址就指向该虚拟页在磁盘上的起始位置。
图中展示了8个虚拟页和4个物理页。四个虚拟页(VP1、VP2、VP7和VP4)当前被缓存在DRAM中。两个页(VP0、VP5)还未被分配,而剩下的页(VP3和VP6)已经被分配了,但还未被缓存在DRAM中。
1.2页命中
页命中是指,该虚拟页面是缓存的。
如图,VP2被缓存在DRAM中。当CPU想要读取包含在VP2中的虚拟页的一个字时,地址翻译硬件将虚拟地址作为索引并根据页表中的PTE2来定位其物理地址,之后通过内存总线读取它。系统通过PTE2中标志位判定VP2是已经缓存在DRAM中。
1.3缺页
习惯上,DRAM中缓存不命中称为缺页。
上图展示了在缺页之前的页表状态的一个示例。CPU引用了VP3的一个字,VP3并未被缓存在DRAM中。地址翻译硬件从内存中读取PTE3,从有效位推断出VP3未被缓存,进而触发了一个缺页异常。
缺页异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,图例中就是存放在PP3中的VP4。如果VP4已经被修改了,那么内核就会将它复制回磁盘。无论如何,内核都会修改VP4的页表条目PTE4,反映出VP4不在缓存在主存中这一事实。
然后,内核会从磁盘复制VP3到内存中的PP3,更新PP3,随后返回。当异常处理程序返回时,它会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重发送到地址翻译硬件。但是现在,VP3已经缓存在主存中了,页命中也能由地址翻译硬件正常处理。
2.虚拟内存作为内存管理的工具
操作系统为每个进程提供一个独立的`页表`,因而也就是独立的虚拟地址空间。
如上图所示,进程i的页表将VP1映射到PP2,VP2映射到PP7。相似的,进程j将VP1映射到PP7,VP2映射到PP10.注意,多个虚拟页面可以映射到同一个`共享的物理页面`上。
按需页面调度和独立的虚拟地址空间的结合,对系统内存的使用和管理造成了深远的影响。VM简化了加载和链接、代码和数据的共享,以及应用程序的内存分配。
2.1简化链接
独立地址空间允许每个进程的内存映像使用相同的基本格式。例如在64位x86-64平台上,代码段总是从虚拟地址0x400000开始。数据段跟在代码段后,中间夹杂着对齐空白。栈占据用户进程地址空间的最高部分,并向下增长。这样的一致性极大地简化了链接器的设计和实现,运行链接器生成完全连接的可执行文件,这些可执行文件是独立于物理内存中代码和数据的最终位置的。
2.2简化加载
把目标文件(可执行文件和共享对象文件)中的.text和.data节加载到一个新创建的进程中,Linux加载器为代码和数据段分配虚拟页,把他们标记为无效的(即未被缓存的),将页表条目指向目标文件中适当的位置。然而,加载器从不从磁盘复制任何数据到内存中,而在每个页被初次引用时,或CPU取指令时,或一条正在执行的指令引用一个内存位置时,虚拟内存系统会按需自动调入数据页。
2.3简化共享
一般情况下,每个进程都有自己私有的代码、数据、堆、以及栈区域,是不和其他进程共享的。在这种情况下,操作系统创建页表,将相应的虚拟页映射到不连续的物理页面。
独立地址空间为操作系统提供了一个管理用户进程和操作系统自身之间共享的一致机制。
在部分情况下,进程间还是需要共享代码和数据的,例如每个C程序都会调用C标准库中的程序(printf)、都需要调用相同的内核代码。操作系统通过将不同进程中适当的虚拟页面映射到相同的物理页面,从而安排多个进程共享这部分代码的一个副本,而不是在每个进程中都包括单独的内核和C标准库的副本。
2.4简化内存分配
当运行在用户进程的程序要求额外的堆空间时(如调用malloc),操作系统分配k个连续的虚拟内存页面,并且将它们映射到物理内存中任意位置的k个任意的物理页面。由于页表的存在,操作系统没必要分配k个连续的物理页面,页面可随机地分散在物理内存中。
3.虚拟内存作为内存保护的工具
操作系统会控制进程对内存系统的访问,例如:
1. 不允许一个用户进程修改它的只读代码段;
2. 不允许用户进程读或修改任何内核中的代码和数据结构;
3. 不允许用户进程读或写其他进程的私有内存;
4. 不允许用户进程修改任何其他进程共享的虚拟页表
而提供独立的地址空间是的区分不同进程的私有内存变得容易。但是,地址翻译机制可以从一种自然的方式扩展到提供更好的访问控制。CPU每次生成一个地址时,地址翻译硬件都会读一个PTE。所以通过在PTE上添加额外的许可位来控制对一个虚拟页面内容的访问十分简单。
上图中,在每个PTE中添加了三个许可位。SUP位表示进程是否必须运行在内核模式下才能访问该页。运行在内核模式下的进程可以访问任何页面,但运行在用户模式中的进程只允许访问那些SUP为0的页面。READ和WRITE位控制对页面的读和写访问。例如,如果进程i运行在用户模式下,那么它有读VP0和读写VP1的权限。然而,不允许访问VP2。
如果一条指令违反了这些许可条件,那么CPU就会触发一个一般保护故障,将控制传递给一个内核中的异常处理程序。Liunx中一般将这种异常报告为段错误(segmentation fault)