本文章以Linux为例,讲解一下虚拟内存系统的工作原理,windows系统的原理也是大同小异,有兴趣的读者可以自行查阅相关资料。
linux内核以及它管理用户内存的机制,下面我们以应用程序gonzo的内存示意图为例,进行详细说明。
Linux进程在内核中是以一个task_struct实例来实现的,称为进程描述符。task_struct的mm字段指向了内存描述符,即mm_struct,它是一份可执行程序的内存结构概要。如上图所示,它存储了内存各个内存端的起始位置和结束位置,进程使用的物理内存页的数量,进程使用的虚拟地址空间等信息。在内存描述符内部,还有两个内存管理的重要结构:virtual memory areas和page tables。下图就是Gonzo的内存区域示意图:
每一个virtual memory area(VMA)都是一段连续的虚拟内存地址,这些内存区域绝不会重合。一个vm_area_struct描述一个内存区域,包括了它的起始地址和结束地址,内存访问权限标志位,以及一个vm_file字段(如果有该字段的话,用来指定哪个文件映射到了该内存区域)。VMA不会映射匿名文件。进程内存布局中除了内存映射段外的每一个内存段都对应一个VMA。这种方式尽管在X86机器上很常见,但这并不是硬性要求。VMA们并不关心它们对应的是哪个段。
一个程序的VMA们都是作为一个链表存在于内存描述符的mmap字段中的,并且按照虚拟地址进行了排序,并且是一个以mm_rb为根节点的红黑树。采用红黑树的数据结构是为了方便内核给定虚拟地址后快速查找对应的内存区域。当你读/proc/pid_of_process/maps这个文件时,内核就是简单的遍历进程的VMA链表并挨个打印。
Windows中的EPROCESS块就是task_struct和mm_struct的混合,它对应于VMA的是一个称为Virtual Address Descriptor(VAD)的数据结构,VAD们存储在一个AVL树中。有意思的是,Windows和linux的区别真的很小。
4GB的虚拟内存地址空间被分成很多页。X86处理器在32位模式下支持4KB、2MB以及4MB大小的页。Linux和Windows都是用的4KB的页来分割用户虚拟地址空间的。0-4096字节落在page 0,4096-8192字节落在page 1,以此类推。VMA的大小一定是page大小的倍数。下图就是用4KB的页分割的3GB用户虚拟地址空间示意:
处理器利用page tables来将虚拟内存地址转换为物理内存地址。每个进程都有自己的page tables,无论进程切换何时发生,用户态的page table也会跟着切换。Linux在内存描述符中的pgd字段存储了一个指向该进程page tables的指针。每一个虚拟内存页对应一个page table entry,一个X86的页的结构如下:
Linux有函数来读取和设置PTE中的每一个标志位。位P告诉处理器该虚拟页是否要在物理内存中呈现。如果设为0,访问该页时会触发一个页错误。R/W标志位代表了读写权限,如果为0则该页为只读。U/S标志位代表了普通用户和超级用户,如果设置为0,则该页只能被内核访问。这些标志位都是用来实现前面看到的只读内存和内核态地址空间的。
D和A标志位代表了dirty和accessed,一个脏页表示该页已经被写过,一个被访问过的页表示该页被读过或者写过。最后,PTE存储了其对应的物理内存地址的起始地址,4KB对齐。
内存保护是以页为单位进行的,因为每个页都共用U/S和R/W标志位。但是同一个物理内存页可以对应多个虚拟内存页,这些不同的虚拟内存也可能有不同的保护标志位,所以要记住:在VMA设置的权限标志位不一定真正的用到了物理内存的保护上。
虚拟内存不存储任何东西,它只是简单的将程序的地址空间映射到底层的物理内存空间,物理地址空间才是处理器真正操作的内存空间。物理地址空间也被分成了以页为单位的大小。每个页是物理内存管理的最小单位。32位Linux和Windows都是以4KB为大小划分页的。下图所示为一个2GB大小的RAM
我们把virtual memory areas,page table entry以及page frame放在一起,理解一下它们是如何协作的,下图是一个用户堆的示例:
蓝色部分代表了VMA对应的地址范围,每一项都是一个page table entry,每个箭头代表从PTE到物理page frame的映射,某些PTE没有箭头,代表这些PTE的P标志位被清零了。这可能是因为这些页从来没有被访问过或者页已经被换出了。无论哪种情况,访问这些页都会导致缺页错误。
一个VMA就像一份你的程序和内核之间合约,你要求完成一些事情,比如内存分配、文件映射等,内核说没问题,然后它创建或者更新合适的VMA。但是为了效率,内核不会立马相应你的请求,直到第一次访问页产生缺页错误时才会去做,这也是虚拟内存的设计原则。
让我们看下所有的这些数据结构联合起来是如何工作的,下图是一个内存分配的示例:
当程序通过brk()系统调用请求更多的内存时,内核简单的更新堆的VMA,这时并没有page frame分配。
文章参考翻译自:https://manybutfinite.com/post/how-the-kernel-manages-your-memory/