概念概述¶
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 杀手会选择一个任务进行牺牲,以维护整体系统健康。被选中的任务会被终止,希望能在此任务退出后释放足够的内存以恢复正常运行。