内存热插拔

本文档描述了 Linux 对内存热插拔的通用支持,重点是系统 RAM,包括 ZONE_MOVABLE 支持。

简介

内存热插拔允许在运行时增加和减少机器可用的物理内存大小。在最简单的情况下,它包括在运行时物理地插入或拔出 DIMM,并与操作系统协调。

内存热插拔用于各种目的

  • 机器可用的物理内存可以在运行时进行调整,从而升级或降级内存容量。这种动态内存调整(有时称为“按需容量”)通常与虚拟机和逻辑分区一起使用。

  • 更换硬件,例如 DIMM 或整个 NUMA 节点,而不会停机。一个示例是更换发生故障的内存模块。

  • 通过物理地拔出内存模块或通过逻辑地拔出 Linux 中的(部分)内存模块来降低能耗。

此外,Linux 中基本内存热插拔基础设施现在也用于将持久内存、其他性能差异内存和保留内存区域作为普通系统 RAM 公开给 Linux。

Linux 仅在选定的 64 位架构(例如 x86_64、arm64、ppc64 和 s390x)上支持内存热插拔。

内存热插拔粒度

Linux 中的内存热插拔使用 SPARSEMEM 内存模型,该模型将物理内存地址空间划分为相同大小的块:内存段。内存段的大小取决于架构。例如,x86_64 使用 128 MiB,ppc64 使用 16 MiB。

内存段组合成称为“内存块”的块。内存块的大小取决于架构,并且对应于可以热插拔的最小粒度。除非架构另有规定,否则内存块的默认大小与内存段大小相同。

所有内存块的大小都相同。

内存热插拔阶段

内存热插拔包括两个阶段

  1. 将内存添加到 Linux

  2. 上线内存块

在第一阶段,分配和初始化元数据(例如内存映射(“memmap”)和直接映射的页表),并创建内存块;后者还会创建 sysfs 文件来管理新创建的内存块。

在第二阶段,添加的内存将暴露给页面分配器。在此阶段之后,内存将显示在系统的内存统计信息中,例如可用内存和总内存。

内存热拔出阶段

内存热拔出包括两个阶段

  1. 下线内存块

  2. 从 Linux 中删除内存

在第一阶段,内存再次从页面分配器中“隐藏”,例如,通过将繁忙内存迁移到其他内存位置并从页面分配器中删除所有相关的空闲页面。在此阶段之后,内存不再显示在系统的内存统计信息中。

在第二阶段,删除内存块并释放元数据。

内存热插拔通知

Linux 有多种方式被通知有关内存热插拔事件,以便它可以开始添加热插拔的内存。此描述仅限于支持 ACPI 的系统;未描述特定于其他固件接口或虚拟机的机制。

ACPI 通知

支持 ACPI 的平台(例如 x86_64)可以通过 ACPI 支持内存热插拔通知。

通常,支持内存热插拔的固件定义一个内存类对象 HID “PNP0C80”。当收到有关新内存设备热插拔的通知时,ACPI 驱动程序会将内存热插拔到 Linux 中。

如果固件支持 NUMA 节点的热插拔,则它会定义一个对象 _HID “ACPI0004”、“PNP0A05”或“PNP0A06”。当收到有关热插拔事件的通知时,ACPI 驱动程序会将所有分配的内存设备添加到 Linux 中。

同样,可以通过 ACPI 将有关热拔出内存设备或 NUMA 节点的请求通知给 Linux。ACPI 驱动程序将尝试下线所有相关的内存块,如果成功,则从 Linux 中热拔出内存。

手动探测

在某些架构上,固件可能无法将有关内存热插拔事件的通知发送给操作系统。相反,必须从用户空间手动探测内存。

探测接口位于

/sys/devices/system/memory/probe

只能探测完整的内存块。通过提供内存块的物理起始地址来探测各个内存块

% echo addr > /sys/devices/system/memory/probe

这会导致创建范围为 [addr, addr + memory_block_size) 的内存块。

注意

不建议使用探测接口,因为它很容易导致内核崩溃,因为 Linux 无法验证用户输入;此接口将来可能会被删除。

上线和下线内存块

创建内存块后,必须指示 Linux 实际使用该内存:必须将内存块“上线”。

在删除内存块之前,Linux 必须停止使用内存块的任何内存部分:必须将内存块“下线”。

可以配置 Linux 内核以自动上线添加的内存块,并且驱动程序在尝试热拔出内存时会自动触发内存块的下线。只有在下线成功后才能删除内存块,并且驱动程序可能会在尝试热拔出内存时触发内存块的下线。

手动上线内存块

如果未启用内存块的自动上线,则用户空间必须手动触发内存块的上线。通常,udev 规则用于自动化用户空间中的此任务。

可以通过以下方式触发内存块的上线

% echo online > /sys/devices/system/memory/memoryXXX/state

或者,也可以

% echo 1 > /sys/devices/system/memory/memoryXXX/online

内核将根据配置的 online_policy 自动选择目标区域。

可以通过以下方式显式请求将脱机内存块与 ZONE_MOVABLE 关联

% echo online_movable > /sys/devices/system/memory/memoryXXX/state

或者,可以通过以下方式显式请求内核区域(通常为 ZONE_NORMAL):

% echo online_kernel > /sys/devices/system/memory/memoryXXX/state

无论如何,如果上线成功,内存块的状态将更改为“online”。如果失败,内存块的状态将保持不变,并且上述命令将失败。

自动上线内存块

可以将内核配置为尝试自动上线新添加的内存块。如果禁用此功能,则内存块将保持离线状态,直到从用户空间显式上线为止。

可以通过以下方式观察配置的自动上线行为:

% cat /sys/devices/system/memory/auto_online_blocks

可以通过向该文件写入 onlineonline_kernelonline_movable 来启用自动上线,例如:

% echo online > /sys/devices/system/memory/auto_online_blocks

与手动上线类似,使用 online,内核将根据配置的 online_policy 自动选择目标区域。

修改自动上线行为只会影响所有后续添加的内存块。

注意

在极端情况下,自动上线可能会失败。内核不会重试。请注意,在默认配置中,自动上线预计不会失败。

注意

ppc64 上的 DLPAR 会忽略 offline 设置,并且仍然会上线添加的内存块;如果上线失败,内存块将再次被删除。

下线内存块

在当前的实现中,Linux 的内存下线将尝试将所有可移动的页面从受影响的内存块中迁移出去。由于大多数内核分配(例如页表)是不可移动的,因此页面迁移可能会失败,从而阻止内存下线成功。

让由 ZONE_MOVABLE 管理的内存块提供的内存可以显著提高内存下线的可靠性;尽管如此,在某些极端情况下,内存下线仍然可能会失败。

此外,内存下线可能会重试很长时间(甚至永远),直到用户中止为止。

可以通过以下方式触发内存块的下线:

% echo offline > /sys/devices/system/memory/memoryXXX/state

或者,也可以

% echo 0 > /sys/devices/system/memory/memoryXXX/online

如果下线成功,内存块的状态将更改为“offline”。如果失败,内存块的状态将保持不变,并且上述命令将失败,例如,通过:

bash: echo: write error: Device or resource busy

或者通过:

bash: echo: write error: Invalid argument

观察内存块的状态

可以通过以下方式观察内存块的状态(在线/离线/正在下线):

% cat /sys/devices/system/memory/memoryXXX/state

或者,也可以通过 (1/0) 进行观察:

% cat /sys/devices/system/memory/memoryXXX/online

对于在线内存块,可以通过以下方式观察其管理的区域:

% cat /sys/devices/system/memory/memoryXXX/valid_zones

配置内存热插拔

系统管理员可以通过多种方式配置内存热插拔并与内存块进行交互,特别是,将其上线。

通过 Sysfs 配置内存热插拔

可以在以下位置通过 sysfs 配置或检查一些内存热插拔属性:

/sys/devices/system/memory/

当前定义了以下文件:

auto_online_blocks

读写:设置或获取新内存块的默认状态;配置自动上线。

默认值取决于 CONFIG_MEMORY_HOTPLUG_DEFAULT_ONLINE 内核配置选项。

有关详细信息,请参阅内存块的 state 属性。

block_size_bytes

只读:内存块的大小(以字节为单位)。

probe

只写:通过提供物理起始地址,从用户空间手动添加(探测)选定的内存块。

可用性取决于 CONFIG_ARCH_MEMORY_PROBE 内核配置选项。

uevent

读写:设备子系统的通用 udev 文件。

crash_hotplug

只读:当由于内存的热插拔而导致系统内存映射发生更改时,如果内核本身更新 kdump 捕获内核内存映射(通过 elfcorehdr 和其他相关的 kexec 段),则此文件包含“1”;如果用户空间必须更新 kdump 捕获内核内存映射,则包含“0”。

可用性取决于 CONFIG_MEMORY_HOTPLUG 内核配置选项。

注意

启用 CONFIG_MEMORY_FAILURE 内核配置选项后,将提供两个额外的文件 hard_offline_pagesoft_offline_page 来触发页面的 hwpoisoning,例如,用于测试目的。请注意,此功能实际上与内存热插拔或内存块的实际下线无关。

通过 Sysfs 配置内存块

每个内存块都表示为一个可以上线或下线的内存块设备。所有内存块的设备信息都位于 sysfs 中。每个存在的内存块都列在 /sys/devices/system/memory 下,如下所示:

/sys/devices/system/memory/memoryXXX

其中 XXX 是内存块 ID;数字位数是可变的。

存在的内存块表示该范围内的某些内存存在;但是,内存块可能会跨越内存空洞。跨越内存空洞的内存块无法下线。

例如,假设内存块大小为 1 GiB。起始于 0x100000000 的内存的设备为 /sys/devices/system/memory/memory4

(0x100000000 / 1Gib = 4)

此设备覆盖的地址范围为 [0x100000000 ... 0x140000000)

当前定义了以下文件:

online

读写:用于触发上线/下线和观察内存块状态的简化接口。上线时,会自动选择区域。

phys_device

只读:仅在 s390x 上使用的旧接口,用于公开覆盖的存储增量。

phys_index

只读:内存块 ID (XXX)。

removable

只读:旧接口,指示内存块是否可能可以下线。如今,当且仅当内核支持内存下线时,内核才返回 1

state

读写:用于触发上线/下线和观察内存块状态的高级接口。

写入时,支持 onlineofflineonline_kernelonline_movable

online_movable 指定上线到 ZONE_MOVABLE。online_kernel 指定上线到内存块的默认内核区域,例如 ZONE_NORMAL。online 让内核自动选择区域。

读取时,可能会返回 onlineofflinegoing-offline

uevent

读写:设备的通用 uevent 文件。

valid_zones

只读:当块在线时,显示它所属的区域;当块离线时,显示当块上线时哪个区域将管理它。

对于在线内存块,可能会返回 DMADMA32NormalMovablenonenone 表示内存块提供的内存由多个区域管理或跨越多个节点;此类内存块无法下线。Movable 表示 ZONE_MOVABLE。其他值表示内核区域。

对于离线内存块,第一列显示当立即上线内存块而不进一步指定区域时,内核将选择的区域。

可用性取决于 CONFIG_MEMORY_HOTREMOVE 内核配置选项。

注意

如果启用 CONFIG_NUMA 内核配置选项,则还可以通过位于 /sys/devices/system/node/node* 目录中的符号链接访问 memoryXXX/ 目录。

例如:

/sys/devices/system/node/node0/memory9 -> ../../memory/memory9

还将创建一个反向链接:

/sys/devices/system/memory/memory9/node0 -> ../../node/node0

命令行参数

一些命令行参数会影响内存热插拔处理。以下命令行参数是相关的:

memhp_default_state

通过本质上设置 /sys/devices/system/memory/auto_online_blocks 来配置自动上线。

movable_node

在使用 contig-zones 上线策略时,配置内核中的自动区域选择。设置后,除非可以保持其他区域连续,否则内核在上线内存块时将默认为 ZONE_MOVABLE。

有关这些命令行参数的更通用描述,请参阅内核的命令行参数

模块参数

现在,memory_hotplug 子系统为模块参数提供了一个专用命名空间,而不是额外的命令行参数或 sysfs 文件。模块参数可以通过在命令行中使用 memory_hotplug. 作为前缀来设置,例如:

memory_hotplug.memmap_on_memory=1

并且可以通过以下方式观察它们(甚至可以在运行时修改一些参数):

/sys/module/memory_hotplug/parameters/

当前定义了以下模块参数:

memmap_on_memory

读写:从添加的内存块本身分配 memmap 的内存。即使启用,实际支持也取决于各种其他系统属性,应仅将其视为是否需要此行为的提示。

虽然从内存块本身分配 memmap 使内存热插拔不太可能失败,并且在任何情况下都将 memmap 保留在同一 NUMA 节点上,但它可能会以更大的粒度碎片化物理内存,从而无法在热插拔内存上形成巨页。

使用“force”值可能会因内存映射大小限制而导致内存浪费。例如,如果一个内存块的内存映射需要 1 MiB,但页面块大小为 2 MiB,则 1 MiB 的热插拔内存将被浪费。请注意,在某些情况下,该特性无法强制执行:例如,如果内存映射小于单个页面,或者如果架构不支持所有配置中的强制模式。

online_policy

读写:设置在不指定目标区域的情况下,在线内存块时用于自动选择区域的基本策略。在添加此参数之前,contig-zones 一直是内核的默认设置。配置在线策略并在线内存后,不应再更改该策略。

当设置为 contig-zones 时,内核将尝试保持区域连续。如果内存块与多个区域相交或没有区域,则行为取决于 movable_node 内核命令行参数:如果设置,则默认为 ZONE_MOVABLE,如果未设置,则默认为适用的内核区域(通常为 ZONE_NORMAL)。

当设置为 auto-movable 时,内核将根据配置和内存设备详细信息,尝试将内存块在线到 ZONE_MOVABLE(如果可能)。使用此策略,可以避免在最终热插拔大量内存后出现区域不平衡,并且仍然希望能够尽可能可靠地热拔出,这在虚拟化环境中非常理想。此策略忽略 movable_node 内核命令行参数,并且不适用于需要它的环境(例如,具有热插拔节点的裸机),在这些环境中,热插拔内存可能会在启动期间通过固件提供的内存映射早期暴露给系统,而不是在启动期间稍后检测、添加和在线(例如,通过 virtio-mem 或某些实现模拟 DIMM 的虚拟机监控程序)。例如,热插拔的 DIMM 将完全在线到 ZONE_MOVABLE 或完全在线到 ZONE_NORMAL,而不是混合。再举一个例子,属于 virtio-mem 设备的许多内存块将尽可能在线到 ZONE_MOVABLE,特别指定只能一起热拔出的内存块单元。此策略不能防止 ZONE_MOVABLE 存在问题的设置,并且不会在内存块上线后动态更改其区域。

auto_movable_ratio

读写:设置 auto-movable 在线策略中 MOVABLE:KERNEL 的最大内存比率(以百分比表示)。该比率是仅适用于跨所有 NUMA 节点的系统,还是也适用于每个 NUMA 节点,取决于 auto_movable_numa_aware 配置。

所有计算都基于区域中存在的内存页面,并结合每个内存设备的计算。专用于 CMA 分配器的内存被计算为 MOVABLE,尽管它位于内核区域之一。可能的比率取决于实际的工作负载。内核默认值为“301%”,例如,允许将 24 GiB 热插拔到 8 GiB 的虚拟机中,并在许多设置中自动将所有热插拔内存在线到 ZONE_MOVABLE。额外的 1% 用于处理某些不存在的页面,例如,由于某些固件分配。

请注意,一个内存设备提供的 ZONE_NORMAL 内存不允许其他内存设备拥有更多的 ZONE_MOVABLE 内存。例如,将热插拔的 DIMM 的内存在线到 ZONE_NORMAL 不允许另一个热插拔的 DIMM 自动在线到 ZONE_MOVABLE。相反,通过 virtio-mem 设备热插拔并在线到 ZONE_NORMAL 的内存将允许在同一个 virtio-mem 设备中有更多的 ZONE_MOVABLE 内存。

auto_movable_numa_aware

读写:配置 auto-movable 在线策略中的 auto_movable_ratio 是否也适用于每个 NUMA 节点,以及跨所有 NUMA 节点的整个系统。内核默认值为“Y”。

在处理应完全热拔出的 NUMA 节点时,禁用 NUMA 感知可能很有用,如果可能,则自动将内存完全在线到 ZONE_MOVABLE。

参数可用性取决于 CONFIG_NUMA。

ZONE_MOVABLE

ZONE_MOVABLE 是一种用于更可靠的内存离线的重要机制。此外,由 ZONE_MOVABLE 管理而不是由内核区域之一管理的系统 RAM 可以增加可能的透明大页和动态分配的大页的数量。

大多数内核分配是不可移动的。重要的示例包括内存映射(通常为 1/64 的内存)、页表和 kmalloc()。此类分配只能从内核区域提供。

大多数用户空间页面(例如匿名内存和页面缓存页面)是可移动的。此类分配可以从 ZONE_MOVABLE 和内核区域提供。

只有可移动的分配才会从 ZONE_MOVABLE 提供,导致不可移动的分配限制在内核区域。如果没有 ZONE_MOVABLE,则绝对无法保证内存块可以成功离线。

区域不平衡

由 ZONE_MOVABLE 管理的系统 RAM 过多称为区域不平衡,这可能会损害系统或降低性能。例如,内核可能会崩溃,因为它用完了不可移动分配的可用内存,尽管 ZONE_MOVABLE 中仍有大量可用内存。

通常,MOVABLE:KERNEL 的比率高达 3:1 甚至 4:1 都没问题。由于内存映射的开销,63:1 的比率绝对不可能。

实际的安全区域比率取决于工作负载。极端情况(例如,长期过度固定页面)可能根本无法处理 ZONE_MOVABLE。

注意

内核区域中的 CMA 内存基本上就像 ZONE_MOVABLE 中的内存一样运行,并且适用类似的注意事项,尤其是在将 CMA 与 ZONE_MOVABLE 组合使用时。

ZONE_MOVABLE 大小调整注意事项

我们通常预计大部分可用系统 RAM 实际上将被用户空间消耗,无论是直接还是通过页面缓存间接消耗。在正常情况下,ZONE_MOVABLE 可以在分配此类页面时正常使用。

考虑到这一点,我们有很大一部分系统 RAM 由 ZONE_MOVABLE 管理是有意义的。但是,使用 ZONE_MOVABLE 时需要考虑一些事项,尤其是在微调区域比率时

  • 拥有大量离线内存块。即使是离线内存块也会在直接映射中消耗元数据和页表的内存;不过,拥有大量离线内存块并非典型情况。

  • 没有气球压缩的内存气球与 ZONE_MOVABLE 不兼容。只有一些实现(例如 virtio-balloon 和 pseries CMM)完全支持气球压缩。

    此外,CONFIG_BALLOON_COMPACTION 内核配置选项可能会被禁用。在这种情况下,气球膨胀将仅执行不可移动的分配,并默默地创建区域不平衡,通常由虚拟机监控程序的膨胀请求触发。

  • 超大页面是不可移动的,导致用户空间消耗大量不可移动的内存。

  • 当架构不支持大页面迁移时,大页面是不可移动的,从而导致与超大页面类似的问题。

  • 页表是不可移动的。过度的交换、映射极大的文件或 ZONE_DEVICE 内存可能会有问题,尽管只在极端情况下才真正相关。当我们管理大量已换出或从文件/持久内存/...提供的用户空间内存时,我们仍然需要大量的页表来管理一旦用户空间访问该内存的内存。

  • 在某些 DAX 配置中,设备内存的内存映射将从内核区域分配。

  • KASAN 可能有很大的内存开销,例如,消耗系统总内存大小的 1/8 作为(不可移动的)跟踪元数据。

  • 长期固定页面。依赖长期固定的技术(尤其是 RDMA 和 vfio/mdev)在本质上与 ZONE_MOVABLE 存在问题,因此,与内存离线存在问题。固定的页面不能驻留在 ZONE_MOVABLE 上,因为这将使这些页面不可移动。因此,它们必须在固定的同时从该区域迁移出去。即使 ZONE_MOVABLE 中有大量可用内存,固定页面也可能会失败。

    此外,使用 ZONE_MOVABLE 可能会由于页面迁移开销而使页面固定更昂贵。

默认情况下,启动时配置的所有内存都由内核区域管理,并且不使用 ZONE_MOVABLE。

要启用 ZONE_MOVABLE 以包含启动时存在的内存并控制可移动和内核区域之间的比率,有两个命令行选项:kernelcore=movablecore=。请参阅 内核的命令行参数 获取其说明。

内存离线和 ZONE_MOVABLE

即使有 ZONE_MOVABLE,在某些极端情况下,内存块的离线也可能会失败

  • 具有内存空洞的内存块;这适用于启动期间存在的内存块,并且适用于通过 XEN 气球和 Hyper-V 气球热插拔的内存块。

  • 单个内存块中的混合 NUMA 节点和混合区域会阻止内存离线;这仅适用于启动期间存在的内存块。

  • 系统阻止离线的特殊内存块。示例包括 arm64 上启动期间可用的任何内存或跨越 s390x 上 crashkernel 区域的内存块;这通常仅适用于启动期间存在的内存块。

  • 与 CMA 区域重叠的内存块无法离线,这仅适用于启动期间存在的内存块。

  • 在同一物理内存区域上运行的并发活动(例如分配超大页面)可能会导致临时离线失败。

  • 在溶解大页面时内存不足,尤其是在启用 HugeTLB Vmemmap 优化 (HVO) 时。

    离线代码可能能够迁移大页面的内容,但可能无法溶解源大页面,因为它无法为 vmemmap 分配(不可移动的)页面,因为系统可能在内核区域中没有剩余的可用内存。

    依赖于内存离线以成功处理可移动区域的用户应仔细考虑,此功能带来的内存节省是否值得在某些情况下可能无法离线内存的风险。

此外,在迁移页面时遇到内存不足的情况,或者在 ZONE_MOVABLE 中仍然遇到永久不可移动的页面(-> BUG)时,内存离线将不断重试,直到最终成功。

当从用户空间触发离线时,可以通过发送信号来终止离线上下文。基于超时的离线可以通过以下方式轻松实现

% timeout $TIMEOUT offline_block | failure_handling