概念概述¶
Linux 中的内存管理是一个复杂的系统,经过多年的发展,包含了越来越多的功能,以支持从无 MMU 微控制器到超级计算机的各种系统。无 MMU 系统的内存管理称为 nommu
,它绝对值得一篇专门的文档,希望最终会被编写出来。然而,尽管某些概念是相同的,但在这里我们假设 MMU 是可用的,并且 CPU 可以将虚拟地址转换为物理地址。
虚拟内存入门¶
计算机系统中的物理内存是一种有限的资源,即使对于支持内存热插拔的系统,可以安装的内存量也存在硬性限制。物理内存不一定是连续的;它可能作为一组不同的地址范围访问。此外,不同的 CPU 架构,甚至同一架构的不同实现,对如何定义这些地址范围有不同的看法。
所有这些使得直接处理物理内存非常复杂,为了避免这种复杂性,开发了虚拟内存的概念。
虚拟内存将物理内存的细节从应用程序软件中抽象出来,允许仅将所需的信息保留在物理内存中(按需分页),并提供了一种在进程之间保护和控制共享数据的机制。
使用虚拟内存时,每次内存访问都使用虚拟地址。当 CPU 解码从系统内存读取(或写入)的指令时,它会将该指令中编码的虚拟地址转换为内存控制器可以理解的物理地址。
物理系统内存被划分为页帧或页。每个页面的大小是特定于架构的。某些架构允许从几个支持的值中选择页面大小;此选择是在内核构建时通过设置适当的内核配置选项执行的。
每个物理内存页面可以映射为一个或多个虚拟页面。这些映射由页表描述,该页表允许从程序使用的虚拟地址转换为物理内存地址。页表以分层方式组织。
层次结构最低级别的表包含软件使用的实际页面的物理地址。更高级别的表包含属于较低级别的页面的物理地址。指向顶级页表的指针驻留在寄存器中。当 CPU 执行地址转换时,它使用此寄存器访问顶级页表。虚拟地址的高位用于索引顶级页表中的条目。然后使用该条目访问层次结构中的下一级别,并将虚拟地址的下一位作为该级别页表的索引。虚拟地址中的最低位定义了实际页面内部的偏移量。
大页¶
地址转换需要多次内存访问,而内存访问相对于 CPU 速度来说是缓慢的。为了避免在地址转换上花费宝贵的处理器周期,CPU 会维护一个此类转换的缓存,称为转换后备缓冲区 (TLB)。通常,TLB 是一种非常稀缺的资源,具有大型内存工作集的应用程序会因 TLB 未命中而导致性能下降。
许多现代 CPU 架构允许通过页表中更高级别的页面直接映射内存页面。例如,在 x86 上,可以使用第二和第三级页表中的条目映射 2M 甚至 1G 页面。在 Linux 中,此类页面称为大页。使用大页可以显著减少 TLB 的压力,提高 TLB 命中率,从而提高整体系统性能。
Linux 中有两种机制可以启用大页的物理内存映射。第一个是 HugeTLB 文件系统,或 hugetlbfs。它是一个伪文件系统,使用 RAM 作为其后备存储。对于在此文件系统中创建的文件,数据驻留在内存中,并使用大页进行映射。hugetlbfs 在 HugeTLB 页面 中描述。
另一种更新的机制可以启用大页的使用,称为 透明大页 或 THP。与 hugetlbfs 要求用户和/或系统管理员配置系统的哪些部分应该并且可以使用大页映射不同,THP 透明地管理此类映射,因此得名。有关 THP 的更多详细信息,请参阅 透明大页支持。
区域¶
硬件通常会对如何访问不同的物理内存范围施加限制。在某些情况下,设备无法对所有可寻址内存执行 DMA。在其他情况下,物理内存的大小超过了虚拟内存的最大可寻址大小,需要特殊操作才能访问部分内存。Linux 根据其可能的用途将内存页分组为区域。例如,ZONE_DMA 将包含可供设备用于 DMA 的内存,ZONE_HIGHMEM 将包含未永久映射到内核地址空间的内存,而 ZONE_NORMAL 将包含正常寻址的页面。
内存区域的实际布局是硬件相关的,因为并非所有架构都定义了所有区域,并且不同平台的 DMA 要求也不同。
节点¶
许多多处理器机器是 NUMA - 非一致内存访问 - 系统。在此类系统中,内存被安排到不同的存储库中,这些存储库具有不同的访问延迟,具体取决于与处理器的“距离”。每个存储库都称为节点,对于每个节点,Linux 构建一个独立的内存管理子系统。一个节点有其自己的一组区域、空闲和已用页面列表以及各种统计计数器。您可以在 什么是 NUMA?` 和 NUMA 内存策略 中找到有关 NUMA 的更多详细信息。
页面缓存¶
物理内存是易失的,将数据放入内存的常见情况是从文件中读取。每当读取文件时,数据都会放入页面缓存中,以避免在后续读取时进行昂贵的磁盘访问。类似地,当写入文件时,数据会放入页面缓存中,并最终进入后备存储设备。写入的页面被标记为脏,当 Linux 决定将它们用于其他目的时,它会确保将设备上的文件内容与更新的数据同步。
匿名内存¶
“匿名内存”或“匿名映射”表示不以文件系统为后备存储的内存。这种映射是为程序的堆栈和堆隐式创建的,或者通过显式调用 mmap(2) 系统调用创建。通常,匿名映射仅定义程序允许访问的虚拟内存区域。读取访问将导致创建引用一个填充了零的特殊物理页面的页表条目。当程序执行写入操作时,将分配一个常规物理页面来保存写入的数据。该页面将被标记为脏页,如果内核决定重新利用它,则脏页将被交换出去。
回收¶
在系统的整个生命周期中,物理页面可以用于存储不同类型的数据。它可以是内核内部数据结构、设备驱动程序使用的 DMA 缓冲区、从文件系统读取的数据、用户空间进程分配的内存等等。
根据页面的使用情况,Linux 内存管理会区别对待。可以随时释放的页面,要么是因为它们缓存的数据可以在其他地方(例如,在硬盘上)找到,要么是因为它们可以再次交换到硬盘上,这些页面被称为“可回收”。可回收页面最主要的类别是页面缓存和匿名内存。
在大多数情况下,保存内部内核数据并用作 DMA 缓冲区的页面不能被重新利用,它们会一直被固定,直到被其用户释放。这样的页面被称为“不可回收”。然而,在某些情况下,即使是占用内核数据结构的页面也可以被回收。例如,文件系统元数据的内存缓存可以从存储设备重新读取,因此在系统内存压力过大时,可以将其从主内存中丢弃。
释放可回收物理内存页面并重新利用它们的过程称为(毫不奇怪!)“回收”。Linux 可以异步或同步地回收页面,具体取决于系统的状态。当系统负载不高时,大部分内存是空闲的,分配请求会立即从空闲页面池中得到满足。随着负载增加,空闲页面的数量会减少,当它达到某个阈值(低水位线)时,分配请求将唤醒 kswapd
守护进程。它将异步扫描内存页面,如果它们包含的数据在其他地方可用,则直接释放它们,或者将它们驱逐到后备存储设备(还记得那些脏页吗?)。随着内存使用量进一步增加并达到另一个阈值(最小水位线),分配将触发“直接回收”。在这种情况下,分配将被暂停,直到回收足够的内存页面来满足请求。
紧凑¶
随着系统的运行,任务会分配和释放内存,这会导致内存碎片化。尽管使用虚拟内存可以将分散的物理页面表示为虚拟连续的范围,但有时需要分配大的物理连续内存区域。例如,当设备驱动程序需要一个大的 DMA 缓冲区,或者当 THP 分配一个大页面时,可能会出现这种需求。内存“紧凑”解决了碎片化问题。这种机制将已占用的页面从内存区域的较低部分移动到该区域较高部分的空闲页面。当紧凑扫描完成时,空闲页面将集中在该区域的开头,并且可以分配大的物理连续区域。
与回收一样,紧凑可以在 kcompactd
守护进程中异步发生,也可以作为内存分配请求的结果同步发生。
OOM 杀手¶
在负载较高的机器上,内存可能会耗尽,并且内核将无法回收足够的内存来继续运行。为了拯救系统的其余部分,它会调用“OOM 杀手”。
“OOM 杀手”会选择一个任务来牺牲,以维护整个系统的健康。被选中的任务将被杀死,希望在其退出后能释放足够的内存以继续正常运行。