在分析了进程的虚拟地址布局,我们转向内核以及他管理用户内存的机制。下图是gonzo的例子:
Linux进程在内核中是由task_struct进程描述符实现的,task_struct的mm字段指向内存描述符mm_struct,他是进程的一个内存执行摘要。如上图所示,mm_struct存储了内存各个段的开始和结束地址、进程所使用的内存页面数(rss代表常驻集合大小)、使用的虚拟地址空间总数等等。在内存描述符中我们也可以找到两个用于管理进程内层的字段:虚拟内存集合和页表。Gonzo的内存区域如下图:
每个虚拟内存区域(VMA)是一个虚拟地址空间上连续的区域;这些区域不会彼此覆盖。Vm_area_struct结构描述了一个内存区域,包括他的开始和技术地址、flags字段指定了他的行为和访问权限,vm_file字段指定了该区域映射的实际文件。一个没有映射文件的VMA成为匿名的。除了内存映射段以外,上面的每个内存段(堆、栈等等)相当于一个单独的VMA。这不是必须的,尽管在x86机器上通常是这样。VMA不会关心他在哪个段里面。
一个进程的所有VMA以两种方式存储在他的内存描述符中,一种是以链表的方式存放在mmap字段,以开始虚拟地址进行了排序,另一种是以红黑树的方式存放,mm_rb字段为这颗红黑树的根。红黑树可以让内核根据给定的虚拟地址快速地找到内存区域。当我们读取文件/proc/pid_of_process/maps,内核仅仅是通过进程VMA的链接同时打印出每一个。
在windows中,块EPROCESS基本上是task_struct和mm_struct的结合体。Windows用虚拟地址描述符,或者说VAD,模拟一个VMA;VAD存储在一个AVL树(平衡二叉树)中。你知道有关Windows和Linux的最有趣的事情是什么?那就是他们之间差异很小。
4GB大小的虚拟地址空间被分为一个个页面。32位的x86处理器支持的页面大小为4KB、2MB和4MB。Linux和windows都使用4KB大小的页面来映射用户空间部分的虚拟地址空间。0~4095字节为页面0,4096~8191字节为页面1等等。VMA的大小必须是一个页面大小的整数倍。下图是4KB页面大小模式的3GB用户空间:
处理器借助页表将虚拟地址转换为物理地址。每个进程有他自己的页表集合;每当一个进程切换发生,他用户空间的页表也随着切换。Linux在进程的内存描述符中存放了一个pgd字段指向进程的页表。每一个虚拟页面对应与页表中的一个页表入口(PTE),这个入口通常在x86下是一个简单的4字节大小:
Linux有对PTE中每个标志进程读取和设置的函数。标志位P高速处理器虚拟页面在物理内存中是否处于当前。如果清空(等于0),访问该页将触发一个缺页中断。要记住的是当该位为0时,其余的字段都无效。R/W位表示读/写;如果清空,该页为只读。标志位U/S表示用户/管理;如果清空,那么只有内核能够对他进行访问。这些标识用来实现内存的只读以及对内核空间进行保护,就像前面我们说的。
标志位D和A是写脏位和访问控制位。一个脏页是已经被写过的页,而一个被访问的页是已经被写过或者读过的页。这两个标志位的相同点是:处理器只设置他们,而内核负责来清空他们。最后,PTE保存页面的起始物理地址,4KB对齐。这幼稚的前瞻域其实是痛苦的源泉,他限制了可寻址的物理内存为4GB。另一个PTE为的是另一件事情,即PAE。
一个虚拟页面是内存保护的一个单元,因为他的所有字节共享U/S和R/W标志位。不管怎样,带有不用标志位、不同的页面可以映射相同的物理内存。注意在PTE中看不到他的执行权限。这就是经典x86分页允许在栈上执行代码的原因,这样很容易利用栈缓存溢出(当然,也可以利用不可执行栈使用返回到libc或其他技术)。缺少PTE的一个不可执行标志说明了一个广泛的事实:在VMA中的权限标志可能会也可能不会完全转化为硬件保护。内核做了他力所能及的,但是最终体系限制了这种可能。
虚拟内存没有存储任何东西,他只是简单的映射一个程序的地址空间到相关的物理内存,这一大块物理内存叫做物理地址空间。然而在总线上的内存操作多少有些涉及,在这里我们可以忽略并假定物理地址范围从0到最大的可用内存以一个字节的形式增长。物理地址空间被内核分解成一个个页框。处理器不我知道也不关心页框,然而他们对内核来说很关键因为页框是物理内存管理器的单元。在32位模式下linux和windows都使用4KB大小的页框;这里有一个装有2GB RAM机器的例子:
在linux中每个页框由一个描述符和几个标志描述。这些描述符一起跟踪计算机中物理内存入口;每个页框精确的状态总是指到的。物理内存由伙伴内存分配技术管理,如果一个页框能通过伙伴系统分配那么他是空闲的,也就是可分配的。一个分配的页框可能是匿名的,持有程序数据,他可能在页面缓存中,持有的数据存储在一个文件或者块设备中。当然页框还有其他用途,但是我们现在不考虑这些。Windows有一个类似的页框号(PFN)数据库来描述物理内存。
让我们把虚拟内存区、页表入口和页框放在一起来说明这一切是怎么工作的。下面是一个用于堆的例子:
蓝色矩形框代表在VMA区域中的页面,箭头代表页框中映射到页面的页表项。一些虚拟页面没有箭头;这意味着他们对应的PTE的Present标志位为0.这可能是这些页面没有被映射或者他们的内容已经被换出。在任何一种情况下访问这些页面都会导致缺页中断,尽管他们在VMA中。VMA和页表之间的这种关系可能看起来很奇怪,但是这是经常发生的。
VMA就像是一个在你的程序和内核之间的契约。你要求一些事情被处理(内存分配、文件映射等等),内核说:“可以”,并且创建或者更新合适的VMA。但是他实际上并不履行请求权,他会等待指到一个缺页中断发生后才去做实际的工作。内核很懒,就是一个骗人的败类;这是虚拟内存的基本原则。这应用到大多数情形下,一些熟悉的一些令人吃惊的,但是规则是VMA记录达成了什么协议,而PTE反映内核实际做了什么。这两个数据结构一起管理一个程序的内存;包括解决缺页中断、释放内存、换出内存等等。让我们举一个内存分配的简单例子:
当程序通过brk()系统调用申请更多的内存空间时,内核简单地更新堆的VMA。在这一点上,没有页框做实际的分配并且新分配的页面在内存中不是处于当前的。一旦程序进入这些页面,处理器缺页中断发生并且do_page_fault()被调用。他使用find_vma()函数搜索覆盖缺页中断虚拟地址空间的VMA。如果找到,VMA上的权限(读或者写)也会再次被检查。如果没有找到合适的VMA,没有契约覆盖试图进入的内存区,处理器产生段错误。
当找到一个VMA,内核必须查看PTE内容和VMA的类型来处理这个缺页中断,在我们的例子中,PTE显示的页面不是当前的。实际上,我们的PTE是完全空白的(全是0),在linux中意味着虚拟页面没有被映射。一旦这是一个匿名VMA,我们必须由do_anonymous_page()来处理一个纯RAM事务,他分配一个页面帧,用它来映射发生缺页异常的虚拟页面。
事情可能会有些不同。对一个换出页的PTE,例如,Present标志位为0但是整个不是0。他存储持有页面内容的交换位置,这个位置必须用do_swap_page()函数从磁盘上读取加载到一个页面,该函数被一个异常调用。
这里总结了内核的用户内存管理器开始一半,在接下来的文章中,我们把文件加入进来,建立一个完整的内存基础构架图,包括他们实现的性能。