Linux 内存管理

2025年3月17日 | 阅读 15 分钟

Linux 内存管理子系统负责管理系统内的内存。它包含按需分页虚拟内存的实现。

此外,它还包括为用户空间程序和内核内部结构分配内存。Linux 内存管理子系统包括映射到进程地址空间的内存以及其他一些内容。

Linux 内存管理子系统是一个复杂系统,具有许多可配置的设置。几乎所有设置都可通过 /proc 文件系统访问,并可以使用 sysctl 进行调整和获取。这些类型的 API 在man 5 procDocumentation for /proc/sys/vm/.中指定。

Linux 内存管理包含其行话。在此,我们将详细讨论如何理解 Linux 内存管理的各种机制。

概念概述

Linux 内存管理是一个复杂的系统,包含许多功能,可支持从无 MMU 的微控制器到超级计算机的各种系统。

对于没有MMU的系统,内存管理称为nommu,它有一个专门的文档,希望最终能写出来。但是,有些概念是相似的。

在此,我们将假定 MMU 存在,并且 CPU 可以将任何虚拟地址转换为物理地址。

Linux Memory Management
  • 巨型页面
  • 虚拟内存入门
  • 区域
  • 页缓存
  • 节点
  • 匿名内存
  • OOM Killer
  • 内存紧缩
  • 回收

巨型页面

地址翻译需要多次内存访问。与 CPU 速度相比,这些内存访问非常慢。为了避免将宝贵的处理器周期花费在地址翻译上,CPU 会管理这些类型翻译的缓存,称为转换查找缓冲区 (TLB)。

虚拟内存入门

在计算机系统中,物理内存是有限的资源。物理内存不一定是连续的。它可以作为一组不同的地址范围进行访问。此外,不同的 CPU 架构和相似架构的实现对这些范围的指定方式有不同的看法。

直接处理物理内存会非常困难,为了避免这种复杂性,指定了虚拟内存机制。

虚拟内存通过应用程序将物理内存细节分离开来。

它允许只将所需信息保留在物理内存中。它为进程之间受控的数据共享和保护提供了机制。

区域

Linux 根据可能的用途将内存页分为几个区域。例如,ZONE_HIGHMEM包含未永久映射到内核地址空间的内存,ZONE_DMA包含可供各种设备用于 DMA 的内存,而ZONE_NORMAL包含普通寻址的页面。

页缓存

将数据加载到内存的常见方法是通过文件读取,因为物理内存不稳定。

每当读取文件时,数据将放入页缓存,以避免后续读取时昂贵的磁盘访问。

类似地,数据将放入页缓存,并在写入文件时写入后端存储设备。

节点

许多多处理器机器可以定义为NUMA - 非统一内存访问系统。内存被组织成银行,这些银行根据与这些系统中的处理器的“距离”具有不同的访问延迟。所有银行都称为节点,对于所有节点,Linux 会创建一个独立的内存管理子系统。单个节点包含其区域集、已用和空闲页面列表以及各种统计计数器。

匿名内存

匿名映射或匿名内存指定不受任何文件系统支持的内存。这些类型的映射是由程序堆栈和堆隐式创建的,或者通过显式调用mmap(2) 系统调用创建的。

匿名映射通常只指定程序可以访问的虚拟内存区域。

OOM Killer

内核可能无法回收足够的内存,并且加载的机器内存将耗尽,无法继续运行。

内存紧缩

随着系统的运行,各种任务会分配和释放内存空间,内存空间会变得碎片化。但是,通过虚拟内存可以限制分散的物理页面。内存紧缩定义了碎片化问题。

回收

根据页面的使用情况,Linux 内存管理会对其进行不同的处理。可以被释放的页面,因为它们缓存了存在于磁盘其他地方的信息,或者因为它们可以被换出到磁盘,这些页面称为可回收

CMA 调试接口

这有助于从 CMA 的各个区域检索基本信息,并测试所有区域的释放/分配。

所有 CMA 区域都在/CMA/ 下指定一个目录,该目录通过内核的 CMA 索引进行索引。因此,第一个 CMA 区域将是

该目录下的文件结构如下

  • [RO] base_pfn: 区域的基址页帧号 (或 PFN)。
  • [RO] order_per_bit: 单个位指定的页面序列。
  • [RO] count: CMA 区域内的内存量。
  • [RO] bitmap: 区域内的页面位图。
  • [WO] alloc: 从 CMA 区域分配 N 页。例如

HugeTLB 页面

引言

此文件的目的是简要概述 Linux 内核中的 hugetlbpage 支持。这种支持建立在大多数最新架构提供的多种页面大小支持之上。

例如,x86 CPU 通常支持 2M 和 4K 页面大小,ia64 架构支持多种页面大小 256M、16M、4M、1M、256K、64K、8K、4K,而 ppc64 支持 16M 和 4K。

TLB 被定义为虚拟到物理地址转换的缓存。它通常是处理器上非常稀缺的资源。

各种操作系统都试图最大限度地利用有限的 TLB 资源。

现在,这种优化更加复杂,因为几十 GB 的(物理)内存已经很普遍了。

用户可以通过使用经典的 SYSV 共享内存系统调用 (shmat 和 shmget) 或 mmap 系统调用来利用 Linux 内核中的巨型页面支持。

最初,Linux 内核需要使用CONFIG_HUGETLBFSCONFIG_HUGETLB_PAGE配置选项进行编译。

文件/proc/meminfo提供了内核巨型页面池中持久巨型页面总数的详细信息。

此外,它还显示了巨型页面大小(默认)以及默认大小巨型页面池中剩余、已保留和空闲巨型页面的数量。

巨型页面的大小对于生成系统调用参数的准确大小和对齐至关重要,这将有助于映射巨型页面的区域。

/proc/sys/vm/nr_hugepages文件表示内核巨型页面池中“持久”巨型页面的数量(当前)。

“持久”巨型页面将在任务释放后返回巨型页面池。动态地,用户可以拥有大量 root 权限,通过减小或增加nr_hugepages值来分配或释放一些持久巨型页面。

巨型页面使用的页面可以在内核中保留,不能用于其他目的。在内存压力下,巨型页面不能被换出。

空闲页面跟踪

动机

此功能允许跟踪工作负载正在访问哪些内存页面。

此信息有助于估算工作集的负载大小,然后可以根据该信息配置工作负载参数、确定工作负载的位置或在计算机集群中设置内存 cgroup 限制。

可以使用以下方法启用它

用户 API

空闲页面跟踪的 API 位于/sys/kernel/mm/page_idle。它目前包含一个读写文件/sys/kernel/mm/page_idle/bitmap

该文件操作一个位图,其中所有位都对应于内存页面。该位图由 8 字节整数数组定义,PFN #i 上的页面映射到 #i/64 数组元素的 #i %64 位(字节顺序为本机)。如果位固定,则表示相应页面为空闲。

实现细节

内部,内核会记录对用户内存页面的访问,以便在内存短缺情况下优先回收未引用的页面。当地址空间中的进程最近访问过页面时,该页面将被视为已引用。后者将发生,如果

  • 用户空间进程通过系统调用写入或读取(例如,read(3) 或 write(3))
  • 设备驱动程序通过get_user_pages()访问页面
  • 页面用于驱动文件系统缓冲区,因为进程需要存储在其上的文件系统元数据(例如,列出目录树)而进行读取或写入

内核同页合并

内核同页合并 (KSM) 是一种内存去重节省功能。它通过CONFIG_KSM=y启用,并包含在 Linux 内核的2.6.32版本中。

最初,KSM 是为与 KSM(当时称为内核共享内存)一起使用而设计的,通过共享它们之间的通用信息来将其他 VM 放入物理内存。

但是,对于生成大量相似数据实例的应用程序,它可能很有用。

KSM 的 ksmd 守护进程会定期扫描注册给它的用户内存区域,查找可以被单个写保护页面替换(稍后当任何进程希望更新其内容时会自动复制)的相同内容页面。

KSM 守护进程在单次扫描中扫描的页面数量以及扫描之间的时间可以通过 sysfs 接口进行配置。

内核同页合并仅合并私有(匿名)页面,从不合并文件(页缓存)页面。最初,KSM 的合并页面被锁定在内核内存中,但现在可以像其他用户页面一样被换出。

通过 madvise 控制 KSM

KSM 仅在应用程序建议可能合并的地址空间区域上实现,使用 madvise(2) 系统调用。

然后,应用程序可以调用

注意:此调用(取消合并)可能突然需要额外内存,并可能因 EAGAIN 而失败。

KSM 守护进程 sysfs 接口

KSM 的守护进程由/sys/kernel/mm/ksm/下的 sysfs 文件管理,任何人都可以读取,但只有 root 用户可以写入。

pages_to_scan

它确定在 ksmd 守护进程休眠之前扫描多少页面。

例如:

注意:默认选择 100 用于演示目的。

sleep_millisecs

它确定 ksmd 守护进程在下次扫描之前必须休眠多少毫秒。

例如:

注意:默认选择 20 用于演示目的。

run

设置为 0 可停止 ksmd 守护进程的执行,但继续合并页面。

设置为 1 可运行 ksmd 守护进程,例如。

设置为 2 可停止 ksmd 守护进程并取消合并所有当前合并的页面,但会保留可合并区域以供下次运行。

max_page_sharing

所有 KSM 页面的最大允许共享数。它强制执行重复限制,以避免虚拟内存的许多操作产生高延迟。它涉及将 KSM 页面分配出去的虚拟映射遍历。

最小值是 2,因为新创建的 KSM 页面至少有两个共享者。减小此遍历表示在页面迁移、NUMA 平衡、内存紧缩和交换期间进行虚拟内存的许多操作会产生更高的延迟。

stable_node_chains_prune_millisecs

它描述了 KSM 检查页面元数据以清除陈旧信息的频率,该元数据已达到重复限制。较小的毫秒值会以较低的延迟释放 KSM 元数据。

但是,它们会使 ksmd 守护进程在扫描期间使用更多 CPU。当没有单个 KSM 页面遇到 max_page_sharing 时,它不起作用。

MADV_MERGEABLE 和 KSM 的有效性显示在

pages_shared

它定义了正在使用的共享页面数量。

pages_sharing

它定义了多少其他站点正在共享它们,即有多少是存储的。

pages_unshared

它定义了多少页面是专用的但反复检查合并。

full_scans

它定义了每个可合并区域被扫描了多少次。

pages_volatile

它定义了多少页面修改得非常快,以至于无法放入树中。

stable_node_chains

它定义了达到限制的 KSM 页面数量,即max_page_sharing

stable_node_dups

它定义了 KSM 页面的数量(重复)。

pages_sharedpages_sharing的高比例表示更好的共享。此外,pages_sharingpages_unshared的高比例表示徒劳的尝试。

pages_volatile涵盖了不同类型的活动。但是,高比例也表示 madvise MADV_MERGEABLE 使用不当。

pages_sharing/pages_shared的最大可能比例受max_page_sharing可调参数的限制。要提高比例,应相应地增加max_page_sharing

内存热插拔

本文档描述了内存热插拔,包括其当前状态以及如何使用它。此文本内容会经常更改,因为内存热插拔仍处于开发阶段。

引言

内存热插拔目标

内存热插拔允许用户增加或减少内存量。通常有两个目标:

(1)更改内存量。这是为了允许按需提供容量等功能。

(2)物理安装或移除 NUMA 节点或 DIMM。这是为了更换 NUMA 节点/DIMM 并降低功耗。

第一个目标是高度虚拟化平台需要的,第二个目标是硬件需要的。此外,第二个目标支持内存的电源管理。

Linux 中的内存热插拔是为这两个目标而开发的。

内存热插拔阶段

内存热插拔主要有两个阶段:

  • 内存热插拔的物理阶段
  • 内存热插拔的逻辑阶段

物理阶段用于与固件或硬件通信,并为热插拔内存进行平台擦除或准备。此阶段对于目标(2)至关重要,但它也是与高度虚拟化平台通信的良好阶段。

内核将在热插拔内存时识别新内存,创建新的内存管理表,并创建用于新内存操作的 sysfs 文件。

如果固件支持通知操作系统新内存连接。此阶段会自动触发。ACPI 可以发出此事件警报。如果没有发出此事件,则由系统管理员使用称为“探测”的操作。

逻辑阶段用于更改内存对用户的可用或不可用状态。此阶段会更改用户视野中的内存量。当一个内存范围可用时,内核将其中所有内存启用为许多空闲页面。

此阶段在本文档中定义为在线/离线

逻辑阶段由系统管理员写入的 sysfs 文件处理。对于热添加情况,它应在物理阶段之后手动运行。

内存在线/离线任务单元

内存热插拔应用了SPARSEMEM内存模型,该模型允许内存被分类为多个相同大小的。这些类型的块称为“段”。内存段大小是架构相关的。

内存段与称为“内存块”相关联。内存块大小表示执行内存在线/离线操作的逻辑单元。它也与架构相关

除非架构另有说明,否则内存块具有与内存段大小相同的默认大小。

要确定内存块的大小,请考虑以下文件

内核配置

要使用内存热插拔功能,应使用以下配置选项编译内核:

对于所有内存热插拔:

  • 允许内存热添加(CONFIG_MEMORY_HOTPLUG)
  • 内存(CONFIG_SPARSEMEM)
  • 内存模型 -> Sparse

此外,以下对于启用内存移除是必需的:

  • 页面迁移(CONFIG_MIGRATION)
  • 允许内存热移除(CONFIG_MEMORY_HOTREMOVE)

此外,以下对于 ACPI 的内存热插拔是必需的:

  • 此选项可以是内核模块。
  • 内存热插拔(ACPI 支持菜单下)

与相应的配置一样,如果我们的计算机通过 ACPI 具有 NUMA 节点热插拔功能,那么此选项也是必需的。

  • PNP0A06、PNP0A05 和 ACPI0004 容器驱动程序(CONFIG_ACPI_CONTAINER)(ACPI 支持菜单下)。
  • 此选项也可以是内核模块。

内存热插拔 sysfs 文件

每个内存块在其 sysfs 文件中都有设备详细信息。所有内存块都指定如下:

其中内存块的 ID 是XXX

假设在此范围内存在每个内存段,并且对于由 sysfs 目录覆盖的内存块没有内存空洞。

目前没有办法确定是否有任何内存空洞。但是,一个内存空洞的存在不应影响内存块的热插拔兼容性。

例如,假设内存块大小为 1GiB。任何从 0x100000000 开始的内存设备将是

此设备将覆盖(0*100000000 ... 0*140000000)地址范围

我们在所有内存块下可以看到五个文件:

无 MMU 支持的内存支持

在无 MMU 的情况下,内核包括有限的内存映射支持。从用户空间的观点来看,内存映射与 mmap() 系统调用、execve() 系统调用和 shmat() 调用结合使用。

从内核的观点来看,execve 映射通过 binfmt 驱动程序执行。它会回调用 mmap() 例程来完成实际工作。

此外,内存映射的行为与 ptrace()、clone()、vfork() 和 fork() 的工作方式相关。在 uClinux 下,clone() 和 fork() 不应提供 CLONE_VM 标志。

无 MMU 和 MMU 情况下的行为相同,但不完全相同。此外,在后一种情况下,它受到更多限制。

1. 匿名映射,

MAP_PRIVATE

  • VM 区域由随机页面支持(在 MMU 的情况下)。
  • VM 区域由随机连续的页面执行支持(在无 MMU 的情况下)。

2. 匿名映射,

MAP_SHARED

它的工作方式与私有映射非常相似。除了它们在 MMU 的情况下会在 clone() 和 fork() 之间共享,而没有 CLONE_VM。由于行为可与 MAP_PRIVATE 互换,而无 MMU 情况不支持这些。

3. 文件,!PROT_WRITE,PROT_READ/PROT_EXEC,MAP_PRIVATE

  • VM 区域由从文件读取的页面支持(在 MMU 的情况下)。更改将反映在映射到底层文件中。
  • 在无 MMU 的情况下
  • 当文件包含兼容的权限时,内核会重用对相似文件相似部分的现有映射,即使它是由其他进程创建的(如果存在)。
  • 如果文件映射具有适当的保护映射和 NOMMU_MAP_DIRECT 功能,则文件映射将直接在后端设备上。mtd、cramfs、romfs 和 ramfs 都可能允许它。
  • 文件写入不会影响映射,映射写入在其他进程中可见,但不得发生。

4. 文件,PROT_WRITE,PROT_READ/PROT_EXEC,MAP_PRIVATE

  • 在 MMU 的情况下:工作方式与非 PROT_WRITE 情况类似。除了在写入实际发生之前会复制页面。
  • 在无 MMU 的情况下:工作方式与非 PROT_WRITE 情况类似。除了复制总是会发生,并且永远不会共享。

5. 文件,PROT_READ/PROT_EXEC/PROT_WRITE,MAP_SHARED,文件/块设备

  • 在 MMU 的情况下: VM 区域由从文件读取的页面支持;文件写入反映到页面的后端内存中;在 fork 之间共享。
  • 在无 MMU 的情况下:不支持。

6. PROT_READ/PROT_EXEC/PROT_WRITE,MAP_SHARED,内存后备块设备

  • 在 MMU 的情况下:与常规文件(普通)相同。
  • 在无 MMU 的情况下:与各种内存后备常规文件相同。但是,块设备可以在不调用 abbreviate 的情况下提供连续页面运行。此外,ramdisk 驱动程序可以在其预先分配每个内存作为连续数组时执行此操作。

7. PROT_READ/PROT_EXEC/PROT_WRITE,MAP_SHARED,内存后备常规文件

  • 在 MMU 的情况下:与常规文件(普通)相同。
  • 在无 MMU 的情况下:提供内存后备文件的文件系统(如 tmpfs 或 ramfs)可以选择通过提供一组连续页面进行映射来响应 mmap、truncate、open 序列。
    在这种情况下,共享可写内存映射是可能的。它的工作方式与 MMU 的情况相同。
    如果文件系统不支持此类支持,则映射请求将被拒绝。

8. PROT_READ/PROT_EXEC/PROT_WRITE,MAP_SHARED,内存后备字符设备

  • 在 MMU 的情况下:与常规文件(普通)相同。
  • 在无 MMU 的情况下:字符设备驱动程序可以选择通过直接访问底层设备来响应mmap(),前提是它提供准内存或可直接访问的内存。如果驱动程序不支持此类支持,则映射请求将被拒绝。

关于无 MMU 内存的其他要点

  • 私有映射请求可能会返回一个未按页面对齐的缓冲区。这是因为 XIP 可能占据位置,并且数据在后备存储中可能未按页面对齐。
  • 对于匿名映射,根据 Linux man 页,由请求分配的内存通常会在映射之前由内核释放。
  • 对于匿名映射,请求将始终按页面对齐。请求大小必须是 2 的幂。
  • 在无 MMU 模式下,可以通过/proc//maps检测进程正在使用的所有映射。
  • 在无 MMU 模式下,可以通过/proc/maps检测系统上的所有匿名映射和私有副本。

下一个主题Linux 重启命令