当前位置:首页>Linux>动态扩容与缩减:Linux 内存热插拔流程与内存块迁移机制

动态扩容与缩减:Linux 内存热插拔流程与内存块迁移机制

  • 2026-10-11 08:25:38
动态扩容与缩减:Linux 内存热插拔流程与内存块迁移机制

本文基于 Linux 7.0 源码,文件路径均相对于内核根目录。 核心文件:mm/memory_hotplug.c(2432 行)、mm/migrate.c、mm/page_alloc.c、include/linux/memory_hotplug.h、drivers/base/memory.c

1. 一个具体的痛点

假设你管理一个 256 节点的 KVM 集群,每台宿主机 64GB 物理内存。某个月底:

  • 节点 #127 跑满了,但上面有 3 个不能中断的生产容器

  • 节点 #088 空闲率 80%,你希望把它的内存拔给 #127 用

如果没有内存热插拔,你只有两个选择:

  1. 关机插内存 → 生产服务中断

  2. 关机迁移 VM → 停机时间 + 数据迁移开销

有了内存热插拔:echo offline > /sys/devices/system/memory/memoryX/state 触发页面迁移,内核把这块内存上的所有已分配页面搬走,然后物理拆除映射。全程不需要重启、不需要停服务。

现代场景中,这个机制更是 CXL 内存扩展、virtio-mem 动态内存调整、以及云厂商按需弹性的基础设施。

2. 本文目标

本文以 mm/memory_hotplug.c 为核心,分析三个问题:

  1. 热添加/热移除的完整执行流程:从 sysfs/驱动 API 到物理映射建立/拆除

  2. offline 阶段的页面迁移机制:如何把已分配的页面从一个物理区域搬到另一个

  3. 设计取舍:为什么必须是两阶段(offline → remove)?迁移失败怎么办?哪些页面不可迁移?

心智模型:内存热插拔回答的是同一个问题 —— 如何在不重启的情况下增减物理内存。 区别在于两个方向的操作难度不对称:

方向
核心问题
关键机制
热添加 (hot-add)
建立物理→虚拟映射
arch_add_memory()
 → sparse_add_section()
热移除 (hot-remove)
先把页面搬走,再拆除映射
offline_pages()
 → migrate_pages() → __remove_pages()

热添加是单向操作:只要地址空间合法,直接建立映射即可。 热移除是双向操作:必须先确认没有人在用这块内存,才能安全拆除。这就是两阶段设计的根本原因。

3. 前置知识

  • SPARSEMEM 内存模型:唯一支持热插拔的内存模型,以 section 为粒度(x86_64 上 128MB–512MB,取决于 CONFIG_SECTION_SIZE_BITS)。FLATMEM 不支持热插拔。

  • struct page / vmemmap:每个物理 page 对应一个 struct page,vmemmap 是虚拟地址空间中预分配的 struct page 区域。

  • ZONE_MOVABLE:可迁移页面集中在此 zone,是内存 offlining 的前提。

  • Folio:Linux 7.0 中页面迁移的基本单位已从 page 升级为 folio。

信息来源:wiki/concepts/mm-sparsemem(SPARSEMEM section 操作)、wiki/concepts/mm-page-migration(页面迁移机制,新创建)。

4. 关键数据结构

4.1 mhp_t — 热插拔标志位

/* include/linux/memory_hotplug.h:35 */typedef int __bitwise mhp_t;#define MHP_NONE              ((__force mhp_t)0)#define MHP_MERGE_RESOURCE    ((__force mhp_t)BIT(0))#define MHP_MEMMAP_ON_MEMORY  ((__force mhp_t)BIT(1))#define MHP_NID_IS_MGID       ((__force mhp_t)BIT(2))
标志
含义
MHP_MERGE_RESOURCE
将新加内存的 iomem resource 与相邻区域合并
MHP_MEMMAP_ON_MEMORY
vmemmap 页面从新加内存自身分配(而非已有内存),减少对已有内存的压力
MHP_NID_IS_MGID
nid 参数实际是 memory group ID,需要解析为实际 node

4.2 memory_block — 用户态可见的热插拔单元

从用户态 /sys/devices/system/memory/ 看到的就是 memory block。它是热插拔的最小可见单元,通常等于一个或多个 memory section 的大小。

每个 memory block 有独立的 online/offline 状态,由 drivers/base/memory.c 管理。

4.3 memory_group — 设备内存分组

memory_group 是 Linux 7.0 引入的新机制,用于支持 CXL 等设备的物理内存分组管理。同一 group 内的 memory block 共享 online 策略。

5. Kconfig 依赖分析

5.1 MEMORY_HOTPLUG

# mm/Kconfigmenuconfig MEMORY_HOTPLUG    bool "Memory hotplug"    select MEMORY_ISOLATION    depends on SPARSEMEM              ← 只有 SPARSEMEM 支持    depends on ARCH_ENABLE_MEMORY_HOTPLUG  ← 架构级门控    depends on 64BIT                  ← 仅 64 位系统    select NUMA_KEEP_MEMINFO if NUMA

三个硬性依赖:

  1. SPARSEMEM:只有 SPARSEMEM 支持 section 级别的按需分配/释放

  2. ARCH_ENABLE_MEMORY_HOTPLUG:架构必须显式声明支持

  3. 64BIT:32 位系统虚拟地址空间不足,无法支持 vmemmap 方案

5.2 MEMORY_HOTREMOVE

config MEMORY_HOTREMOVE    bool "Allow for memory hot remove"    select HAVE_BOOTMEM_INFO_NODE if (X86_64 || PPC64)    depends on MEMORY_HOTPLUG && ARCH_ENABLE_MEMORY_HOTREMOVE    depends on MIGRATION              ← 需要页面迁移支持

关键依赖 MIGRATION:没有页面迁移,offline 步骤无法执行,hot-remove 也就无从谈起。

5.3 架构支持对照表

架构
HOTPLUG
HOTREMOVE
备注
x86_64
if X86_64if MEMORY_HOTPLUG
最完整支持
arm64
无条件
无条件
完整支持
loongarch
无条件
无条件
完整支持
powerpc
无条件
无条件
完整支持
s390
if SPARSEMEM
无条件
需要 SPARSEMEM
riscv
if SPARSEMEM_VMEMMAPif MEMORY_HOTPLUG
需要 vmemmap

本锁架构(不启用):alpha, arc, arm(32), csky, hexagon, m68k, microblaze, mips, nios2, openrisc, parisc, sh, sparc, um, xtensa

5.4 子选项

config MHP_MEMMAP_ON_MEMORY    def_bool y    depends on MEMORY_HOTPLUG && SPARSEMEM_VMEMMAP && ARCH_MHP_MEMMAP_ON_MEMORY_ENABLE

当此选项开启时,热添加内存的 vmemmap 页面从新内存自身分配,而不是从已有内存分配。这对大块内存 hotplug 场景意义重大 —— 减少了对已有内存的压力。

6. 调用链

上图展示了内存热插拔的三条核心路径:热添加路径从上到下建立映射,热移除路径先检查 offline 状态再拆除映射,offline 迁移循环(左下)反复执行 scan→migrate→isolate 直到所有页面可迁移。

6.1 热添加路径

sysfs write / ACPI event / virtio-mem driver        │        ▼    add_memory(nid, start, size, mhp_flags)        │ lock_device_hotplug()        ▼    __add_memory(nid, start, size, mhp_flags)        │        ├── register_memory_resource(start, size, "System RAM")        │       → mhp_range_allowed()          // 检查物理地址合法性        │       → __request_region()           // 在 /proc/iomem 中保留        │        ▼    add_memory_resource(nid, res, mhp_flags)        │        ├── check_hotplug_memory_range()       // 对齐检查        ├── mem_hotplug_begin()                // percpu 写锁 + CPU hotplug 读锁        ├── memblock_add_node()                // 注册到 memblock 分配器        ├── __try_online_node()                // 如 node 首次出现,初始化        ├── [分支:MHP_MEMMAP_ON_MEMORY]        │       → create_altmaps_and_memory_blocks()  // vmemmap 在新内存上        │   [否则]        │       → arch_add_memory() + create_memory_block_devices()        ├── register_memory_blocks_under_node_hotplug()        ├── firmware_map_add_hotplug()        ├── mem_hotplug_done()        └── [自动上线] mhp_get_default_online_type() != MMOP_OFFLINE                → online_memory_block()

6.2 热移除路径

用户态 sysfs / driver 请求        │        ▼    offline_pages(start_pfn, nr_pages, zone, group)        │ 迁移循环(见第 8 节)        ▼    __offline_isolated_pages()           // 从 buddy 移除隔离页面    remove_pfn_range_from_zone()        │        ▼    remove_memory(start, size)        │ lock_device_hotplug()        ▼    try_remove_memory(start, size)        │        ├── walk_memory_blocks() + check_memblock_offlined_cb()  // 检查全部 offline        ├── firmware_map_remove()        ├── mem_hotplug_begin()        ├── [无 altmap] remove_memory_block_devices() + arch_remove_memory()        │   [有 altmap] remove_memory_blocks_and_altmaps()        ├── memblock_remove()            // 如 CONFIG_ARCH_KEEP_MEMBLOCK        ├── release_mem_region_adjustable()        ├── try_offline_node(nid)        // node 无内存时 offline        └── mem_hotplug_done()

6.3 组合操作:offline + remove

/* mm/memory_hotplug.c */offline_and_remove_memory()    ├── walk_memory_blocks() + try_offline_memory_block()  // 逐个 offline    ├── try_remove_memory()                                // 物理移除    └── [部分失败] try_reonline_memory_block()             // 回滚

支持故障回滚:如果 offline 中途失败,会把已 offline 的 block 重新 online。

7. 核心执行流程:热添加走一遍

以添加 1GB 内存到 node 0 为例(x86_64, section = 128MB, 共 8 个 section):

add_memory(0, 0x100000000, 0x40000000, MHP_NONE)    │    │ 步骤 1: __add_memory() 注册 iomem resource    │   register_memory_resource(0x100000000, 0x40000000, "System RAM")    │   → /proc/iomem 中出现: "100000000-13fffffff : System RAM"    │    │ 步骤 2: add_memory_resource() 核心逻辑    │   check_hotplug_memory_range()     ← 检查 1GB 对齐到 section    │   mem_hotplug_begin()              ← 获取 percpu 写锁    │   memblock_add_node(0, 0x100000000, 0x40000000)  ← 注册到 memblock    │   __try_online_node(0)             ← node 0 可能已经 online,跳过    │    │ 步骤 3: MHP_MEMMAP_ON_MEMORY 分支    │   如果设置了 MHP_MEMMAP_ON_MEMORY:    │     create_altmaps_and_memory_blocks()    │       逐 memory block:    │         构造 vmem_altmap    │         arch_add_memory(0, block_start, block_size, &params)    │           → __add_pages() 按 section 迭代:    │               for each section:    │                   sparse_add_section()  ← 创建 struct page + 页表映射    │         create_memory_block_devices()  ← 创建 sysfs 设备节点    │   否则:    │     arch_add_memory() + create_memory_block_devices()    │    │ 步骤 4: 注册到 node    │   register_memory_blocks_under_node_hotplug()    │   firmware_map_add_hotplug()    │    │ 步骤 5: 自动上线    │   如果 mhp_get_default_online_type() != MMOP_OFFLINE:    │     online_memory_block()  ← 内存立即可用于分配    │    │ 步骤 6: 释放锁    │   mem_hotplug_done()

关键决策点:MHP_MEMMAP_ON_MEMORY 标志决定了 vmemmap 页面从哪里分配。如果开启,1GB 新内存的 vmemmap 页面(约 8MB)从这 1GB 自身分配,减少了 8MB 的已有内存压力。如果关闭,则从已有内存分配。

8. 核心执行流程:offline 迁移循环

offline_pages() 是热移除的核心。它不是直接拆除映射,而是先让这块内存上的所有页面"搬家"。

8.1 offline 阶段概览

/* mm/memory_hotplug.c:1898 */int offline_pages(unsigned long start_pfn, unsigned long nr_pages,                  struct zone *zone, struct memory_group *group)

执行步骤:

阶段
代码行
说明
1. 边界检查
L1925
pageblock_aligned()
 + section 对齐
2. 空洞检查
L1937
walk_system_ram_range()
 — 确保范围内无空洞
3. 单 zone 检查
L1950
确认整个范围属于同一个 zone
4. PCP/LRU 禁用
L1961
zone_pcp_disable()
 + lru_cache_disable() — 防止并发释放
5. 页面隔离
L1965
start_isolate_page_range()
 — 设置 pageblock 隔离标志(MIGRATE_ISOLATE)
6. Node 通知
L1977
如果这是 node 最后的内存,通知 NODE_REMOVING_LAST_MEMORY
7. MEM_GOING_OFFLINE
L1987
通知 memory notifier chain
8. 迁移循环
L1994
核心循环 — 反复执行直到所有页面可迁移
9. 从 buddy 移除
L2042
__offline_isolated_pages()
 — 将隔离页面从 buddy 删除
10. 清理恢复
L2050
LRU/PCP 恢复、watermark 重算、kswapd 停止等

8.2 迁移循环详解

这是 offline 的心脏,反复执行直到所有页面迁移完毕:

/* mm/memory_hotplug.c:1994-2039 — 核心迁移循环 */do {    /* 可中断 — 用户可 Ctrl+C 终止长时间 offlining */    if (signal_pending(current)) {        ret = -EINTR;        goto failed_removal;    }    /* 设置 offlining 标志,告知其他子系统正在进行 offline */    set_mem_offlining(start_pfn, end_pfn);    /* 扫描可迁移页面:LRU 页、movable_ops 页、hugetlb 页 */    pfn = start_pfn;    do {        ret = scan_movable_pages(pfn, end_pfn, &pfn);        if (ret)            break;        /* 实际页面迁移 */        do_migrate_range(pfn, end_pfn);        cond_resched();    } while (pfn < end_pfn);    /* 释放 free hugetlb folio */    dissolve_free_hugetlb_folios(start_pfn, end_pfn);    /* 清除 offlining 标志 */    unset_mem_offlining(start_pfn, end_pfn);    /* 验证:所有页面是否已成功隔离? */    ret = test_pages_isolated(start_pfn, end_pfn, false);} while (ret == -EBUSY);

设计要点:这个循环是可中断的(signal_pending 检查),允许用户在 offlining 时间过长时终止操作。

8.3 scan_movable_pages — 判断哪些页面能搬

/* mm/memory_hotplug.c:1742 */static int scan_movable_pages(unsigned long start, unsigned long end,                              unsigned long *movable_pfn){    for (pfn = start; pfn < end; pfn++) {        page = pfn_to_page(pfn);        /* PageLRU → 可迁移(匿名页或 page cache) */        if (PageLRU(page) || page_has_movable_ops(page))            goto found;        /* PageOffline + refcount > 0 → 不可迁移 */        if (PageOffline(page) && page_count(page))            return -EBUSY;        /* hugetlb + migratable → 可迁移 */        folio = page_folio(page);        if (!folio_test_hugetlb(folio))            continue;        if (folio_test_hugetlb_migratable(folio))            goto found;        /* 跳过巨大的 hugetlb 块 */        pfn |= nr_pages - 1;    }    return -ENOENT;found:    *movable_pfn = pfn;    return 0;}

判断逻辑:

条件
结果
PageLRU(page)
可迁移 — 匿名页或 page cache
page_has_movable_ops(page)
可迁移 — 驱动显式标记为 movable
PageOffline(page) && page_count(page) > 0不可迁移
 — 返回 -EBUSY
hugetlb && migratable
可迁移
其他
跳过

关键设计:PageOffline 页面如果还有引用计数,说明有驱动在使用,绝对不能迁移。这是 offline 失败的常见原因。

8.4 do_migrate_range — 实际执行迁移

/* mm/memory_hotplug.c:1789 */static void do_migrate_range(unsigned long start_pfn, unsigned long end_pfn){    LIST_HEAD(source);    /* 1. 遍历 pfn 范围,对每个 folio 尝试获取引用 */    for (pfn = start_pfn; pfn < end_pfn;) {        folio = page_folio(pfn_to_page(pfn));        if (!folio_try_get(folio)) { pfn++; continue; }        /* 2. 跳过 poisoned huge folio */        if (folio_test_hugetlb(folio) && folio_test_hwpoisoned(folio))            goto next;        /* 3. 隔离到迁移列表 */        if (!isolate_folio_to_list(folio, &source))            goto next;        pfn += folio_nr_pages(folio);next:        folio_put(folio);    }    /* 4. 用 MIGRATE_SYNC 模式迁移整个列表 */    if (!list_empty(&source)) {        ret = migrate_pages(&source, alloc_migration_target, NULL, 0,                            MIGRATE_SYNC, MR_MEMORY_HOTPLUG, NULL);        /* 5. 失败时回退 */        if (ret)            putback_movable_pages(&source);    }}

迁移模式:offline 阶段使用 MIGRATE_SYNC(可以阻塞),因为它是在用户态触发的操作,可以等待 I/O 完成。与 compaction 的 MIGRATE_ASYNC(不能阻塞)形成对比。

8.5 __offline_isolated_pages — 从 buddy 移除

当迁移循环完成后,所有页面要么被搬走,要么是 free 的但被隔离了。__offline_isolated_pages() 把这些隔离页面从 buddy allocator 中永久移除:

/* mm/page_alloc.c:7368 */unsigned long __offline_isolated_pages(unsigned long start_pfn,                                        unsigned long end_pfn)

它遍历隔离的 pageblock,从 zone 的 free_area 中移除页面,并更新 managed_pages 计数。此后,这块内存的 PFN 范围不再参与 buddy 分配。

9. 分支路径分析

9.1 错误路径 — add_memory_resource

/* mm/memory_hotplug.c:1490 */int add_memory_resource(int nid, struct resource *res, mhp_t mhp_flags){    /* ... 正常流程 ... */    /* 错误回滚:按依赖关系倒序释放 */    error:        if (new_node) { node_set_offline(); unregister_node(); }    error_memblock_remove:        memblock_remove(nid, start, size);    error_mem_hotplug_end:        mem_hotplug_done();        release_memory_resource(res);        return ret;}

关键设计:错误回滚按依赖关系倒序进行(后建立的先拆除),确保状态一致性。

9.2 错误路径 — try_remove_memory

/* mm/memory_hotplug.c:2234 */static int try_remove_memory(u64 start, u64 size){    /* 1. 检查所有 memory block 是否已 offline */    walk_memory_blocks(start, size, &nid, check_memblock_offlined_cb);    if (ret)        return -EBUSY;  /* 有 block 仍然 online */    /* 2. 只有在全部 offline 后才开始物理拆除 */    mem_hotplug_begin();    /* ... arch_remove_memory, memblock_remove, ... */    mem_hotplug_done();    return 0;}

关键约束:try_remove_memory() 是"尽力而为"式的 —— 如果任何 block 未 offline,直接返回 -EBUSY,不会强制拆除。这保证了内核不会在仍有页面被引用的情况下物理移除内存。

9.3 不可迁移页面的处理

/* mm/memory_hotplug.c:2010-2013 */ret = scan_movable_pages(pfn, end_pfn, &pfn);if (ret)    break;  /* -EBUSY 或 -ENOENT → 退出迁移循环 */

如果 scan 返回 -EBUSY(有 PageOffline 页面且 refcount > 0),迁移循环立即终止,offline 失败。

Open Question: 当有不可迁移页面(如驱动分配的 offline 页面)时,当前实现直接返回失败。是否有优雅的回退机制或通知机制告知用户哪些 page 不可迁移?需进一步确认。

10. 并发与生命周期分析

10.1 锁层次结构

锁
类型
保护内容
获取位置
device_hotplug_lock
mutex(全局)
串行化所有 hotplug 操作
add_memory()
 / remove_memory() 最外层
mem_hotplug_lock
percpu 读写锁
memory 状态变更(on/offline)
mem_hotplug_begin/end()
cpus_read_lock()
CPU hotplug 读锁
CPU 热插拔期间的稳定性
mem_hotplug_begin()
 内
zone->lock
spinlock
zone 级统计(nr_isolate_pageblock)
online_pages()
 / offline_pages() 内

调用栈约束:

add_memory() / remove_memory()    ├── device_hotplug_lock (mutex — 最外层)    └── mem_hotplug_lock (percpu write — 中间层)        └── zone->lock (spinlock — 最内层)

offline_pages() 和 online_pages() 在 device_hotplug_lock 之下、mem_hotplug_lock 保护下被调用。这种层次结构确保了:

  1. 同一时刻只有一个 hotplug 操作在进行

  2. 状态变更期间 CPU 不会热插拔

  3. zone 级别的统计操作是原子的

10.2 lru_cache_disable — 为什么需要全局禁用 LRU

/* offline_pages() */zone_pcp_disable(zone);     /* 禁用 per-CPC page list */lru_cache_disable();        /* 全局禁用 LRU cache */

lru_cache_disable() 是全局操作 —— 它会阻塞所有 CPU 的 LRU 添加操作。这是因为 offline 需要精确控制哪些页面在隔离范围内,如果不禁用 LRU,其他 CPU 可能把页面放到隔离范围内的 LRU list 上,导致隔离失败。

11. Kconfig 子选项与相关子系统

11.1 MHP_MEMMAP_ON_MEMORY

config MHP_MEMMAP_ON_MEMORY    def_bool y    depends on MEMORY_HOTPLUG && SPARSEMEM_VMEMMAP && ARCH_MHP_MEMMAP_ON_MEMORY_ENABLE

默认开启(def_bool y)。当热添加 1GB 内存时,约 8MB 的 vmemmap 页面从新内存自身分配。如果不从自身分配,这 8MB 就要从已有内存中挤出来,可能导致已有内存紧张。

11.2 ZONE_DEVICE

config ZONE_DEVICE    bool "Device memory (pmem, HMM, etc.) hotplug"    depends on MEMORY_HOTPLUG && MEMORY_HOTREMOVE && SPARSEMEM_VMEMMAP && ARCH_HAS_PTE_DEVMAP

为持久化内存(PMEM)和 HMM(异构内存管理)提供 zone。它通过 SPARSEMEM 的 subsection 机制支持设备内存热插拔。

11.3 依赖 MEMORY_HOTPLUG 的关键子系统

子系统
Kconfig
用途
DAX
drivers/dax/Kconfig:68
需要 add_memory() 映射 DAX 设备
virtio-mem
drivers/virtio/Kconfig:127
虚拟机动态内存调整
ACPI
drivers/acpi/Kconfig:417
ACPI 内存热插拔事件处理
XEN_BALLOON
drivers/xen/Kconfig:15
Xen balloon 内存热插拔

12. 调试与验证方法

12.1 sysfs 操作

查看 memory block 状态:

# 工作目录: 任意,需 root 权限ls /sys/devices/system/memory/# 输出: memory0  memory128  memory256  ...  block_size_bytes# 查看某个 memory block 的状态cat /sys/devices/system/memory/memory128/state# 输出: online 或 offlinecat /sys/devices/system/memory/memory128/phys_device# 输出: 0(表示物理设备编号)cat /sys/devices/system/memory/memory128/removable# 输出: 1(可移除)或 0(不可移除)

手动 offline 一个 memory block:

# 工作目录: 任意,需要 root 权限# 注意:这会触发页面迁移,可能需要数分钟echo offline > /sys/devices/system/memory/memory128/state

查看 memory block 大小:

cat /sys/devices/system/memory/block_size_bytes# 输出: 134217728 (128MB,x86_64 默认)

12.2 ftrace 追踪 offline 流程

# 工作目录: /sys/kernel/tracing/,需要 root 权限cd /sys/kernel/tracing/# 启用 memory hotplug 相关 tracepointecho 1 > events/memory_hotplug/enable# 追踪 offline_pages 函数echo offline_pages > set_ftrace_filterecho function > current_tracer# 触发 offline(在另一个终端)echo offline > /sys/devices/system/memory/memory128/state# 查看 tracecat trace

12.3 验证在线内存总量

# 工作目录: 任意# online 前cat /proc/meminfo | grep MemTotal# MemTotal: 65536000 kB# offline 128MB 后cat /proc/meminfo | grep MemTotal# MemTotal: 65404928 kB  (减少了约 128MB)

12.4 /proc/iomem 验证

# 工作目录: 任意# hot-add 前grep "System RAM" /proc/iomem# 100000000-23fffffff : System RAM  (4GB)# hot-add 1GB 后grep "System RAM" /proc/iomem# 100000000-27fffffff : System RAM  (5GB — 增加了 1GB)

12.5 测试环境说明

项目
要求
CPU 架构
x86_64 或 arm64(支持 MEMORY_HOTPLUG)
内核配置
CONFIG_MEMORY_HOTPLUG=y
, CONFIG_MEMORY_HOTREMOVE=y, CONFIG_SPARSEMEM_VMEMMAP=y, CONFIG_MIGRATION=y
内存
至少 2GB 以上(需要有至少一个完整的 memory block 可以 offline)
权限
root(操作 sysfs memory block 需要 root)
验证方式
对比 offline 前后 /proc/meminfo 的 MemTotal 值

13. 实践场景:何时需要关心内存热插拔

场景
涉及的机制
为什么重要
CXL 内存扩展
add_memory()
 + MHP_MEMMAP_ON_MEMORY
CXL 设备动态添加内存时,需要 vmemmap 自分配以减少对已有内存的压力
virtio-mem 动态调整
add_memory()
 + online_pages() + offline_pages()
虚拟机内存按需扩缩,host 驱动通过热插拔 API 调整 guest 内存
驱动开发:不可迁移页面
PageOffline
 + refcount
驱动分配的 offline 页面如果没释放,会阻塞整个 memory block 的 offline
云厂商弹性伸缩
memory_group
 + offline/online 策略
不同 group 的 memory block 有不同 online 策略,支持精细化资源管理
内核 oops 调试
mem_hotplug_lock
 + notifier chain
hotplug 期间的 crash 可能与锁竞争或 notifier 回调有关
大页内存与 offlining
dissolve_free_hugetlb_folios()
free hugetlb 页面会被自动释放,但 in-use hugetlb 页面会阻塞 offlining

14. 总结

内存热插拔的核心机制可以归纳为三句话:

  1. 热添加是建立映射:add_memory() → arch_add_memory() → sparse_add_section() 建立物理→虚拟映射,创建 sysfs 设备节点。

  2. 热移除是两阶段操作:先 offline_pages() 迁移所有页面(scan → do_migrate_range → migrate_pages),确认没有人在用后,才 remove_memory() → __remove_pages() → sparse_remove_section() 拆除映射。

  3. 迁移是离线成功的前提:MIGRATION Kconfig 是 MEMORY_HOTREMOVE 的硬依赖。不可迁移的页面(如 PageOffline + refcount > 0)是 offline 失败的常见原因。

设计取舍:

  • 两阶段分离:offline(逻辑下线/迁移)和 remove(物理拆除映射)分离,保证了安全性。remove_memory() 要求先 offline,offline_and_remove_memory() 是带回滚的便捷封装。

  • 可中断 offlining:迁移循环检查 signal_pending(),允许长时间操作被用户中断。

  • vmemmap 自分配:MHP_MEMMAP_ON_MEMORY 在新增内存上分配 vmemmap 页面,减少对已有内存的压力,是大块内存 hotplug 的默认行为。

  • 故障回滚:offline_and_remove_memory() 在部分失败时会重新 online 已 offline 的 block,保证状态一致性。

Open Question: 当有不可迁移页面时,当前实现直接返回失败。是否有优雅的回退机制或通知机制告知用户哪些 page 不可迁移?需要在有实际驱动开发的场景下进一步确认。

15. 参考资料

  • mm/memory_hotplug.c — Linux 7.0, 2432 行

  • mm/migrate.c — 页面迁移引擎

  • mm/page_alloc.c:7368 — __offline_isolated_pages()

  • mm/compaction.c — 内存整理(复用迁移引擎)

  • include/linux/memory_hotplug.h — mhp_t 标志、API 声明

  • include/linux/migrate_mode.h — migrate_mode 枚举

  • drivers/base/memory.c — memory block 设备模型

  • mm/Kconfig:536-580 — MEMORY_HOTPLUG / MEMORY_HOTREMOVE 定义

  • wiki/concepts/mm-sparsemem — SPARSEMEM section 操作

  • wiki/concepts/mm-page-migration — 页面迁移机制(新创建)

最新文章

随机文章