Still Shines.

How The Kernel Manages Your Memory

Word count: 2.2k / Reading time: 7 min
2020/04/24 Share

翻译自 https://manybutfinite.com/post/how-the-kernel-manages-your-memory/(懒了,80%以上机翻。。)

———————————————————禁止ZDDR白嫖结界———————————————————

在讨论完进程在虚拟地址中的布局后之后,我们转向内核及其管理用户内存的机制。同样使用gonzo的程序举例:
<–!more–>

Linux kernel mm_struct

Linux进程在内核中是以一个task_struct实例来实现的,称为进程描述符。task_struct的mm字段指向了内存描述符,即mm_struct,它是一份可执行程序的内存结构概要。如上图所示,它存储了内存各个内存端的起始位置和结束位置,进程使用的物理内存页的数量,进程使用的虚拟地址空间等信息。在内存描述符内部,还有两个内存管理的重要结构:虚拟内存区域(virtual memory areas)和页表(page tables)。Gonzo的内存区域如下图所示:

Kernel memory descriptor and memory areas

每个虚拟内存区域(VMA)是一个连续的虚拟地址范围;这些区域从不重叠。vm_area_struct的一个实例完整地描述了一个内存区域,包括它的起始地址和结束地址、flags来确定访问权限和行为,以及vm_file字段来指定该区域正在映射哪个文件(如果有的话)。不映射文件的VMA是匿名的。上面的每个内存段(例如*、堆、栈)对应于一个单独的VMA,内存映射段除外。这不是必须的,尽管它在x86机器中很常见。VMAs不关心它属于哪个段。

程序的vma都存储在它的内存描述符的链表mmap领域,下令开始虚拟地址, red-black tree 扎根在 mm_rb领域。红黑树允许内核快速搜索覆盖给定虚拟地址的内存区域。当您读取文件’ /proc/pid_of_process/maps ‘时,内核只是遍历进程的VMAs链接列表并打印每一个

在Windows中,EPROCESS块大致是task_struct和mm_struct的混合。与VMA类似的Windows操作系统是虚拟地址描述符,或VAD;它们存储在一个AVL tree.中。你知道Windows和Linux最有趣的地方是什么吗?这是一些小的区别。

4GB虚拟地址空间分为。32位模式的x86处理器支持4KB、2MB和4MB的页面大小。Linux和Windows都使用4KB页面来映射虚拟地址空间的用户部分。0-4095字节位于第0页,4096-8191字节位于第1页,依此类推。VMA 的大小必须是页面大小的倍数。以下是4KB页面中3GB的用户空间:

4KB Pages Virtual User Space

处理器参考页表将虚拟地址转换为物理内存地址。每个进程都有自己的一组页表;每当发生进程切换时,也会切换用户空间的页表。Linux在内存描述符的pgd字段中存储一个指向进程页表的指针。每个虚拟页对应一个页表条目** (PTE),在常规的x86分页中是一个简单的4字节记录,如下图所示:

x86 Page Table Entry (PTE) for 4KB page

Linux具有读取和设置PTE中的每个标记的功能。位P告诉处理器虚拟页是否在物理内存中存在。如果清除(等于0),则访问该页将触发页错误。请记住,当这个位为0时,内核可以对剩下的字段做它想做的任何事情。R/W旗代表读/写;如果清除,页面是只读的。U/S标志代表用户/管理员;如果清除,则该页面只能由内核访问。这些标志用于实现我们前面看到的只读内存和受保护的内核空间。

位D和A分别表示脏页访问。脏页面具有写操作,而已访问的页面具有写操作或读操作。两个标志都是粘性的:处理器只设置它们,它们必须由内核清除。最后,PTE存储与此页面对应的初始物理地址,并对齐到4KB。这个看起来很天真的字段是一些痛苦的根源,因为它将可寻址物理内存限制为4 GB。其他的PTE字段是另一天的,物理地址扩展也是。

虚拟页面是内存保护单元,因为它的所有字节都共享U/S和R/W标志。但是,相同的物理内存可以由不同的页面映射,可能使用不同的保护标志。注意,执行权限在PTE不见了。这就是为什么经典x86分页允许代码执行到栈上,使其更容易利用堆栈缓冲区溢出(仍然可以利用非可执行的堆栈使用return-to-libc和其他技术)。这种PTE不执行标志的缺乏说明了一个更广泛的事实:VMA中的权限标志可能转换成硬件保护,也可能不转换成硬件保护。内核做它所能做的,但是最终架构限制了它所能做的。

Physical Address Space

在Linux中,每个页面帧都由一个描述符几个标记跟踪。这些描述符一起跟踪计算机中的整个物理内存;每个页面帧的精确状态总是已知的。物理内存使用buddy内存分配技术进行管理,因此,如果可以通过buddy系统进行分配,那么页面框架是免费的。已分配的页帧可以是匿名的,保存程序数据,也可以是在页面缓存中,保存存储在文件或块设备中的数据。还有其他新奇的页面框架用法,但现在先不考虑它们。Windows有一个类似的页帧号(PFN)数据库来跟踪物理内存。

让我们将虚拟内存区域、页表条目和页帧放在一起,以了解它们是如何工作的。下面是一个用户堆的例子:

Physical Address Space

蓝色矩形表示VMA范围内的页面,而箭头表示将页面映射到页帧的页表条目。一些虚拟页面缺少箭头;这意味着它们对应的pte清除了Present标志。这可能是因为页面从未被触摸过,或者是因为它们的内容被换出了。无论在哪种情况下,访问这些页面都会导致页面错误,即使它们在VMA中也是如此。VMA和页面表不一致似乎有些奇怪,但这种情况经常发生。

VMA就像是程序和内核之间的契约。当您请求执行某些操作(分配内存、映射文件等)时,内核会说“确定”,然后它会创建或更新适当的VMA。但是它不会立即执行请求,它会等到页面错误发生时才执行真正的工作。果核是一袋懒惰的、骗人的渣滓;这是虚拟内存的基本原理。它适用于大多数情况,有些是熟悉的,有些是令人惊讶的,但是规则是VMAs记录什么是“一致的”,而pte反映的是“懒惰的内核”实际上做了什么。这两种数据结构一起管理程序的内存;它们都在解决页面错误、释放内存、交换内存等方面发挥作用。让我们以内存分配为例:

Example of demand paging and memory allocation

当程序通过brk()系统调用请求更多内存时,内核只需更新堆VMA并调用它。此时没有实际分配页帧,并且新页不存在于物理内存中。一旦程序尝试访问这些页面,就会调用处理器页面错误和do_page_fault()。它使用[find_vma()][http://lxr.linux.no/linux+v2.6.28/arch/x86/mm/fault.c#L692]来查找覆盖故障虚拟地址的VMA。如果找到,还将根据尝试的访问(读或写)检查VMA的权限。如果没有合适的VMA,则没有契约覆盖试图访问的内存,进程将受到分段错误的惩罚。

当一个VMA被发现时,内核必须通过查看PTE内容和VMA类型来处理这个错误。在我们的例子中,PTE显示页面不存在。事实上,我们的PTE是完全空白的(所有的0),这在Linux中意味着虚拟页面从来没有被映射过。由于这是一个匿名的VMA,我们有一个纯粹的RAM问题,必须由do_anonymous_page()来处理,它分配一个页面帧,并生成一个PTE来将出错的虚拟页面映射到新分配的帧。

事情本可以有所不同。例如,换出页面的PTE在当前标记中为0,但不是空的。相反,它存储交换位置页面内容,必须从磁盘读取并加载到页面逐do_swap_page在所谓主要断层。

这就结束了我们对内核用户内存管理的前半部分。

CATALOG