当前位置: 首页 > 科技观察

带你了解内核是如何管理内存的

时间:2023-03-20 19:22:57 科技观察

学习完一个进程的虚拟地址布局,我们再回到内核来学习它是如何管理用户内存的。这里又用到了Gonzo:Linuxkernelmm_struct在内核中实现了一个Linux进程作为进程描述符task_struct(LCTT译注:是Linux中描述进程完整信息的数据结构)的一个实例。task_struct中的mm字段指向内存描述符,mm_struct是一个程序在内存中的执行摘要。如上图所示,它保存了起始和结束内存段、进程使用的物理内存页数(RSS常驻内存大小ResidentSetSize)、使用的虚拟地址空间总量等碎片。在内存描述符中,我们可以了解到它有两种管理内存的方式:虚拟内存区域集和页表。Gonzo的内存区域如下:内核内存描述符和内存区域每个虚拟内存区域(VMA)是一个连续的虚拟地址范围;这些领域永远不会重叠。vmareastruct的一个实例完整地描述了一个内存区域,包括它的开始和结束地址,标志决定了访问权限和行为,而vm_file字段指定了映射到这个区域的文件(如果有的话)。(内存映射段除外)VMA不能匿名映射文件。上面的每个内存段(例如,堆、栈)都对应一个VMA。虽然它通常用于x86机器,但不是必需的。VMA也不关心它们在哪个段。程序的VMA作为mmap域的链表存储在内存描述符中,按照起始虚拟地址的顺序排列,作为红黑树的根在mm_rb字段中。红黑树允许内核快速搜索给定虚拟地址的内存区域。当您读取文件/proc/pid_of_process/maps时,内核只是读取每个进程的VMA链表并显示它们。在Windows上,EPROCESS块大致类似于taskstruct和mmstruct的组合。在Windows中模拟VMA的是虚拟地址描述符或VAD;它存储在AVL树中。你知道Windows和Linux最有趣的地方是什么吗?事实上,它们之间只有很小的区别。4GB的虚拟地址空间以页为单位分配。32位模式下的x86处理器支持4KB、2MB和4MB页面大小。Linux和Windows都使用4KB的页面来映射部分用户的虚拟地址空间。字节0-4095在第0页,字节4096-8191在第1页,依此类推。VMA的大小必须是页面大小的倍数。下图是一个总个数为3GB的4KB页的用户空间:4KBPagesVirtualUserSpace处理器通过查看页表将虚拟内存地址转换为真实的物理内存地址。每个进程都有自己的一组页表;每当发生进程切换时,用户空间页表都会同时切换。Linux在内存描述符的pgd字段中存储一个指向进程页表的指针。对于每一个虚拟页面,在页表中都有对应的页表条目(PTE),在常规的x86页表中是一个简单的4字节记录,看起来像这样:x86PageTableEntry(PTE)for4KBpageLinuxuses函数读取和设置PTE条目中的每个标志。标志位P告诉处理器这个虚拟页面是否在物理内存中。如果该位被清除(设置为0),访问该页面将触发页面错误。请记住,当此标志为0时,内核可以对剩余字段做任何它想做的事情。R/W标志为读/写标志;如果清除,页面变为只读。U/S标志表示用户/超级用户;如果被清除,这个页面将只被内核访问。这些标志都用于实现我们之前看到的只读内存和内核空间保护。标志位D和A用于标识页面是否“脏”或是否已被访问。脏页表示它已被写入,访问页表示发生了写入或读取。两个标志都是粘性位:处理器只能设置它们,清除由内核完成。最后,PTE保存了该页对应的起始物理地址,它们整齐地排列在4KB中。这个看似不起眼的字段是一些痛苦的根源,因为它将物理内存限制为最大4GB。其他的PTE字段留待下次使用,因为涉及到物理地址扩展的知识。由于虚拟页面上的所有字节共享一个U/S和R/W标志,因此内存保护的最小单位是虚拟页面。然而,相同的物理内存可能映射到不同的虚拟页面,因此相同的物理内存可能具有不同的保护标志。请注意,运行权限在PTE中是不可见的。这就是为什么经典的x86页面允许代码在堆栈上执行,这很容易导致挖掘堆栈缓冲区溢出(可能通过使用return-to-libc和其他技术来查找不可执行的堆栈)。PTE缺少禁止运行标志说明了一个更广泛的事实,即VMA中的许可标志可能会或可能不会完全转化为硬件保护。内核只能做它能做的,然而,由此产生的架构限制了它能做的。虚拟内存不存储任何东西,它只是将程序的地址空间映射到底层物理内存上。处理器将物理内存作为一个巨大的块访问,称为物理地址空间。虽然内存操作涉及一些总线,但我们在这里忽略它并假设物理地址范围从0到字节增量可用的最大值。物理地址空间被内核进一步分解为页框。处理器并不关心frame的具体情况,这对内核来说也是至关重要的,因为pageframe是物理内存管理的最小单位。Linux和Windows在32位模式下都使用4KB页框;下图是具有2GB内存的机器的示例:物理地址空间Linux上的每个页框都由一个描述符和几个标志来跟踪。通过这些描述符和标志,跟踪机器上的整个物理内存;每个页面框架的具体状态都是公开的。物理内存是通过Buddy内存分配(LCTT译注:一种内存分配算法)技术进行管理的,所以如果一个页框可以通过Buddy系统分配,那么它就是未分配的(free)。分配的页框可以是匿名的,保存程序数据,也可以在页面缓存中,保存存储在文件或块设备中的数据。还有其他外星人页面框架,但这些天使用得不多。Windows有一个类似的页框编号(PFN)数据库来跟踪物理内存。让我们将虚拟内存区域(VMA)、页表条目(PTE)和页框放在一起,以了解它们的工作原理。以下是用户堆的示例:物理地址空间蓝色矩形表示VMA范围内的页面,而箭头表示将页面映射到页面框架的页表条目。一些缺少箭头的虚拟页面表示其对应的PTE的当前标志已被清除(设置为0)。这可能是因为该页面从未被使用过,或者其内容已被换出。在这两种情况下,即使页面在VMA中,访问它们也会导致页面错误。在VMA和页表之间出现这种不一致可能看起来很奇怪,但它经常发生。VMA就像您的程序和内核之间的合同。你要求它做某事(分配内存、映射文件等),内核响应“收到”,然后创建或更新相应的VMA。然而,它并没有立即“履行”它对您的承诺,而是等到页面错误发生后才真正开始工作。内核是一个“懒惰的家伙”,“不诚实的败类”;这就是虚拟内存的基本原理。它在大多数情况下都有效,有一些类似的情况,也有一些意想不到的情况,但是,VMA记录约定的内容是规则,而PTE反映了这个“惰性内核”真正做了什么。这两个数据结构一起用来管理程序的内存;它们一起工作来解决页面错误、释放内存、从内存中换出数据等。下图是内存分配的一个简单案例:请求分页和内存分配示例当程序通过brk()系统调用请求一些内存时,内核只是简单地更新堆的VMA,并回复“已修复”给程序。此时,实际上没有分配任何页框,新页也没有映射到物理内存。一旦程序尝试访问该页面,处理器将出现页面错误并调用dopagefault()。此函数将使用find_vma()来搜索具有页面错误的VMA。如果找到,则对VMA进行权限检查以防止恶意访问(读取或写入)。如果没有合适的VMA并且没有针对正在尝试访问的内存的“合同”,则会向进程返回一个段错误。当找到合适的VMA时,内核必须通过查找PTE的内容和VMA的类型来处理失败。在我们的案例中,PTE显示该页面不存在。其实我们的PTE全是空白(全0),这在Linux中意味着虚拟内存还没有被映射。由于这是一个匿名VMA,我们有一个完整的RAM事务,它必须由doanonymouspage()处理,它分配页框,并使用PTE将错误的虚拟页映射到新分配的页框。有时,情况可能会有所不同。例如,对于被换出内存的页面的PTE,当前(Present)标志为0,但不是空白。相反,在交换位置仍然有页面内容,必须从磁盘读取并通过doswappage()将其加载到称为主要错误的页面框架中。这是我们通过探究内核的用户内存管理得出的结论的前半部分。在下一篇文章中,我们将构建一个完整的内存框架图,以及将文件加载到内存中对性能的影响。