第九章 虚拟存储器
将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,它高效地使用了主存。其实这一章还是讲解虚拟内存的,也就是和内核中的内存管理相关。
- 它为每个进程提供了一致的地址空间,从而简化了存储器管理
- 它保护了每个进程的地址空间不被其他进程破坏。
- 在硬件异常、汇编器、链接器、加载器、共享对象、文件和进程的设计中扮演着重要角色。
- 存储器让应用程序有了强大的能力,可以创建和销毁存储器片、将存储器片映射到磁盘文件的某个部分,以及与其他进程共享存储器。
一、物理和虚拟寻址
现在都开始虚拟地址,使用虚拟地址时,CPU通过生成一个虚拟地址来访问主存,这个虚拟地址在被送到存储器之前先转换成适当的物理地址。将一个虚拟地址转换为物理地址的任务叫做地址翻译。就像异常处理一样,地址翻译需要CPU硬件和操作系统之间的紧密合作。CPU芯片上叫做存储器管理单元的专用硬件,利用存放在主存中的查询表来动态翻译虚拟地址,该表的内容是由操作系统管理的。
二、地址空间
地址空间时一个非负整数地址的有序集合,如果地址空间中的整数是连续的,那么我们说它是一个线性地址空间。CPU从一个有N=2n个地址空间中生成虚拟地址,这个地址空间称为虚拟地址空间。一个地址空间的大小事由表示最大地址所需要的位数来描述的。一个包含N=2n个地址的虚拟地址空间就叫做一个n位地址空间。一个系统还有一个物理地址空间,它与系统中物理存储器的字节数目相对应。主存中的每个字节都有一个选自虚拟地址空间的虚拟地址和一个选自物理地址空间的物理地。
三、虚拟存储器作为缓存的工具
虚拟存储器被组织为一个由存放在磁盘上的N个连续的字节大小的单元组成的数组。每个字节都有一个唯一的虚拟地址,这个唯一的虚拟地址是作为到数组的索引的。磁盘上数据的内容被缓存在主存中。和存储器层次结构中其他缓存一样,磁盘上的数据被分割成块,这些块作为磁盘和主存之间的传输单元。
四、虚拟存储器作为存储器管理的工具
操作系统为每个进程提供了一个单独的页表,因而也就是一个独立的虚拟地址空间。在下图中,进程I的页表VP1映射到PP2,VP2映射到PP7。相似地,进程J的页表将VP1映射到PP7,VP2映射到PP0。多个虚拟页面可以映射到同一个共享物理页面上。
将一组连续的虚拟页映射到任意一个文件中的任意位置的表示法称作存储器映射。
五、虚拟存储器作为存储器保护的工具
六、地址翻译
实现翻译的过程,如何利用页表来实现这种映射。CPU中的一个控制寄存器,页表基址寄存器(PTBR)指向当前页表。n位的虚拟地址包含两个部分:一个p位的虚拟页面偏移和一个(n-p)位的虚拟页号。MMU利用VPN来选择适当的PTE(页表条目,一个页表其实就是多个页表条目的数组)。页面命中,CPU硬件执行的步骤:
- 处理器生成一个虚拟地址,并把它传送给MMU
- MMU生成PTE地址,并从高速缓存/主存请求得到它
- 高速缓存/主存向MMU返回PTE
- MMU构造物理地址,并把它传送给高速缓存/主存
- 高速缓存/主存返回所请求的数据字给处理器
页面命中完全是由硬件来处理的,与之不同的是,处理缺页要求硬件和操作系统内核协作完成:
- 前三步都是一样的
- PTE中的有效位为0,所以MMU触发了一次异常,传递CPU中的控制到操系统内核中的缺页异常处理程序。
- 缺页处理程序确定出物理存储器中的牺牲页,如果这个页面被修改了,则把它换出到磁盘。
- 缺页处理程序页面调入新的页面,并更新存储器中的PTE
- 缺页处理程序返回到原来的进程,再次执行导致缺页的指令。CPU将引起缺页的虚拟地址重新发送给MMU
上面是前三步的过程,在页面命中的时候进行的处理。如果页面没有命中。
整体过程将是上图所示。最后还是处理器再次发送虚拟地址给MMU,不过这次不会导致缺页异常。
ps:由上图可知,虚拟页偏移量(VPO)和物理页偏移量(PPO)大小是相等的,因为虚拟页偏移量就是虚拟页的大小,对于给定的虚拟地址位数和物理地址位数就可以构造各个量的大小。比如32位虚拟地址空间和24为物理地址,对于大小为1KB的页面。那么VPO和PPO的大小为10位,22位的虚拟页号,物理页号为12位。
但是在现在的系统中,都是结合高速缓存和虚拟存储器一起使用的,大多数系统都是选择物理地址来访问高速缓存。
每次CPU产生一个虚拟地址,MMU就必须查阅一个PTE,以便将虚拟地址翻译为物理地址。在最糟糕的情况下,这又会要求从存储器取一次数据,代价是几十到几百个周期。如果PTE碰巧在L1中,那么开销下降到1个或2个周期,试图先去这种开销,他们在MMU中包括了一个关于PTE的小的缓存,称为翻译后备缓冲器(TLB),它是一个小的。虚拟寻址的缓存,其中每一行都保存着一个由单个PTE组成的块。
下面看看在TLB命中和不命中的情况下都是什么处理流程:
CPU产生一个虚拟地址
MMU从TLB中取出相应的PTE
MMU将这个虚拟地址翻译成一个屋里地址,并且将它发送到高速缓冲/主存
高速缓存/主存将所请求的数据字返回给CPU。
如果TLB不命中时,MMU必须从L1缓存中取出相应的PTE,新取出的PTE存放在TLB中,可能覆盖一个已经存在的条目。
多级页表
如果我们有一个32位的地址空间,4KB的页面和一个4字节的PTE(就按照这样的说法,如果4KB的页面,那么虚拟页面的偏移位需要12位),那么即使应用所引用的只是虚拟地址空间中的很小的一部分,页总是需要一个4MB的页表驻留在存储器中(为啥需要4MB的空间呢,因为32位地址中间,4KB的页面,可能会用1MB的页面,PTE中存储的就是页面的地址,那么就是1MB个地址,每个地址需要4字节,那么需要4MB的空间存储一个地址空间)。
常用的方法就是压缩页表,使用层次结构的页表,假设32位虚拟地址空间被分为4KB的页,而每个页表条目都是4字节。还假设在这一时刻,虚拟地址空间有如下形式:存储器的前2K个页面分配给了代码和数据,接下来的6K个页面还未分配,再接下来的1023个页面页未分配,接来下的1个页面分配给了用户栈。
一级页表的每个PTE负责映射虚拟地址空间中一个4MB的片,这里每一片都是由1024个连续的页面组成的。假设地址空间为4GB,1024个PTE已经足够覆盖整个空间了。
如果偏i中的每个页面都未被分配,那么一级PTEi就为空,上图中的片2~7是未被分配的。如果在片i中至少有一个页是分配了的,那么一级PTEi就指向一个二级页表的基址。上图中的片0、1、8的所有或则部分已被分配,所以他们的一级PTE就指向二级页表。
二级页表中的每个PTE都负责映射一个4KB的虚拟存储器页面,就像我们查看一级页表一样。注意,使用4字节的PTE,每个一级和二级页表都是4KB字节,这刚好和一个页面的大小一样的(为啥呢?4字节的PTE,一个页表有1024个页表项,所以一个页表就是4KB)。
如果一级页表中的一个PTE是空的,那么相应的二级页表就根本不会存在,这代表这一种巨大的潜在节约;只有一级页表才需要总是存在主存中;虚拟存储器系统可以在需要时创建、页面调入或调出二级页表,这就减少了主存的压力;只有最经常使用的二级页表才需要缓存在主存中。
上图展示了使用k级页表层次结构的地址翻译。虚拟地址被划分为k个VPN和1个VPO,每个VPNi都是一个到第i级页表的索引,其中1<=i<=k。第k级页表中的每个PTE包含某个物理页面的PPN或一个磁盘块的地址。在能够确定PPN之前,MMU必须访问K个PTE。对于只有一级的页表结构,PPO和VPO是相同的。
访问k个PTE,第一眼看上去昂贵不切实际,这TLB能够起作用,是通过将不同层次上页表的PTE缓存起来。实际上,带多级页表的地址翻译并不比单级页表的慢很多。
七、存储器映射
linux虚拟存储器系统的存储映射问题:
linux为每个进程维护一个单独的虚拟地址空间。
内核虚拟存储器包含内核中的代码和数据结构。内核虚拟存储器的某些区域被映射到所有进程共享的物理页面。例如,每个进程共享内核的代码和全局数据结构。linux页将一组连续的虚拟页面映射到相应的一组连续的物理页面。这就为内核提供了一种遍历的方法来访问物理存储器中任何特定的位置,例如,当它需要访问页表,或在一些设备上执行存储器映射的I/O操作,而这些设备被映射到特定的物理储存器位置时。
内核虚拟存储器的其他区域包含每个进程都不相同的数据。例如,页表,内核在进程的上下文中执行代码时使用的栈,以及记录虚拟地址空间当前组织的各种数据结构。
上图强调了记录一个进程中虚拟存储器区域的内核数据结构。内核为系统中的每个进程维护一个单独的任务结构(源代码中的task_struct)。任务结构中的元素包含或者指向内核运行该进程所需要的所有信息(例如,PID,指向用户栈的指针、可执行目标文件的名字以及程序计数器)
task_struct中的一个条码指向mm_struct,它描述了虚拟存储器的当前状态。其中pgd指向第一级页表(页全局目录)的基址。而mmap指向一个vm_area_structs的链表,其中每个vm_area_structs都描述了当前虚拟地址空间的一个区域。当内核运行这个进程时,它就将pgdf存放在CR3控制寄存器中。
- vm_start:指向这个区域的起始处
- vm_end:指向这个区域的结束处
- vm_port:描述这个区域内包含的所有页的读写徐和权限
- vm_flags:描述这个区域内的页面是与其他进程共享的,还是这个进程私有的
- vm_next:指向链表中下一个区域结构。
ps:虚拟存储器是对主存的一个抽象。支持虚拟存储器的处理器通过使用一种叫做虚拟寻址的间接形式来引用主存。处理器产生一个虚拟地址,在被发送到主存之前,这个地址被翻译成一个物理地址。从虚拟地址空间到物理地址翻译要求硬件和软件紧密合作。专门的硬件通过使用页表来翻译虚拟地址,页表的内容是由操作系统提供的。
八 存储器映射
1、Linux通过将一个虚拟存储器区域与一个磁盘上的对象(object)关联起来,以初始化这个虚拟存储器区域的内容,这个过程称为存储器映射(memory mapping)。虚拟存储器区域可以映射到两种类型的对象:
1)unix文件系统中的普通文件:一个区域可以映射到一个普通磁盘文件的连续部分。
2)匿名文件:一个区域也可以映射到一个匿名文件,匿名文件是由内核创建的,包含的全是二进制零。
一旦一个虚拟页面被初始化了,它就在一个由内核维护的专门的交换文件(swap file)间换来换去。
2、共享对象
一个对象可以被映射到虚拟存储器的一个区域,要么作为共享对象,或作为私有对象
3、私有对象,写时拷贝(copy-on-write)
私有对象是使用一种叫做写时拷贝的技术被映射到虚拟存储器中的。如图中所示:只要有一个进程试图写私有区域内的某个页面,那么这个写操作就会触发一个保护策略。它就会在物理存储器中创建这个页面的一个新拷贝,更新页面条目指向这个新的拷贝,然后恢复这个页面的可写权限。
4、unix进程可以使用mmap函数来创建新的虚拟存储器区域,并将对象映射到这些区域中。
九 动态存储器分配
1、需要额外的虚拟存储器时,使用一种动态存储器分配器(dynamic memory allocator)。一个动态存储器分配器维护着一个进程的虚拟存储器区域,称为堆(heap)。在大多数的unix系统中,堆是一个请求二进制0的区域;对于每个进程,内核维护着一个变量brk,它指向堆的顶部。
2、分配器将堆视为一组不同大小的块(block)的集合来维护。每个块就是一个连续的虚拟存储器组块(chunk),要么是已分配的,要么是未分配的。
1)显式分配器(explicit allocator):如通过malloc,free或C++中通过new,delete来分配和释放一个块。
2)隐式分配器(implicit allocator):也叫做垃圾收集器(garbage collector)。自动释放未使用的已分配的块的过程叫做垃圾回收(garbage collection)。
3、malloc不初始化它返回的存储器,calloc是一个基于malloc的包装(wrapper)函数,它将分配的存储器初始化为0。想要改变一个以前已分配的块的大小,可以使用realloc函数。
4、分配器必须对齐块,使得它们可以保存任何类型的数据对象。在大多数系统中,以8字节边界对齐。
不修改已分配的块:分配器只能操作或者改变空闲块。一旦被分配,就不允许修改或者移动它。
5、碎片(fragmentation)
有内部碎片(internal)和外部碎片(external)。
外部碎片:在一个已分配块比有效载荷在时发生的。(如对齐要求,分配最小值限制等)
外部碎片:当空闲存储器合计起来足够满足一个分配请求,但是没有一个单独的空闲块足够大可以来处理这个请求时发生的。
6、隐式空间链表
放置分配的块的策略有:首次适配(first fit),下一次适配(next fit),和最佳适配(best fit)。
如果空闲块已经最大程度的合并,而仍然不能生成一个足够大的块,来满足要求的话,分配器就会向内核请求额外的堆存储器,要么是通过调用nmap,要么是通过调用sbrk函数;分配器都会将额外的(增加的)存储器转化成一个大的空闲块,将这个块插入到空闲链表中,然后将被请求的块放置在这个新的空闲块中。
7、一种流行的减少分配时间的方法,称为分离存储(segregated storage),维护多个空闲链表,其中每个链表中的块有大致相等的大小。
十 垃圾回收
1、垃圾收集器将存储器视为一张有向可达图(reachability graph)。
2、Mark%Sweep垃圾收集器由标记(mark)阶段和清除(sweep)阶段组成。标记阶段标记出根节点的所有可达的和已分配的后继,而后面的清除阶段释放每个被标记的已分配块。典型地,块头部中空闲的低位中的一位来表示这个块是否被标记了。
3、指针的算术运算是以它们指向的对象的大小为单位来进行的。
十一 常见错误
1.读未初始化的存储器
2.允许栈缓冲区溢出
3.假设指针和他们指向的对象时相同大小的
4.引用坏指针
5.造成错位错误
6.引用指针而不是它所指向的对象