深入理解操作系统(26)第十章:虚拟存储器(3)案例研究:Pentium/Linux内存系统(Pentium存储器系统,地址翻译/linux 虚拟存储器区域,缺页异常处理)
1. Pentium 存储器系统
1.1 Pentium 存储器系统的重要部分组成
我们以一个实际系统的案例研究来概括我们对缓存和虚拟存储器的讨论,这里选用的系统是pentium类的系统(第三版是core i7),运行的是Linux。
图10.22给出了Pentium存储器系统的重要部分。
图10.22
Pentium系统有一个32位(4GB)的地址空间。
处理器组件(processor package)包括CPU芯片、一个统一的L2高速缓存和一个连接它们的高速缓存总线(背板总线)。
CPU芯片适当地包含了四个不同的缓存:一个指令TLB、数据TLB、L1 i-cache以及L1 d-cache。TLB是虚拟寻址的。
L1和L2缓存是物理寻址的。Pentium中的所有缓存(包括B)都是四路组相联的。
说明:
TLB缓存32位的页表条目。
指令TLB缓存取指单元产生的虚拟地址的PTE(PTE:page table entry页表条目)。
数据TLB缓存数据的虚拟地址的PTE。
指令TLB有32个条目。数据TLB有64个条目。页面大小可以在启动时配置成4kb或者4MB。运行在pentium上的Linux使用4KB的页面。
LI和L2高速缓存的块大小为32字节。每个L1高速缓存的大小是16KB,有128个组,其中每个组都包含4行。
L2高速缓存的大小可以在最小值]28到最大值2MB之间变化。典型的大小是512KB。
1.2 Pentium 地址翻译
图10.23 Pentium 地址翻译概况
1.2.1 Pentium 页表
每个Pentium系统都使用如图10.24所示的两级页表。
第一级页表,叫做页面目录(page directory),包含用24个32位的PDE(page directory entry,页面目录条目),
其中每一个条目都指向1024个2级页表中的一个。
每个页表包含1024个32位的PTE(页表条目),其中每个都指向物理存储器或者磁盘上的一个页面。
图10.24
每个进程都有一个惟一的页面目录和页表集合。
当一个Linux进程正在运行时,尽管Pentium的体系结构允许页表换进换出,但是页表目录和与己分配页面相关的页表都是常驻存储器的。页面目录基址寄存器(page base register PDBR)指向页表目录的起始位置。
图10.25(a)展示了PDE的格式。当P=1时(L1nux中总是这样的),地址字段中包含一个20位的物理页号,指向相应的页表的起始位置。注意,这要求页表要4KB对齐。
图10.25(a)
图10.25(b)展示了PTE的格式。当P=I时,地址字段包含一个20位的物理页号,指向物理存储器中某个页的基址。同样,这也要求物理页要4KB对齐。
图10.25(b)
说明:
1. PTE有两个许可位,用来控制对这个页面的访问。
R/W位确定这个页面的内容是可读/可写的,还是只读的。
U/S位确定是否可以在用户模式下访问这个页面,
这就保护了操作系统内核中的代码和数据不受用户程序的影响。
2. 就像MMU翻译每个虚拟地址一样,它也会更新两个其他的位,内核的缺页处理程序可能会使用这两位。
每次访问一个页面时,MMU就设置A位,也叫做引用位(reference bit)。内核可以用引用位来实现它的页面替换算法。
每次写页面时,MMU就设置D位,也叫做修改位(diny bit)。
3. 一个己经被修改了的页面有时也叫做修改页面(dirty page)。
修改位告诉内核在它拷入一个替代页面时,是否必须写回牺牲页面。
内核可以调用一个特殊的内核模式指令来清除引用或者修改位。
旁注:执行许可和缓冲区溢出攻击
注意,Pentium页表条目缺少一个执行许可位,用来控制一个页面的内容是否可以被执行。
缓冲区溢出攻击利用了这个疏漏,在用户栈上直接加载和运行代码。如果有这样一个执行位,那么内核就可以通过限制对只读代码段的执行权限,来消除这种攻击的威胁了.
1.2.2 Pentium 页表翻译
图10.26展示了Pentium MMU如何使用两级页表,将一个虚拟地址翻译成物理地址。
20位的VPN被分成2个10位的块。
VPN1在PDBR指向的页目录中索引一个PDE,PDE中的地址指向的某个页表的基址被VPN2索引。
被VPN2索引的PTE中的PPN和VPO连接起来形成了物理地址。
图10.26
1.1.3 Pentium TLB翻译
图10.27描绘了Pentium系统中TLB翻译的过程。
如果PTE被缓存在TLBI索引的组里(TLB命中),那么就从这个缓存的PTE中抽取出PPN,并把这个PPN和VPO连接起来形成物理地址。
如果没有缓存PTE,但是缓存了PDE(部分命中),那么MMU必须在它形成物理地址之前,从存储器中提取相应的PTE。
最后,如果PDE和PTE都没有被缓存(TLB不命中),那么MMU必须从有储器中取出PDE和PTE,以形成物理地址。
图10.27
2. linux 虚拟存储器系统
一个虚拟存储器系统要求硬件和内核软件之间的紧密协作,然而对此完整的阐释超出了我们讨论的范围,在这一小节中我们的目标是对Linux的虚拟存储器系统做一个描述,使你能够大致了解一个实际的操作系统是如何组织虚拟存储器,以及如何处理缺页的。
inux为每个进程维持了一个单独的虚拟地址空间,形式如图10.28所示。
图10.28
我们己经多次看到过这幅图了,包括它那些熟悉的代码、数据、堆、共享库以及栈段。既然我们已理解地址翻译,我们就能够填入更多的关于内核虚拟存储器的细节了,部分虚拟存储器位于地址0xc0000000之上。
内核虚拟存储器包含内核中的代码和数据。
内核虚拟存储器的某些区域被映射到所有进程共享的物理页面。
例如,每个进程共享内核的代码和全局数据结构。有趣的是,Linux也将一组连续的虚拟页面(大小等于系统中DRAM的总量)映射到相应的一组连续的物理页面。这就为内核提供了一种便利的方法,来访问物理存储器中任何特定的位置,例如,当它需要在一些设备上执行存储器映射的I/O操作时,而这些设备被映射到特定的物理存储器位置。
内核虚拟存储器的其他区域包含每个进程都不相同的数据。示例包括页表、内核在进程的上下文中执行代码时使用的栈,以及记录虚拟地址空间当前组织的各种数据结构。
2.1 linux 虚拟存储器区域
Linux将虚拟存储器组织成一些区域(也叫做段)的集合。
一个区域(area)就是己经存在着的(已分配的)虚拟存储器的连续组块(chunk),这些虚拟存储器的页面是以某种方式相关联的。
例如,代码段、数据段、堆、共享库段,以及用户栈都是不同的区域。
每个存在的虚拟页面都保存在某个区域中,而不属于某个区域的虚拟页是不存在的,并且不能被进程引用。
区域的概念很重要,因为它允许虚拟地址空间有间隙。内核并不记录那些不存在的虚拟页,而这样的页面也不占用存储器,磁盘或内核本身的额外资源。
2.1.1 一个linux进程虚拟存储器(图)
图10.29强调了记录一个进程中虚拟存储器区域的内核数据结构。
图10.29
内核在系统中为每个进程维护一个单独的任务结构(源代码中的task_struct)。任务结构中的元素包含或者指向内核运行该进程所需要的所有信息(例如·PID、指向用户栈的指针,可执行目标文件的名字.以及程序计数器)。
task_struct中的一个条目指向mm_struct。它描述了虚拟存储器的当前状态。
我们感兴趣的两个字段是pgd和mmap,其中pgd指向页面目录表的基址,而mmap指向一个vm_area_structs(区域结构)的链表,其中每个vm_area_structs都描述了当前虚拟地址空间的一个区域(area)。当内核运行这个进程时.它就将pgd存放在PDBR控制寄存器中。
为了我们的目的、一个貝体区域的区域结构(vm_area_structs)包含下面的字段:
vm_start:指向这个区域的起始处。
vm_end:指向这个区域的结束处。
vm_port:描述这个区域内包含的所有页面的读写许可权限。
vm_flags:描述这个区域内的页面是否是与其他进程共享的,
还是这个进和私有的(还描述了其他一些信息)。
vm_next:指向链表中下一个区域结构。
2.2 linux 缺页异常处理
L假设我们在试图翻译某个虚拟地址A时,触发了一个缺页。这个异常导致控制转移到内核的缺页处理程序,处理程序随后就执行下面的步骤:
1. 虚拟地址A是合法的吗?
换句话说,A在某个区域结构(vm_area_struct)定义的区域内吗?
为了回答这个问题,缺页处理程序搜索区域结构的链表,把A和每个区域结构中的vm-start和vm-end做比较。如果这个指令是不合法的,那么缺页处理程序就触发一个段错误,从而终这个进程。如图中的1。
因为一个进程可以创建任意数量的新虚拟存储器区域(使用在下一节中描述的mmap函数),所以顺序搜索区域结构的链表花销可能会很大。因此在实际中,使用某些我们没有显示出来的字段,Linux在链表中添加了一棵树,并在这棵树上进行查找。
2. 试图进行的对存储器的访问是否合法?
换句话说,进程是否有读或者写这个区域内页面的权限?
例如,这个缺页是不是由一条试图对这个代码段里的只读页面进行写操作的存储指令造成的?
这个缺页是不是因为一个运行在用户模式中的进程试图从内核虚拟存储器中读取字造成的?
如果试图进行的访问是不合法的,那么缺页处理程序会触发一个保护异常,从而终止这个进程。
如图中的2。
3.此刻,内核知道了这个缺是由于对合法的虚拟地址进行合法的操作造成的。
它选择一个牺牲页面,如果这个牺牲页面被修改过,别么就将它交换出去,换入新的页面,并更新页表,从而处理这次缺页。当缺页处理程字返回时,CPU重新启动引起缺页的指令,这条指令将再次发送A到MMU。这一回,MMU就能正常地翻译A而不会再产生一个缺页中断了。