CSAPP学习笔记—虚拟内存

CSAPP学习笔记—虚拟内存

 

 

符号说明

 

虚拟内存地址寻址

    图9-12展示了MMU如何利用页表来实现这种映射。CPU中的一个控制寄存器,页表基址寄存器(Page Table Base Register,PTBR)指向当前页表。N位的虚拟地址包含两个部分:一个p位的虚拟页表偏移(Virtual Page Offset,VPO)和一个(n-p)位的虚拟页号(Virtual Page Number,VPN)。MMU利用VPN来选择适当的PTE。将页表条目中物理页号(Physical Page Number,PPN)和虚拟地址中的VPO串联起来,就得到了相应的物理地址。需要注意的是物理和虚拟页面都是p字节的,所以物理页面偏移(Physical Page Offset,PPO)和VPO是相同的。

    下图展示了当页面命中时,CPU硬件执行的步骤。

第一步:处理器生成一个虚拟地址,并把它传送给MMU。

第二步:MMU生成PTE地址,并从高速缓存/主存请求得到它。

第三步:高速缓存/主存向MMU返回PTE。

第四步:MMU构造物理地址,并把它传送给高速缓存/主存。

第五步:高速缓存/主存返回所请求的数据字给处理器。

页面命中完全是由硬件来处理的,与之不同的是,处理缺页要求硬件和操作系统内核协作完成。过程如下图所示:

第一步到第三步和上图的过程是一样的。

第四步:PTE中的有效位是0,所以MMU触发了一次异常,传递CPU中的控制到操作系统内核中的缺页异常处理程序。

第五步:缺页处理程序确定出物理内存中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘。

第六步:缺页处理程序页面调入新的页面,并更新内存中的PTE。

第七步:缺页处理程序返回到原来的进程,再次执行导致缺页的命令,CPU将引起缺页的虚拟地址重新发送给MMU。因为虚拟页面现在缓存在物理内存中,所以就会命中。

利用TLB加速地地址翻译

许多系统会在MMU中包括一个关于PTE的小的缓存,称为翻译后备缓冲器(Translation Lookaside Buffer,TLB)。

    TLB是一个小的、虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。TLB通常有高度的相联度。

    如图所示,用于组选择和行匹配的索引和标记字段是从虚拟地址中的虚拟页号中提取出来的。如果TLB有T=个组,那么TLB索引(TLBI)是由VPN的t个最底位组成的,而TLB标记(TLBT)是由VPN中剩余的位组成的。

多级页表

    如果我们有一个32位的地址空间,4KB的页面和一个4字节的PTE,那么即使应用所引用的只是虚拟地址空间很小的一部分,也总是需要一个4MB的页表驻留在内存中。对于地址空间为64位的系统来说,问题将变得更复杂。

这种方法从两个方面减少了内存要求。第一,如果一级页表中的一个PTE是空的,那么对应的二级页表就根本不会存在。这代表着一种巨大的潜在节约,因为对于一个典型的程序,4GB的虚拟地址空间的大部分都是未分配的。第二,只有一级页表才需要总是在主存中;虚拟内存系统可以在需要时创建、页面调入或调出二级页表,这就减少了主存的压、力。只有最经常使用的二级页表才需要缓存在主存中。

    对于K级的页表层次结构的地址翻译如下:

    

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

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

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

Core i7 地址翻译

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

    下图中给出了第一级,第二级,第三极页表中条目的格式。

    当P=1时(Linux中就总是如此),地址字段包含了一个40位物理页号(PPN),它指向适当的页表的开始处。注意,这强加一个要求,要求物理页表4KB对齐。

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

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

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

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

Linux虚拟内存系统

    Linux为每个进程维护了一个单独的虚拟地址空间,它包括代码,数据,堆,共享库以及栈段。

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

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

Linux虚拟内存区域

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

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

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

    

内存映射

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

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

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

    无论哪种情况下,一旦一个虚拟页面被初始化了,它就在一个由内核维护的专门的交换文件(swap file)之间换来换去。交换文件也叫作交换空间(swap space)或者交换区域(swap area)。需要意识到的很重要的一点是,在任何时刻,交换空间都限制着当前运行着的进程能够分配的虚拟页面的总数。

小结

    虚拟内存是对主存的一个抽象。支持虚拟内存的处理器通过使用一种叫做虚拟寻址的间接形式来引用主存。处理器产生一个虚拟地址,在被送到主存之前,这个地址被翻译成一个物理地址。从虚拟地址空间到物理地址空间的地址翻译要求应硬件和软件紧密合作。专门的硬件通过使用页表来翻译虚拟地址,而页表的内容是由操作提供的。

    虚拟内存提供三个重要的功能。第一,它在主存中自动缓存最近使用的存放磁盘上的虚拟地址空间的内容。虚拟内存缓存中的块叫做页。对磁盘上页的引用会触发缺乏,缺页将控制转移到操作系统中的一个缺页处理程序。缺页处理程序将页面从磁盘复制到主存缓存,如果必要,将写回被驱逐的页。第二,虚拟内存简化了内存管理,进而又简化了链接,在进程间共享数据,进程的内存分配以及程序加载。最后,虚拟内存通过在每条页表条目加入保护位,从而简化了内存保护。

    地址翻译的过程必须和系统中所有的硬件缓存的操作集成在一起。大多数页表条目位于L1高速缓存中,但是一个称为TLB的页表条目的片上高速缓存,通常会消除访问在L1上的页表条目的开销。

    现代系统通过将虚拟内存片和磁盘上的文件片关联起来,来初始化虚拟内存片,这个过程称为内存映射。内存映射为共享数据、创建新的进程以及加载程序提供了一种高效的机制。应用可以使用mmap函数来手工地创建和删除虚拟地址空间的区域。然而,大多数程序依赖于动态内存分配器,例如malloc,它管理虚拟地址空间区域内一个称为堆的区域。动态内存分配器是一个感觉像系统级程序的应用级程序,它直接操作内存,而无需类型系统的很多帮助。分配器有两种类型。显示分配器要求应用显示释放它们的内存块。隐式分配器(垃圾收集器)自动释放任何未使用的和不可达的块。

    对于C程序员来说,管理和使用虚拟内存是一件困难和容易出错的任务。常见的错误示例包括:间接引用坏指针,读取未初始化的内存,允许栈缓冲区溢出,假设指针和它们指向的对象大小相同,引用指针而不是它所指向的对象,误解指针运算,引用不存在的变量,以及引起内存泄漏。

posted on 2018-11-10 12:56  kexinxin  阅读(773)  评论(0编辑  收藏  举报

导航