内存热插拔

本文档介绍了 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。

同样,Linux 可以通过 ACPI 收到有关热移除内存设备或 NUMA 节点的请求的通知。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_MHP_DEFAULT_ONLINE_TYPE 内核配置选项。

有关详细信息,请参阅内存块的 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”可能会由于 memmap 大小限制而导致内存浪费。例如,如果内存块的 memmap 需要 1 MiB,但 pageblock 大小为 2 MiB,则将浪费 1 MiB 的热插拔内存。请注意,在某些情况下仍然无法强制执行该功能:例如,如果 memmap 小于单个页面,或者如果架构不支持所有配置中的强制模式。

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”%,例如,允许为 8 GiB VM 热插拔 24 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 中仍有大量可用内存。

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

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

注意

内核区域的 CMA 内存本质上表现得像 ZONE_MOVABLE 中的内存,并且适用类似的考虑因素,尤其是在将 CMA 与 ZONE_MOVABLE 结合使用时。

ZONE_MOVABLE 大小调整注意事项

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

考虑到这一点,我们让 ZONE_MOVABLE 管理系统 RAM 的很大一部分是有意义的。但是,在使用 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