本文基于 Linux 7.0 源码,文件路径均相对于内核根目录。 核心文件:
mm/memory_hotplug.c(2432 行)、mm/migrate.c、mm/page_alloc.c、include/linux/memory_hotplug.h、drivers/base/memory.c
假设你管理一个 256 节点的 KVM 集群,每台宿主机 64GB 物理内存。某个月底:
如果没有内存热插拔,你只有两个选择:
关机插内存 → 生产服务中断
关机迁移 VM → 停机时间 + 数据迁移开销
有了内存热插拔:echo offline > /sys/devices/system/memory/memoryX/state 触发页面迁移,内核把这块内存上的所有已分配页面搬走,然后物理拆除映射。全程不需要重启、不需要停服务。
现代场景中,这个机制更是 CXL 内存扩展、virtio-mem 动态内存调整、以及云厂商按需弹性的基础设施。
本文以 mm/memory_hotplug.c 为核心,分析三个问题:
热添加/热移除的完整执行流程:从 sysfs/驱动 API 到物理映射建立/拆除
offline 阶段的页面迁移机制:如何把已分配的页面从一个物理区域搬到另一个
设计取舍:为什么必须是两阶段(offline → remove)?迁移失败怎么办?哪些页面不可迁移?
心智模型:内存热插拔回答的是同一个问题 —— 如何在不重启的情况下增减物理内存。 区别在于两个方向的操作难度不对称:
方向 核心问题 关键机制 热添加 (hot-add) 建立物理→虚拟映射 arch_add_memory()→ sparse_add_section()热移除 (hot-remove) 先把页面搬走,再拆除映射 offline_pages()→ migrate_pages()→__remove_pages()
热添加是单向操作:只要地址空间合法,直接建立映射即可。 热移除是双向操作:必须先确认没有人在用这块内存,才能安全拆除。这就是两阶段设计的根本原因。
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(页面迁移机制,新创建)。
/* 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 | |
MHP_MEMMAP_ON_MEMORY | |
MHP_NID_IS_MGID |
从用户态 /sys/devices/system/memory/ 看到的就是 memory block。它是热插拔的最小可见单元,通常等于一个或多个 memory section 的大小。
每个 memory block 有独立的 online/offline 状态,由 drivers/base/memory.c 管理。
memory_group 是 Linux 7.0 引入的新机制,用于支持 CXL 等设备的物理内存分组管理。同一 group 内的 memory block 共享 online 策略。
# mm/Kconfigmenuconfig MEMORY_HOTPLUGbool "Memory hotplug"select MEMORY_ISOLATIONdepends on SPARSEMEM ← 只有 SPARSEMEM 支持depends on ARCH_ENABLE_MEMORY_HOTPLUG ← 架构级门控depends on 64BIT ← 仅 64 位系统select NUMA_KEEP_MEMINFO if NUMA
三个硬性依赖:
SPARSEMEM:只有 SPARSEMEM 支持 section 级别的按需分配/释放
ARCH_ENABLE_MEMORY_HOTPLUG:架构必须显式声明支持
64BIT:32 位系统虚拟地址空间不足,无法支持 vmemmap 方案
config MEMORY_HOTREMOVEbool "Allow for memory hot remove"select HAVE_BOOTMEM_INFO_NODE if (X86_64 || PPC64)depends on MEMORY_HOTPLUG && ARCH_ENABLE_MEMORY_HOTREMOVEdepends on MIGRATION ← 需要页面迁移支持
关键依赖 MIGRATION:没有页面迁移,offline 步骤无法执行,hot-remove 也就无从谈起。
if X86_64 | if MEMORY_HOTPLUG | ||
if SPARSEMEM | |||
if SPARSEMEM_VMEMMAP | if MEMORY_HOTPLUG |
本锁架构(不启用):alpha, arc, arm(32), csky, hexagon, m68k, microblaze, mips, nios2, openrisc, parisc, sh, sparc, um, xtensa
config MHP_MEMMAP_ON_MEMORYdef_bool ydepends on MEMORY_HOTPLUG && SPARSEMEM_VMEMMAP && ARCH_MHP_MEMMAP_ON_MEMORY_ENABLE
当此选项开启时,热添加内存的 vmemmap 页面从新内存自身分配,而不是从已有内存分配。这对大块内存 hotplug 场景意义重大 —— 减少了对已有内存的压力。

上图展示了内存热插拔的三条核心路径:热添加路径从上到下建立映射,热移除路径先检查 offline 状态再拆除映射,offline 迁移循环(左下)反复执行 scan→migrate→isolate 直到所有页面可迁移。
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()
用户态 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()
/* 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。
以添加 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, ¶ms)│ → __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 的已有内存压力。如果关闭,则从已有内存分配。
offline_pages() 是热移除的核心。它不是直接拆除映射,而是先让这块内存上的所有页面"搬家"。
/* mm/memory_hotplug.c:1898 */int offline_pages(unsigned long start_pfn, unsigned long nr_pages,struct zone *zone, struct memory_group *group)
执行步骤:
pageblock_aligned() | ||
walk_system_ram_range() | ||
zone_pcp_disable()lru_cache_disable() — 防止并发释放 | ||
start_isolate_page_range() | ||
NODE_REMOVING_LAST_MEMORY | ||
__offline_isolated_pages() | ||
这是 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 时间过长时终止操作。
/* 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_has_movable_ops(page) | |
PageOffline(page) && page_count(page) > 0 | 不可迁移 |
hugetlb && migratable | |
关键设计:
PageOffline页面如果还有引用计数,说明有驱动在使用,绝对不能迁移。这是 offline 失败的常见原因。
/* 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(不能阻塞)形成对比。
当迁移循环完成后,所有页面要么被搬走,要么是 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 分配。
/* 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;}
关键设计:错误回滚按依赖关系倒序进行(后建立的先拆除),确保状态一致性。
/* 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,不会强制拆除。这保证了内核不会在仍有页面被引用的情况下物理移除内存。
/* 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 不可迁移?需进一步确认。
device_hotplug_lock | add_memory()remove_memory() 最外层 | ||
mem_hotplug_lock | mem_hotplug_begin/end() | ||
cpus_read_lock() | mem_hotplug_begin() | ||
zone->lock | 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 保护下被调用。这种层次结构确保了:
同一时刻只有一个 hotplug 操作在进行
状态变更期间 CPU 不会热插拔
zone 级别的统计操作是原子的
/* 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 上,导致隔离失败。
config MHP_MEMMAP_ON_MEMORYdef_bool ydepends on MEMORY_HOTPLUG && SPARSEMEM_VMEMMAP && ARCH_MHP_MEMMAP_ON_MEMORY_ENABLE
默认开启(def_bool y)。当热添加 1GB 内存时,约 8MB 的 vmemmap 页面从新内存自身分配。如果不从自身分配,这 8MB 就要从已有内存中挤出来,可能导致已有内存紧张。
config ZONE_DEVICEbool "Device memory (pmem, HMM, etc.) hotplug"depends on MEMORY_HOTPLUG && MEMORY_HOTREMOVE && SPARSEMEM_VMEMMAP && ARCH_HAS_PTE_DEVMAP
为持久化内存(PMEM)和 HMM(异构内存管理)提供 zone。它通过 SPARSEMEM 的 subsection 机制支持设备内存热插拔。
drivers/dax/Kconfig:68 | add_memory() 映射 DAX 设备 | |
drivers/virtio/Kconfig:127 | ||
drivers/acpi/Kconfig:417 | ||
drivers/xen/Kconfig:15 |
查看 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 默认)
# 工作目录: /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
# 工作目录: 任意# online 前cat /proc/meminfo | grep MemTotal# MemTotal: 65536000 kB# offline 128MB 后cat /proc/meminfo | grep MemTotal# MemTotal: 65404928 kB (减少了约 128MB)
# 工作目录: 任意# 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)
CONFIG_MEMORY_HOTPLUG=yCONFIG_MEMORY_HOTREMOVE=y, CONFIG_SPARSEMEM_VMEMMAP=y, CONFIG_MIGRATION=y | |
/proc/meminfo 的 MemTotal 值 |
add_memory()MHP_MEMMAP_ON_MEMORY | ||
add_memory()online_pages() + offline_pages() | ||
PageOfflinerefcount | ||
memory_group | ||
mem_hotplug_lock | ||
dissolve_free_hugetlb_folios() |
内存热插拔的核心机制可以归纳为三句话:
热添加是建立映射:add_memory() → arch_add_memory() → sparse_add_section() 建立物理→虚拟映射,创建 sysfs 设备节点。
热移除是两阶段操作:先 offline_pages() 迁移所有页面(scan → do_migrate_range → migrate_pages),确认没有人在用后,才 remove_memory() → __remove_pages() → sparse_remove_section() 拆除映射。
迁移是离线成功的前提: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 不可迁移?需要在有实际驱动开发的场景下进一步确认。
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 — 页面迁移机制(新创建)