一个进程的所有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和页表之间的这种关系可能看起来很奇怪,但是这是经常发生的。